native android qr scan routing

This commit is contained in:
Boof2015
2026-07-07 16:57:09 -04:00
parent c4212f81e0
commit c072a1df16
3 changed files with 130 additions and 2 deletions
+6 -2
View File
@@ -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<string> {
if (!isNotificationClickPath(path)) return path;
if (isNotificationClickPath(path)) return getNotificationClickRedirectPath();
return getNotificationClickRedirectPath();
const eqShareRedirect = getEQPresetShareRedirectPath(path);
if (eqShareRedirect) return eqShareRedirect;
return path;
}
+95
View File
@@ -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<EQPreset | null>(() => {
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 (
<Screen>
<View style={styles.header}>
<Pressable style={styles.back} onPress={goToEq} hitSlop={8}>
<Ionicons name="chevron-back" size={22} color={colors.textSecondary} />
<Text variant="body" color={colors.textSecondary}>
Equalizer
</Text>
</Pressable>
</View>
<View style={styles.errorCard}>
<Ionicons name="alert-circle-outline" size={28} color={colors.warning} />
<Text variant="body">This link does not contain a valid Astra EQ preset.</Text>
<Pressable style={styles.primaryButton} onPress={goToEq}>
<Text variant="body" color={colors.accentTextStrong}>
Go to Equalizer
</Text>
</Pressable>
</View>
</Screen>
);
}
return (
<Screen>
<EQPresetPreviewSheet
preset={preset}
title="Shared EQ preset"
onConfirm={() => importPreset(preset)}
onClose={goToEq}
/>
</Screen>
);
}
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',
},
}));
+29
View File
@@ -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:<payload>`
* 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}`;
}