Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a93b5e7
fix(mobile): durable push registration, single-flight token refresh, …
dhairyashiil May 28, 2026
89b0257
fix(mobile): address review feedback on push/auth/cache hardening
dhairyashiil May 28, 2026
1964e96
fix(mobile): guard auth session with a generation/epoch; harden push …
dhairyashiil May 29, 2026
f89b6f8
fix(mobile): close epoch-guard gaps in refresh/boot/logout/push paths
dhairyashiil May 29, 2026
ffdd940
fix(mobile): close residual epoch/partial-write and push-404 ownershi…
dhairyashiil May 29, 2026
a83f6c3
fix(mobile): close rollback/web-login/push-cleanup gaps from round 4
dhairyashiil May 29, 2026
030a4c3
fix(mobile): epoch-guard setupAfterLogin/loginWithOAuth, tighten roll…
dhairyashiil May 29, 2026
932aebf
fix(mobile): close remaining setupAfterLogin/loginWithOAuth/web-sessi…
dhairyashiil May 29, 2026
aa2e54e
fix(mobile): serialize auth/storage marker mutations via auth-transit…
dhairyashiil May 29, 2026
7898757
fix(mobile): recheck auth generation after setupAfterLogin before ins…
dhairyashiil May 29, 2026
6a96a8f
fix(mobile): coalesce concurrent logouts, re-entrancy guard for auth …
dhairyashiil May 29, 2026
9955d4f
fix(mobile): park (not delete) push reg on session change; snapshot l…
dhairyashiil May 30, 2026
8e00ed3
fix(mobile): move pending-deregistration DELETEs outside the queue lo…
dhairyashiil Jun 1, 2026
831d43e
fix(mobile): registeredAt-aware pending removal; recheck auth generat…
dhairyashiil Jun 1, 2026
cbad055
fix(mobile): enqueue replaces same-identity pending record with the n…
dhairyashiil Jun 1, 2026
d674a55
fix(mobile): remove deprecated notification alert option
dhairyashiil Jun 1, 2026
5276f39
chore(mobile): bump app version to 1.0.8
dhairyashiil Jun 1, 2026
38528e3
fix(mobile): create android notification channel before permission
dhairyashiil Jun 1, 2026
c069b5b
fix(mobile): install refresh handler before profile fetch
dhairyashiil Jun 1, 2026
e29609a
chore(mobile): add booking action auth diagnostics
dhairyashiil Jun 1, 2026
2224e69
fix(mobile): preserve auth on booking authorization failure
dhairyashiil Jun 1, 2026
7645c0f
fix(mobile): hide booking confirmation actions for attendees
dhairyashiil Jun 1, 2026
0ea6abe
fix(mobile): show unauthorized booking action messages
dhairyashiil Jun 1, 2026
c064db8
chore(mobile): remove temporary booking and widget debug logs
dhairyashiil Jun 1, 2026
83a1318
fix(mobile): silence app review prompt in dev
dhairyashiil Jun 1, 2026
5bd7e7e
chore(mobile): ignore local firebase config
dhairyashiil Jun 1, 2026
7251b18
chore(mobile): load android firebase config from eas secret
dhairyashiil Jun 1, 2026
4b81619
fix(mobile): harden push cleanup logout flow
dhairyashiil Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 53 additions & 26 deletions apps/mobile/components/PushNotificationProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ import * as Notifications from "expo-notifications";
import { useRouter } from "expo-router";
import { type ReactNode, useEffect, useRef } from "react";
import { useAuth } from "@/contexts/AuthContext";
import { deregisterPushToken, requestAndRegisterPushToken } from "@/hooks/use-push-notifications";
import {
deregisterPersistedPushRegistration,
drainPendingDeregistrations,
requestAndRegisterPushToken,
} from "@/hooks/use-push-notifications";

// How long the pre-logout callback waits for an in-flight registration to
// settle before deregistering, so a token that registered moments before
// logout still gets removed — without blocking the UI logout indefinitely.
const REGISTRATION_SETTLE_TIMEOUT_MS = 3000;

const LAST_HANDLED_NOTIF_KEY = "calcom_last_handled_notification_id";

Expand Down Expand Up @@ -36,9 +45,9 @@ function handleNotificationUrl(url: string, router: ReturnType<typeof useRouter>
}

export function PushNotificationProvider({ children }: PushNotificationProviderProps) {
const { isAuthenticated, registerPreLogoutCallback } = useAuth();
const { isAuthenticated, userInfo, registerPreLogoutCallback } = useAuth();
const router = useRouter();
const registeredTokenRef = useRef<string | null>(null);
const registrationInFlightRef = useRef<Promise<unknown> | null>(null);
const coldStartHandledRef = useRef(false);
const isAuthenticatedRef = useRef(isAuthenticated);
const routerRef = useRef(router);
Expand All @@ -51,38 +60,56 @@ export function PushNotificationProvider({ children }: PushNotificationProviderP
routerRef.current = router;
}, [router]);

// Register token on login.
// Register token on login.
// Register token on login. The registration promise is tracked in a ref so
// the pre-logout callback can wait for an in-flight registration before
// deregistering. requestAndRegisterPushToken persists a durable record on
// success, which is what logout reads to deregister.
const userId = userInfo?.id;
const userIdRef = useRef(userId);
useEffect(() => {
userIdRef.current = userId;
}, [userId]);
useEffect(() => {
if (!isAuthenticated) {
registeredTokenRef.current = null;
if (!isAuthenticated || userId == null) {
return;
}

let cancelled = false;
void (async () => {
const result = await requestAndRegisterPushToken();
if (cancelled) return;
if (result.success) {
registeredTokenRef.current = result.token;
} else if (result.token) {
// Server registration failed but token was obtained — store it so
// we can still deregister on logout.
registeredTokenRef.current = result.token;
const promise = requestAndRegisterPushToken({ userId });
registrationInFlightRef.current = promise;
void promise.finally(() => {
if (registrationInFlightRef.current === promise) {
registrationInFlightRef.current = null;
}
})();
return () => {
cancelled = true;
};
}, [isAuthenticated]);
});
}, [isAuthenticated, userId]);

// Deregister token before auth is cleared — the pre-logout callback runs
// while the Bearer token is still valid so the API call succeeds.
// while the Bearer token is still valid so the API call succeeds. Reading
// the persisted registration (instead of an in-memory ref) means logout can
// still clean up the server subscription after an app restart.
useEffect(() => {
return registerPreLogoutCallback(async () => {
if (registeredTokenRef.current) {
await deregisterPushToken(registeredTokenRef.current);
registeredTokenRef.current = null;
// Snapshot the account being logged out BEFORE the settle wait. A relogin
// landing during the wait could repoint userIdRef at a new account, which
// would make us deregister/drain the wrong identity.
const currentUserId = userIdRef.current;
// Wait briefly for any in-flight registration to settle so a token that
// registered moments before logout is persisted and thus deregistered.
const inFlight = registrationInFlightRef.current;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
if (inFlight) {
await Promise.race([
inFlight.catch(() => undefined),
new Promise((resolve) => setTimeout(resolve, REGISTRATION_SETTLE_TIMEOUT_MS)),
]);
}
await deregisterPersistedPushRegistration(currentUserId ?? undefined);
// A registration that settled in the wait above (after logout advanced
// the generation) parks itself in the pending queue rather than the active
// slot. Drain it now — while the Bearer token is still valid — so its
// server subscription is deleted in this logout instead of lingering
// until the next login for this user.
if (currentUserId != null) {
await drainPendingDeregistrations(currentUserId);
}
});
}, [registerPreLogoutCallback]);
Expand Down
Loading
Loading