fix(mobile): replay cold-start notification taps after auth bootstrap
This commit is contained in:
parent
7a8e1e3e25
commit
9a5333c617
5 changed files with 396 additions and 22 deletions
|
|
@ -58,6 +58,10 @@ import { MobileAdsProvider } from './src/lib/mobile-ads-context'
|
|||
import { DeviceProvider } from './src/lib/device-context'
|
||||
import { useNotificationRuntime } from './src/features/notifications/use-notification-runtime'
|
||||
import type { NotificationNavigationTarget } from './src/features/notifications/notification-contract'
|
||||
import {
|
||||
notificationNavigationQueue,
|
||||
type NotificationNavigationReadiness,
|
||||
} from './src/features/notifications/notification-navigation-queue'
|
||||
import { loadPendingInviteToken } from './src/features/teams/pending-invite'
|
||||
import { listTeams } from './src/features/teams/team-service'
|
||||
import { listHistoryPage } from './src/features/history/history-service'
|
||||
|
|
@ -200,7 +204,6 @@ function RootNavigator(): React.ReactElement {
|
|||
} = useMobilePreferences()
|
||||
const [authEntryRoute, setAuthEntryRoute] = useState<AuthEntryRoute>('Login')
|
||||
const navigationRef = useNavigationContainerRef<RootStackParamList>()
|
||||
const pendingNotificationTarget = useRef<NotificationNavigationTarget | null>(null)
|
||||
const lastRoutedIncomingMediaId = useRef<string | null>(null)
|
||||
const previousNeedsOnboarding = useRef<boolean | null>(null)
|
||||
const navigationTheme = useMemo(
|
||||
|
|
@ -213,17 +216,37 @@ function RootNavigator(): React.ReactElement {
|
|||
!authLoading && privacyCleanupState === 'ready',
|
||||
)
|
||||
|
||||
const navigateFromNotification = useCallback((target: NotificationNavigationTarget): void => {
|
||||
if (user === null && target.name !== 'InviteAccept') return
|
||||
if (!navigationRef.isReady()) {
|
||||
pendingNotificationTarget.current = target
|
||||
return
|
||||
// Readiness is read through a ref so the notification callbacks keep a
|
||||
// stable identity; the flush effect below re-runs when it changes.
|
||||
const authenticatedAppReady = !authLoading
|
||||
&& !preferencesLoading
|
||||
&& user !== null
|
||||
&& !needsOnboarding
|
||||
&& !recoveryMode
|
||||
const authenticatedAppReadyRef = useRef(authenticatedAppReady)
|
||||
authenticatedAppReadyRef.current = authenticatedAppReady
|
||||
|
||||
const flushNotificationNavigation = useCallback((): void => {
|
||||
const readiness: NotificationNavigationReadiness = {
|
||||
navigationReady: navigationRef.isReady(),
|
||||
authenticatedAppReady: authenticatedAppReadyRef.current,
|
||||
}
|
||||
const target = notificationNavigationQueue.take(readiness)
|
||||
if (target === null) return
|
||||
const navigator = navigationRef as unknown as {
|
||||
navigate: (name: string, params: Record<string, string>) => void
|
||||
}
|
||||
navigator.navigate(target.name, target.params ?? {})
|
||||
}, [navigationRef, user])
|
||||
}, [navigationRef])
|
||||
|
||||
// Opened notifications can arrive before auth bootstrap finished (cold
|
||||
// start: the native payload is one-shot) or while signed out. Queue the
|
||||
// target in the app-lifetime queue and replay it once the authenticated
|
||||
// stack is ready instead of dropping it.
|
||||
const navigateFromNotification = useCallback((target: NotificationNavigationTarget): void => {
|
||||
notificationNavigationQueue.offer(target)
|
||||
flushNotificationNavigation()
|
||||
}, [flushNotificationNavigation])
|
||||
|
||||
const routeIncomingMedia = useCallback(async (): Promise<void> => {
|
||||
if (user === null || needsOnboarding || recoveryMode || !navigationRef.isReady()) return
|
||||
|
|
@ -285,6 +308,12 @@ function RootNavigator(): React.ReactElement {
|
|||
navigationRef.resetRoot({ index: 0, routes: [{ name: 'Main' }] })
|
||||
}, [authLoading, navigationRef, needsOnboarding, preferencesLoading, user])
|
||||
|
||||
// Declared after the onboarding reset so a replayed notification target is
|
||||
// pushed on top of the fresh Main root rather than wiped by resetRoot.
|
||||
useEffect(() => {
|
||||
if (authenticatedAppReady) flushNotificationNavigation()
|
||||
}, [authenticatedAppReady, flushNotificationNavigation])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?.id) return
|
||||
let active = true
|
||||
|
|
@ -319,9 +348,7 @@ function RootNavigator(): React.ReactElement {
|
|||
theme={navigationTheme}
|
||||
linking={linking}
|
||||
onReady={() => {
|
||||
const pending = pendingNotificationTarget.current
|
||||
pendingNotificationTarget.current = null
|
||||
if (pending !== null) navigateFromNotification(pending)
|
||||
flushNotificationNavigation()
|
||||
void routeIncomingMedia()
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue