diff --git a/js/mobile/src/lib/push-notifications.ts b/js/mobile/src/lib/push-notifications.ts index 5d4dca5..7a60610 100644 --- a/js/mobile/src/lib/push-notifications.ts +++ b/js/mobile/src/lib/push-notifications.ts @@ -1,14 +1,11 @@ import Constants from "expo-constants"; import * as Device from "expo-device"; import * as Notifications from "expo-notifications"; -import * as SecureStore from "expo-secure-store"; import { Platform } from "react-native"; import { apiClient } from "@/api/client"; import { logError } from "@/lib/errors"; import { routeNotificationTap } from "@/lib/notification-routing"; -const STORED_TOKEN_KEY = "expo_push_token"; - let configured = false; let tokenListenerSubscription: Notifications.Subscription | null = null; @@ -49,9 +46,9 @@ export function configureNotifications(): void { } /** - * Acquires (or returns the cached) Expo push token for this device. Returns - * null on simulators, when permission is denied, or when any step fails — the - * caller should treat that as "no push, no further action". + * Acquires the Expo push token for this device. Returns null on simulators, + * when permission is denied, or when any step fails — the caller should treat + * that as "no push, no further action". */ async function acquirePushToken(): Promise { if (!Device.isDevice) return null; @@ -78,43 +75,18 @@ async function acquirePushToken(): Promise { return tokenResult.data; } -async function getStoredToken(): Promise { - try { - return await SecureStore.getItemAsync(STORED_TOKEN_KEY); - } catch { - return null; - } -} - -async function setStoredToken(token: string): Promise { - try { - await SecureStore.setItemAsync(STORED_TOKEN_KEY, token); - } catch (err) { - logError(err, { scope: "push.store" }); - } -} - -async function clearStoredToken(): Promise { - try { - await SecureStore.deleteItemAsync(STORED_TOKEN_KEY); - } catch { - // ignore - } -} - /** - * Compares the freshly-fetched token to whatever we last sent to Orion and - * only POSTs on a delta. Never throws — push registration is best-effort and - * must never block the auth path. + * Posts the current Expo token to Orion. Runs on every launch and on token + * rotation — the backend upsert is idempotent so re-posting is cheap, and + * skipping the local dedupe means an env switch (e.g. dev → prod build on the + * same device) re-registers correctly. Never throws — push registration is + * best-effort and must never block the auth path. */ export async function syncPushToken(token?: string | null): Promise { try { const next = token ?? (await acquirePushToken()); if (!next) return; - const stored = await getStoredToken(); - if (stored === next) return; - const platform = Platform.OS === "ios" ? "ios" : "android"; const appVersion = Constants.expoConfig?.version ?? ""; @@ -123,28 +95,22 @@ export async function syncPushToken(token?: string | null): Promise { platform, app_version: appVersion, }); - await setStoredToken(next); } catch (err) { logError(err, { scope: "push.sync" }); } } /** - * Best-effort unregister at sign-out. Wipes the stored token even if the - * server call fails so the next signed-in user re-registers cleanly. + * Best-effort unregister at sign-out. Fetches the current token (without + * prompting — getPermissionsAsync is read-only) and deletes it server-side. */ export async function unregisterPushToken(): Promise { try { - const stored = await getStoredToken(); - if (stored) { - try { - await apiClient.unregisterPushToken(stored); - } catch (err) { - logError(err, { scope: "push.unregister" }); - } - } - } finally { - await clearStoredToken(); + const token = await acquirePushToken(); + if (!token) return; + await apiClient.unregisterPushToken(token); + } catch (err) { + logError(err, { scope: "push.unregister" }); } }