fix: always upsert push token on mobile start

This commit is contained in:
talksik
2026-05-18 11:59:31 -07:00
parent 90d9acff4d
commit 90ff18a788
+15 -49
View File
@@ -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<string | null> {
if (!Device.isDevice) return null;
@@ -78,43 +75,18 @@ async function acquirePushToken(): Promise<string | null> {
return tokenResult.data;
}
async function getStoredToken(): Promise<string | null> {
try {
return await SecureStore.getItemAsync(STORED_TOKEN_KEY);
} catch {
return null;
}
}
async function setStoredToken(token: string): Promise<void> {
try {
await SecureStore.setItemAsync(STORED_TOKEN_KEY, token);
} catch (err) {
logError(err, { scope: "push.store" });
}
}
async function clearStoredToken(): Promise<void> {
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<void> {
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<void> {
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<void> {
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" });
}
}