Reinitialize Pusher during delegate account transitions - #93265
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 683b5e321a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return openApp().then(() => { | ||
| if (!CONFIG.IS_HYBRID_APP || !policyID) { | ||
| return openApp() | ||
| .then(() => initializePusherForDelegateTransition()) |
There was a problem hiding this comment.
Avoid blocking delegate switch on Pusher connection
In delegate transitions launched through DelegatorConnectGate, the returned connect() promise is used to suspend the UI until it resolves, and in HybridApp this chain also gates switchAccount(). Because initializePusherForDelegateTransition() waits on Pusher.init(), which only resolves after a successful Pusher connected event and does not reject on connection/auth failures, users whose Pusher websocket is blocked or unavailable after openApp() succeeds can remain stuck on the loading gate and never complete the account switch. Keep the Pusher reinitialization fire-and-forget or add a timeout/fallback before resolving the delegate transition.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Hi @huult, thank you for taking the time and review this.
I tried reproducing this by using a delegate transition URL with delegatorEmail and blocking the Pusher websocket in Chrome DevTools, but I couldn’t reproduce the stuck loading state locally.
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-06-04.at.10.53.44.movAndroid: mWeb ChromeScreen.Recording.2026-06-04.at.10.32.20.moviOS: HybridAppScreen.Recording.2026-06-04.at.10.47.45.moviOS: mWeb SafariScreen.Recording.2026-06-04.at.10.51.00.movMacOS: Chrome / SafariScreen.Recording.2026-06-04.at.10.11.27.mov |
|
@MelvinBot It looks like the unit test is failing due to an issue unrelated to this PR. Do you have any suggestions on how we should address it? |
|
You're right — this failure is unrelated to your PR and looks like a flaky test, not something your changes introduced. What's failing: Why it's not your PR: Your PR only touches Suggested way to address it:
Evidence
|
|
@iwiznia It's the same flaky test that Melvin mentioned. I think we should create a ticket to track it. Do you have any recommendations on how we should proceed? |
joekaufmanexpensify
left a comment
There was a problem hiding this comment.
Good for product 👍
|
Hmmmm but I retried it several times and it failed in all of them, are you sure it's a flakey test and not something we actually broke? |
2 similar comments
|
Hmmmm but I retried it several times and it failed in all of them, are you sure it's a flakey test and not something we actually broke? |
|
Hmmmm but I retried it several times and it failed in all of them, are you sure it's a flakey test and not something we actually broke? |
|
@nabi-ebrahimi Could you update the code as shown below so we can test it? App/tests/actions/OnyxUpdateManagerTest.ts Lines 139 to 141 in fc67c31 + await Onyx.set(ONYXKEYS.IS_LOADING_APP, false);
await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1);
await Onyx.set(ONYX_KEY, initialData);
+ await waitForBatchedUpdates(); |
|
Hi @huult, thank you for the review. We are still testing this approach locally to confirm the delegate Pusher reinitialization behaves correctly across different network conditions. I will share the updated results once verification is complete. |
|
@nabi-ebrahimi Thanks! Let me know when it’s ready |
Hi @huult, I tested this PR across different devices and network conditions, and it worked as expected. Thanks. |
@nabi-ebrahimi You haven't updated the explanation to reflect the latest changes. Could you please update it? |
|
@nabi-ebrahimi Please update the QA steps. |
|
@nabi-ebrahimi I've left a few minor comments. Could you address them today? I'll approve the PR once those are addressed. |
|
@huult, thank you. I’ve addressed all of your feedback. |
|
@iwiznia all yours |
|
🚧 iwiznia has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
@nabi-ebrahimi @iwiznia How to configure a delegated access? |
|
🚀 Deployed to staging by https://github.com/iwiznia in version: 9.4.26-0 🚀
|
|
🤖 Help site review: no changes required. This PR is a purely internal fix to how the Pusher (real-time updates) connection is reinitialized during Copilot/delegate account transitions. It changes only the connection-management infrastructure in three The public help articles that cover this area document the workflow (assigning and switching to delegated accounts), not the underlying real-time connection behavior:
From a reader's perspective the documented behavior is unchanged — this fix simply ensures live updates keep working after a switch, which is already the implicitly expected behavior. So there's nothing to update, and no draft PR was created. If you feel a specific article's wording should change as a result of this fix, reply with the article and what you'd like updated and I'll open a draft PR. |
|
|
🚀 Deployed to production by https://github.com/grgia in version: 9.4.27-2 🚀
Bundle Size Analysis (Sentry): |

Explanation of Change
This change improves delegate account switching by ensuring Pusher is reinitialized with the correct account context after a delegate connect or disconnect flow. That keeps the app subscribed to the newly active account’s private user channel instead of staying attached to the previous account’s live updates.
During the investigation, it also became clear that the delegate transition and the normal app startup should share the same Pusher bootstrap path.
src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsxnow registers a reusable reinitialize handler, andsrc/libs/actions/Delegate.tscallsrequestPusherReinitialize(...)afteropenApp()with the account details returned by the switch response. That keeps the Pusher setup and user-event subscription logic centralized and avoids relying only onSESSIONtiming.To address this, the PR keeps the change focused on the Pusher initialization flow. The updated logic now:
src/libs/actions/Delegate.tsopenApp()usingrequestPusherReinitialize(...)accountIDandemailexplicitly during delegate connect and disconnectsrc/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsxwhen explicit values are not providedsrc/libs/requestPusherReinitialize.tswhen no handler is registered, so missing setup is easier to diagnoseOverall, these changes make delegate switching more reliable by reconnecting live updates through one shared initialization path and ensuring Pusher resubscribes with the correct account information.
Fixed Issues
$ #90694
PROPOSAL: #90694 (comment)
Tests
Preconditions
Offline tests
QA Steps
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Screenrecorder-2026-05-21-19-11-24-229.mp4
Android: mWeb Chrome
Screenrecorder-2026-05-22-18-29-02-482.mp4
iOS: Native
IOS.NATIVE.mp4
iOS: mWeb Safari
iosweb.mov
MacOS: Chrome / Safari
Screen.Recording.2026-05-20.at.11.43.33.PM.mov