[feat](client): join without an account; the token the join returns is the identity

There is no anonymous session to start and nothing to mint: the app comes up on
Home with no account, and the first QR scan performs the unauthenticated
joinAlert, which returns a deviceToken the client stores with the alert. That
token — not a user, not a session — is what the subscriber calls carry as
X-Device-Token, and what the socket puts in ?deviceToken=.

- apiClient takes an optional device token per request; when present it is sent
  instead of the access token, because it is the narrower credential
- alarmApi.joinAlert returns it; getAlert/alarmStatus/ackAlarm/
  removeSubscription/qrImage and the socket URL accept it
- the alert record keeps the token beside its subscribe token, so a queued ack
  for an alert that is gone still resolves the right device
- login is now a destination, never a wall: without a session the app is the
  subscriber view, and the login screen's 'continue without an account' button
  is gone because there is nothing to continue from

tsc clean, 129 tests (8 new ones pin the header transport and the
no-access-token join).
This commit is contained in:
2026-10-19 18:41:49 +01:00
parent 020a90f0d3
commit 31cff32824
12 changed files with 326 additions and 64 deletions
+4 -6
View File
@@ -132,11 +132,9 @@ function Root() {
useEffect(() => {
if (token === prevToken.current) return;
prevToken.current = token;
if (token) {
navigation.reset({ index: 0, routes: [{ name: 'Home' }] });
} else {
navigation.reset({ index: 0, routes: [{ name: 'Login' }] });
}
// Home is always reachable: without an account it is the subscriber view
// (rev 10). Login is an explicit destination, never a wall.
navigation.reset({ index: 0, routes: [{ name: 'Home' }] });
}, [token, navigation]);
useNativeLaunchTakeover();
@@ -144,7 +142,7 @@ function Root() {
if (booting) return <LoadingView label="Starting…" />;
return (
<AlarmProvider>
<Navigator initialRoute={token ? 'Home' : 'Login'} />
<Navigator initialRoute="Home" />
</AlarmProvider>
);
}