diff --git a/shared/constants/platform-specific/push.native.tsx b/shared/constants/platform-specific/push.native.tsx index 6a953c78b767..d06916db43c6 100644 --- a/shared/constants/platform-specific/push.native.tsx +++ b/shared/constants/platform-specific/push.native.tsx @@ -41,7 +41,26 @@ type DataChatExtension = DataCommon & { type: 'chat.extension' convID?: string } -type Data = DataReadMessage | DataNewMessage | DataNewMessageSilent2 | DataFollow | DataChatExtension +type DataDeviceRevoked = DataCommon & { + type: 'device.revoked' + device_id?: string +} +type DataDeviceNew = DataCommon & { + type: 'device.new' + device_id?: string +} +type DataAutoreset = DataCommon & { + type: 'autoreset' +} +type Data = + | DataReadMessage + | DataNewMessage + | DataNewMessageSilent2 + | DataFollow + | DataChatExtension + | DataDeviceRevoked + | DataDeviceNew + | DataAutoreset type PushN = Data & { message?: string @@ -115,6 +134,30 @@ const normalizePush = (_n?: object): T.Push.PushNotification | undefined => { username: data.username, } : undefined + case 'device.revoked': + return forUid + ? { + forUid, + type: 'device.revoked', + userInteraction, + } + : undefined + case 'device.new': + return forUid + ? { + forUid, + type: 'device.new', + userInteraction, + } + : undefined + case 'autoreset': + return forUid + ? { + forUid, + type: 'autoreset', + userInteraction, + } + : undefined case 'chat.extension': return data.convID ? { diff --git a/shared/constants/push.native.tsx b/shared/constants/push.native.tsx index 3feb7ede9afb..d7c41ce1aa86 100644 --- a/shared/constants/push.native.tsx +++ b/shared/constants/push.native.tsx @@ -177,6 +177,19 @@ export const usePushState = Z.createZustand((set, get) => { logger.info('[Push] account has no stored secret, cannot switch') return } + // Guard against re-triggering a switch that is already in progress. + // After a logout, Z.resetAllStores() resets userSwitching to false but + // preserves pendingPushNotification. If applicationDidBecomeActive + // re-emits the same notification before the bootstrap sets currentUid, + // we would call login() a second time — clobbering the first switch's + // navigation. Skip if the pending notification is already queued for + // the same account. + const existingPending = get().pendingPushNotification + const existingForUid = existingPending ? (existingPending as {forUid?: string}).forUid : undefined + if (existingForUid === forUid) { + logger.info('[Push] switch already in progress for this account, skipping duplicate') + return + } // Store the notification and trigger an account switch. We do NOT // process the notification here — execution continues once the uid // changes, picked up by the subscriber in @@ -205,10 +218,21 @@ export const usePushState = Z.createZustand((set, get) => { case 'follow': // We only care if the user clicked while in session if (notification.userInteraction) { - const {username} = notification - storeRegistry.getState('profile').dispatch.showUserProfile(username) + storeRegistry.getState('profile').dispatch.showUserProfile(notification.username) } break + case 'device.revoked': + case 'device.new': + if (notification.userInteraction && storeRegistry.getState('config').loggedIn) { + switchTab(Tabs.settingsTab) + navUpToScreen('devicesRoot') + } + break + case 'autoreset': + // The ResetModal is always mounted and self-shows when autoreset.active + // is true (driven by Gregor/badge state sync). The account switch above + // is sufficient; no explicit navigation needed. + break case 'chat.extension': { const {conversationIDKey} = notification diff --git a/shared/constants/types/push.tsx b/shared/constants/types/push.tsx index 22e5166d6ee1..db26c6a798df 100644 --- a/shared/constants/types/push.tsx +++ b/shared/constants/types/push.tsx @@ -28,6 +28,21 @@ export type PushNotification = userInteraction: boolean username: string } + | { + forUid?: string + type: 'device.revoked' + userInteraction: boolean + } + | { + forUid?: string + type: 'device.new' + userInteraction: boolean + } + | { + forUid?: string + type: 'autoreset' + userInteraction: boolean + } | { conversationIDKey: ChatTypes.ConversationIDKey forUid?: string