[feat](client): scan an instance-relative join payload

The service mints /api/alarm/a/{alertId}#{token} with no host, so the QR renders against the configured instance and keeps working when the instance moves or is reached by another name. The scanner now matches path shape and fragment and never the host, so legacy alarm.dcntrlized.org codes and other instances codes still join; error copy distinguishes a malformed code from a wrong-alert token.
This commit is contained in:
2026-10-19 02:34:20 +01:00
parent fc66b8e088
commit dcfc9a5df8
5 changed files with 163 additions and 202 deletions
+17 -4
View File
@@ -20,6 +20,9 @@ import { ScanLine, X } from 'lucide-react-native';
import { AppText, Button, ErrorBanner, Field, Header, TextInputBox } from '../components/ui';
import { useAlarm } from '../state/AlarmContext';
import { joinErrorMessage } from '../services/alarmApi';
import { buildDeviceLabel } from '../lib/deviceLabel';
import { canDeliverRealAlarm } from '../lib/takeoverReadiness';
import { useAlarmPermissions } from '../state/useAlarmPermissions';
import { parseQrPayload, parseManualToken, qrErrorMessage } from '../lib/qrPayload';
import { openAppSettings } from '../services/alarmNative';
import type { RootStackParamList } from '../navigation';
@@ -64,7 +67,7 @@ type Rt = RouteProp<RootStackParamList, 'Scanner'>;
export default function ScannerScreen() {
const navigation = useNavigation<Nav>();
const { params } = useRoute<Rt>();
const { joinAlert, deviceId } = useAlarm();
const { joinAlert } = useAlarm();
const { hasPermission, requestPermission, canRequestPermission } = useCameraPermission();
const device = useCameraDevice('back');
@@ -117,10 +120,19 @@ export default function ScannerScreen() {
}, [reduceMotion, sweepValue]);
const targetAlertId = params?.alertId;
const { perms } = useAlarmPermissions();
const complete = useCallback(
async (alertId: string, token: string) => {
if (busy || handled.current) return;
// Subscribing on a device that cannot fire a real alarm would present as
// armed while being unable to ring. Block it and name the fix.
if (perms && !canDeliverRealAlarm(perms)) {
setError(
'This device cannot deliver a real alarm yet. Allow exact alarms and the full-screen takeover in Settings, then scan again.',
);
return;
}
handled.current = true;
setBusy(true);
setError(null);
@@ -128,7 +140,8 @@ export default function ScannerScreen() {
const record = await joinAlert({
alertId,
subscribeToken: token,
deviceLabel: `Android ${deviceId?.slice(-4) ?? ''}`.trim(),
// Device *role*, never a slice of the identity token.
deviceLabel: buildDeviceLabel(),
});
setJoinedName(record.name || alertId);
// Give the confirmation a beat, then return to the detail screen.
@@ -142,7 +155,7 @@ export default function ScannerScreen() {
setBusy(false);
}
},
[busy, deviceId, joinAlert, navigation],
[busy, joinAlert, navigation, perms],
);
const onScanned = useCallback(
@@ -287,7 +300,7 @@ export default function ScannerScreen() {
onChangeText={setManual}
autoCapitalize="none"
autoCorrect={false}
placeholder="https://alarm.dcntrlized.org/a/…#token"
placeholder="/api/alarm/a/…#token or https://your-instance/…"
accessibilityLabel="Alert code or link"
/>
</Field>