Revert "fix: always upsert push token on mobile start"

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