Skip to content

OpenApp fires twice on supportal login: no in-flight deduplication for OpenApp #98002

Description

@mountiny

Version Number: 9.4.50-0
Reproducible in staging?: Needs verification
Reproducible in production?: Yes (observed on production)
If this was caught during regression testing, add the test name, ID and link from BrowserStack: N/A
Email or phone of affected tester (no customers): vit@expensify.com
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: @mountiny
Slack conversation (hyperlinked to channel name):

Action Performed:

  1. Sign in to New Expensify on web as an Expensify employee.
  2. Start a supportal session into another account from Expensify Classic.
  3. Watch the network tab (or backend request logs) while New Expensify finishes loading.

Expected Result:

Exactly one OpenApp request is sent for the newly opened session.

Actual Result:

Two OpenApp requests are sent back to back for the same session. Both run the full app-load payload, so the account pays the OpenApp cost twice.

Analysis

OpenApp has no in-flight deduplication, so two openApp() triggers inside the request window both reach the server.

openApp() sends the request through API.writeWithNoDuplicatesConflictAction, which resolves conflicts with resolveDuplicationConflictAction(persistedRequests, matcher):

https://github.com/Expensify/App/blob/main/src/libs/actions/RequestConflictUtils.ts#L60

That resolver only searches persistedRequests. processNextRequest() moves the request that is currently in flight out of persistedRequests and into ongoingRequest:

https://github.com/Expensify/App/blob/main/src/libs/actions/PersistedRequests.ts#L406

So while OpenApp #1 is in flight, the matcher finds nothing, the resolver returns push instead of replace, and OpenApp #2 is enqueued and sent.

ReconnectApp already closes this gap. writeWithNoDuplicatesReconnectConflictAction passes getOngoingRequest() into its resolver, so a ReconnectApp that arrives mid-flight is collapsed:

https://github.com/Expensify/App/blob/main/src/libs/API/index.ts#L71

OpenApp never got the equivalent treatment.

Is this supportal only?

No. The missing in-flight guard is generic — any account that produces two openApp() triggers close together sends two OpenApp requests. Supportal just raises the odds, because the flow signs the agent out, clears Onyx, signs in with the support auth token, and swaps the Public navigator for the Auth navigator. The openApp() trigger in AuthScreensInitHandler runs in a mount-only effect ([] deps), so every remount of AuthScreens fires another one:

https://github.com/Expensify/App/blob/main/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx#L177

The remount during a support login is already documented in signInWithSupportAuthToken, which added a token guard for the same reason:

https://github.com/Expensify/App/blob/main/src/libs/actions/Session/index.ts#L244

Other openApp() callers can stack the same way — PriorityModeHandler, SignInModal, useIsLoadingAppRecovery, and the delegate paths in Delegate.ts.

Prior art

Fix doubled OpenApp on delegate (copilot) account switch was the same class of bug for copilot. It was fixed by removing the two specific triggers, not by adding deduplication, so the underlying gap stayed open.

App hangs when OpenApp / ReconnectApp / GetMissingOnyxMessages fire in rapid stampedes tracks the broader stampede symptom.

Proposed fix

Give OpenApp the same in-flight awareness ReconnectApp has: feed getOngoingRequest() into the conflict resolver so a second OpenApp is dropped or collapsed while one is already running. Fixing it at the dedupe layer covers every trigger at once, instead of chasing each caller.

Workaround:

Yes, users can keep using Expensify. The duplicate request is invisible in the UI beyond a slower app load, and it is more noticeable on large accounts.

Platforms:

Select the officially supported platforms where the issue was reproduced:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @daledah

Metadata

Metadata

Labels

#qualityRelates to work in the #quality roomBugSomething is broken. Auto assigns a BugZero manager.InternalRequires API changes or must be handled by Expensify staffPerformanceReviewingHas a PR in reviewWeeklyKSv2

Type

No type

Projects

Status
CRITICAL

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions