From a6c717eb679cf71424eb7ebaf9991c53b88030dc Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 30 Apr 2026 13:57:47 +0200 Subject: [PATCH] Seed LAST_FULL_RECONNECT_TIME before Onyx.clear on delegate transition --- src/libs/actions/Delegate.ts | 23 +++++++++++++++++++---- src/libs/actions/Session/index.ts | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts index 4cbacabd6034..e19114fe58ba 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -4,6 +4,7 @@ import type {OnyxEntry, OnyxKey, OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import type {AddDelegateParams as APIAddDelegateParams, RemoveDelegateParams as APIRemoveDelegateParams, UpdateDelegateRoleParams as APIUpdateDelegateRoleParams} from '@libs/API/parameters'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; import {clearPreservedSearchNavigatorStates} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState'; @@ -41,6 +42,20 @@ const KEYS_TO_PRESERVE_DELEGATE_ACCESS = [ ONYXKEYS.COLLECTION.DEVICE_BIOMETRICS, ]; +/** + * Reset Onyx for a delegate-access transition while seeding LAST_FULL_RECONNECT_TIME=now. + * + * subscribeToFullReconnect compares this timestamp against the server-supplied + * NVP_RECONNECT_APP_IF_FULL_RECONNECT_BEFORE that lands in OpenApp's response.onyxData. + * Because applyHTTPSOnyxUpdates applies response.onyxData before successData, the + * timestamp would still be empty when the comparison runs, falsely triggering a + * duplicate ReconnectApp on top of the OpenApp that follows the transition. Seeding + * it to `now` short-circuits the subscriber until OpenApp's successData refreshes it. + */ +function clearOnyxForDelegateTransition(): Promise { + return Onyx.merge(ONYXKEYS.LAST_FULL_RECONNECT_TIME, DateUtils.getDBTime()).then(() => Onyx.clear([...KEYS_TO_PRESERVE_DELEGATE_ACCESS, ONYXKEYS.LAST_FULL_RECONNECT_TIME])); +} + type WithDelegatedAccess = { // Optional keeps call sites clean, but still encourages passing `account?.delegatedAccess`. delegatedAccess: DelegatedAccess | undefined; @@ -195,7 +210,7 @@ function connect({email, delegatedAccess, credentials, session, activePolicyID, }) .then(() => { NetworkStore.setAuthToken(response?.restrictedToken ?? null); - return Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS); + return clearOnyxForDelegateTransition(); }) .then(() => { confirmReadyToOpenApp(); @@ -294,7 +309,7 @@ function disconnect({stashedCredentials, stashedSession}: DisconnectParams) { }) .then(() => { NetworkStore.setAuthToken(response?.authToken ?? null); - return Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS); + return clearOnyxForDelegateTransition(); }) .then(() => { Onyx.set(ONYXKEYS.CREDENTIALS, { @@ -663,7 +678,7 @@ function updateDelegateRole({email, role, validateCode, delegatedAccess}: Update } function restoreDelegateSession(authenticateResponse: Response) { - Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS).then(() => { + clearOnyxForDelegateTransition().then(() => { updateSessionAuthTokens(authenticateResponse?.authToken, authenticateResponse?.encryptedAuthToken); updateSessionUser(authenticateResponse?.accountID, authenticateResponse?.email); @@ -690,5 +705,5 @@ export { updateDelegateRole, removeDelegate, openSecuritySettingsPage, - KEYS_TO_PRESERVE_DELEGATE_ACCESS, + clearOnyxForDelegateTransition, }; diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 90da86594b56..a5a44a83e43d 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -51,7 +51,7 @@ import Timers from '@libs/Timers'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {confirmReadyToOpenApp, KEYS_TO_PRESERVE, openApp} from '@userActions/App'; import {clearCachedAttachments} from '@userActions/Attachment'; -import {KEYS_TO_PRESERVE_DELEGATE_ACCESS} from '@userActions/Delegate'; +import {clearOnyxForDelegateTransition} from '@userActions/Delegate'; import * as Device from '@userActions/Device'; import type HybridAppSettings from '@userActions/HybridApp/types'; import {close} from '@userActions/Modal'; @@ -718,7 +718,7 @@ function setupNewDotAfterTransitionFromOldDot(hybridAppSettings: HybridAppSettin } Log.info('[HybridApp] User switched account on OldDot side. Clearing onyx and applying delegate data'); - return Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS) + return clearOnyxForDelegateTransition() .then(() => Onyx.multiSet({ ...stashedData,