From c072a1df163a0c5a9c253f691541380cc2ad0ea8 Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:57:09 -0400 Subject: [PATCH] native android qr scan routing --- src/app/+native-intent.ts | 8 +++- src/app/eq/import.tsx | 95 ++++++++++++++++++++++++++++++++++++++ src/audio/eqShareIntent.ts | 29 ++++++++++++ 3 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 src/app/eq/import.tsx create mode 100644 src/audio/eqShareIntent.ts diff --git a/src/app/+native-intent.ts b/src/app/+native-intent.ts index 81ab094..5a39e45 100644 --- a/src/app/+native-intent.ts +++ b/src/app/+native-intent.ts @@ -2,6 +2,7 @@ import { getNotificationClickRedirectPath, isNotificationClickPath, } from '@/audio/notificationIntent'; +import { getEQPresetShareRedirectPath } from '@/audio/eqShareIntent'; type RedirectSystemPathEvent = { path: string; @@ -9,7 +10,10 @@ type RedirectSystemPathEvent = { }; export async function redirectSystemPath({ path }: RedirectSystemPathEvent): Promise { - if (!isNotificationClickPath(path)) return path; + if (isNotificationClickPath(path)) return getNotificationClickRedirectPath(); - return getNotificationClickRedirectPath(); + const eqShareRedirect = getEQPresetShareRedirectPath(path); + if (eqShareRedirect) return eqShareRedirect; + + return path; } diff --git a/src/app/eq/import.tsx b/src/app/eq/import.tsx new file mode 100644 index 0000000..4ce6e0a --- /dev/null +++ b/src/app/eq/import.tsx @@ -0,0 +1,95 @@ +import { useMemo } from 'react'; +import { Pressable, StyleSheet, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useLocalSearchParams, useRouter } from 'expo-router'; +import { Screen } from '@/components/Screen'; +import { Text } from '@/components/Text'; +import { EQPresetPreviewSheet } from '@/components/eq/EQPresetPreviewSheet'; +import { decodeEQPresetQr, EQ_PRESET_QR_PREFIX } from '@/audio/eqShare'; +import { genEqId } from '@/audio/eqPresets'; +import { useEQStore } from '@/stores/eqStore'; +import { radius, spacing } from '@/theme'; +import { createThemedStyles, useColors } from '@/theme/themed'; +import type { EQPreset } from '@/types/audio'; + +export default function EQPresetImportScreen() { + const styles = useStyles(); + const colors = useColors(); + const router = useRouter(); + const importPreset = useEQStore((state) => state.importPreset); + const { data } = useLocalSearchParams<{ data?: string }>(); + + const preset = useMemo(() => { + if (!data) return null; + try { + return decodeEQPresetQr(`${EQ_PRESET_QR_PREFIX}${data}`, genEqId); + } catch { + return null; + } + }, [data]); + + const goToEq = () => router.replace('/eq' as never); + + if (!preset) { + return ( + + + + + + Equalizer + + + + + + This link does not contain a valid Astra EQ preset. + + + Go to Equalizer + + + + + ); + } + + return ( + + importPreset(preset)} + onClose={goToEq} + /> + + ); +} + +const useStyles = createThemedStyles((colors) => ({ + header: { + marginTop: spacing.md, + marginBottom: spacing.lg, + }, + back: { + flexDirection: 'row', + alignItems: 'center', + gap: 2, + }, + errorCard: { + borderRadius: radius.md, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.glassBorder, + backgroundColor: colors.glassBg, + padding: spacing.lg, + gap: spacing.md, + }, + primaryButton: { + minHeight: 44, + borderRadius: radius.sm, + backgroundColor: colors.accent, + paddingHorizontal: spacing.lg, + alignItems: 'center', + justifyContent: 'center', + }, +})); diff --git a/src/audio/eqShareIntent.ts b/src/audio/eqShareIntent.ts new file mode 100644 index 0000000..afd628e --- /dev/null +++ b/src/audio/eqShareIntent.ts @@ -0,0 +1,29 @@ +import { EQ_PRESET_QR_PREFIX } from './eqShare'; + +// The QR marker without the `astra:` scheme, e.g. 'eq-preset:v1:'. The scheme is +// stripped/normalized inconsistently by the OS before it reaches redirectSystemPath, +// so we match on the scheme-less marker instead of the full prefix. +const EQ_PRESET_MARKER = EQ_PRESET_QR_PREFIX.replace(/^astra:/, ''); + +/** + * Returns a router path for an EQ-preset-share deep link (`astra:eq-preset:v1:` + * scanned by the native camera), or null if `path` is not one. + * + * The exact shape handed to redirectSystemPath for an opaque `astra:...` URI varies across + * OS/expo versions (`astra:`, `astra://`, bare, slash-prefixed, and colons possibly + * percent-encoded), so we decode once, locate the marker, and pull the trailing base64url + * run. That alphabet (`[A-Za-z0-9_-]`) is query-safe, so no re-encoding is needed. + */ +export function getEQPresetShareRedirectPath(path: string): string | null { + let candidate = path.trim(); + try { + candidate = decodeURIComponent(candidate); + } catch { + // Malformed percent-encoding — fall back to the raw string. + } + const idx = candidate.indexOf(EQ_PRESET_MARKER); + if (idx === -1) return null; + const payload = candidate.slice(idx + EQ_PRESET_MARKER.length).match(/^[A-Za-z0-9_-]+/)?.[0]; + if (!payload) return null; + return `/eq/import?data=${payload}`; +}