fix: always upsert push token on mobile start
This commit is contained in:
@@ -1,14 +1,11 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -49,9 +46,9 @@ export function configureNotifications(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Acquires (or returns the cached) Expo push token for this device. Returns
|
* Acquires the Expo push token for this device. Returns null on simulators,
|
||||||
* null on simulators, when permission is denied, or when any step fails — the
|
* when permission is denied, or when any step fails — the caller should treat
|
||||||
* caller should treat that as "no push, no further action".
|
* 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;
|
||||||
@@ -78,43 +75,18 @@ 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares the freshly-fetched token to whatever we last sent to Orion and
|
* Posts the current Expo token to Orion. Runs on every launch and on token
|
||||||
* only POSTs on a delta. Never throws — push registration is best-effort and
|
* rotation — the backend upsert is idempotent so re-posting is cheap, and
|
||||||
* must never block the auth path.
|
* 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> {
|
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 ?? "";
|
||||||
|
|
||||||
@@ -123,28 +95,22 @@ 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. Wipes the stored token even if the
|
* Best-effort unregister at sign-out. Fetches the current token (without
|
||||||
* server call fails so the next signed-in user re-registers cleanly.
|
* prompting — getPermissionsAsync is read-only) and deletes it server-side.
|
||||||
*/
|
*/
|
||||||
export async function unregisterPushToken(): Promise<void> {
|
export async function unregisterPushToken(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const stored = await getStoredToken();
|
const token = await acquirePushToken();
|
||||||
if (stored) {
|
if (!token) return;
|
||||||
try {
|
await apiClient.unregisterPushToken(token);
|
||||||
await apiClient.unregisterPushToken(stored);
|
} catch (err) {
|
||||||
} catch (err) {
|
logError(err, { scope: "push.unregister" });
|
||||||
logError(err, { scope: "push.unregister" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await clearStoredToken();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user