Repository navigation
[$250] Native share - Android - No sharing flow/app freezes when share image signed out #101425
Description
If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.
Version Number: 9.4.81-0
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: https://test-management.browserstack.com/projects/2219752/test-runs/74212640/folder/13176721/41236968/1766200870?sort=%2Bstatus_id
Email or phone of affected tester (no customers): [email protected]
Issue reported by: Applause Internal Team
Bug source: Regression TC Execution
Device used: samsung galaxy a34 5g/16
App Component: Other
Action Performed:
Preconditions:
The Expensify app is installed
User is not signed in
Scenario 1:
- Select a photo from device gallery
- Share it to Expensify app
- Verify the login screen for Expensify app displayed
- Login with new user with private domain
Scenario 2:
- Select a photo from device gallery
- Share it to Expensify app
- Verify the login screen for Expensify app displayed
- Login with new user with public domain
Expected Result:
After signing you are redirected to the sharing flow
Actual Result:
Scenario 1: App freezes on sharing page
Scenario 2: After signing in share modal appears and disappears when onboarding modal show up. Also, there is a error messages on onboarding modal
Workaround:
Unknown
Platforms:
- Android: App
- Android: mWeb Chrome
- iOS: App
- iOS: mWeb Safari
- iOS: mWeb Chrome
- Windows: Chrome
- MacOS: Chrome Safari
Screenshots/Videos
Bug7261874_1789592511813.Recording__1641.mp4
Bug7261874_1789634712783.ScreenRecording_09-16-2026_23-52-51_1.mp4
Applause Internal Information
Exported by: Svetlana Lazutkina
Bug ID: 7261874
Issue Owner
Current Issue Owner: @jayeshmangwaniUpwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022108292945299052480
- Upwork Job ID: 2108292945299052480
- Last Price Increase: 2026-10-08
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
applause-bot commentedon Sep 17, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/6206f76e5be76d4d949b1f10c2dc5772d019fa70faa669870ebdcf893dde4aba
https://platform.applause.com/services/links/v1/external/2ebe0bc8f96f524359e8209034e7dcf780825d61b858502bc06a4907afea2b45
MelvinBot commentedon Sep 17, 2026
Proposal
What is the root cause of that problem?
The Android share intent is delivered as a plain deep link (
new-expensify://share/root) with no session or onboarding awareness (FileIntentHandler.kt:36-41). When the user is signed out, that route does not exist yet (SHARE_MODAL_NAVIGATORis only registered inAuthScreens), so the deep link is parked and replayed after sign-in byopenReportFromDeepLink. For a brand new user, three separate things then break it:Link.ts:613-617returns early wheneverhasCompletedGuidedSetupFlowwasfalseat sign-in, on the assumption thatnavigateAfterOnboardingowns the destination. ButnavigateAfterOnboardingonly knows about reports and Home — it has no notion of a pending share, so the shared file is abandoned. That is the "no sharing flow" half.NAVIGATORS.SHARE_MODAL_NAVIGATORis listed inMODAL_ROUTES_TO_DISMISS, andRootStackRouter.ts:136-160slices the stack at the first such route wheneverOnboardingGuardissues a redirect. The share flow is deleted, not stacked — "share modal appears and disappears".Link.ts:599-603setsonboarding.purpose.errorBackButton("Please finish the setup questions to start using the app") when the replayed link resolves while an onboarding screen is focused. A public-domain (gmail) user lands onOnboardingWorkEmail, whose Android back handler sets the same error — private-domain users land on a screen that swallows back silently, which is why only Scenario 2 shows errors.Why Scenario 1 (private domain) freezes instead: the onboarding navigator is registered as a transparent,
gestureEnabled: falsemodal (AuthScreens.tsx:352-363) and rendersnullwhile account metadata loads (OnboardingModalNavigator.tsx:109-112). The share page stays visible underneath but every touch goes to the empty focused screen on top — an apparent freeze, not a crash.What changes do you think we should make in order to solve the problem?
ROUTES.SHARE_ROOTfrom the early return atLink.ts:613-617, and instead persist it as a pending destination thatnavigateAfterOnboardingconsumes once guided setup finishes. This keeps the anti-flash behaviour that early return was added for while making the share flow survive onboarding.Link.ts:599-603, don't callsetOnboardingErrorMessagewhen the deep link is a share intent — the user never pressed back, so "Please finish the setup questions" is wrong.ShareRootPagefail loudly instead of hanging. Wrap theJSON.parse(processedFiles)atShareRootPage.tsx:103in a guard and set an error state. The native module invokes the callback with the bare string"No data found"when the prefs are empty (ShareActionHandlerModule.kt:32), which throws inside the callback, soisFileReadynever flips and the page rendersTabNavigatorSkeletonforever with no retry.Share_RoottoEXCLUDE_FROM_LAST_VISITED_PATH(runtimeDefaults.ts:92-109)./share/rootis currently persisted and restored on a later cold start, where there is no share payload at all — restoration is skipped for public-domain accounts (NavigationRoot.tsx:163-165), which is a second way private-domain users specifically reach a permanently skeletal share page.What alternative solutions did you explore? (Optional)
Alternatives considered and evidence
Alternative: defer onboarding while a share intent is pending. There is no such guard today —
startOnboardingFlowdeliberately bypasses the router with a rawnavigationRef.resetRoot(OnboardingFlow.ts:91-108, called fromuseOnboardingFlow.ts:135-139) precisely becauseOnboardingGuardwould block a normal navigate. Letting share pre-empt onboarding would mean a new user could post an attachment before having an account set up, so resuming after onboarding (fix 1) is safer.Alternative: gate the native intent on session state. The native handler could check for an auth token before firing the deep link and show a "sign in first" screen. This duplicates session logic in Kotlin and still needs the JS-side resume, so it doesn't remove any of the work above.
Ruled out: a native ANR / main-thread stall. The reported symptom is a freeze on the Expensify sharing page, i.e. JS already rendered the share screen, so the intent handling and file copy completed.
Timing note. The share modal is visible at all in Scenario 2 because
OnboardingGuardallows navigation whileONYXKEYS.IS_LOADING_APPis true; onceOpenAppreturns, the reset/redirect above tears it down. Exactly which of the two teardown paths wins is a race, so the ordering (but not the mechanism) is unconfirmed.Not verified: the report is build 9.4.81-0 on a Samsung device, which is likely HybridApp. All the JS fixes above are in this repo, but if HybridApp ships its own
MainActivity/manifest fromMobile-Expensify, the native half of the intent path should be re-checked there. I could not inspect that repo.Possibly related issues
Link.ts:613-617dropping a deep link for new users.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/101425)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
melvin-bot commentedon Sep 17, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @jayeshmangwani (
External)melvin-bot commentedon Sep 17, 2026
Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.
melvin-bot commentedon Sep 22, 2026
@jayeshmangwani Huh... This is 4 days overdue. Who can take care of this?
melvin-bot commentedon Sep 24, 2026
@jayeshmangwani Still overdue 6 days?! Let's take care of this!
jayeshmangwani commentedon Sep 25, 2026
Melvin, let’s raise a draft PR so I can test the changes on my side and see how they work.
jayeshmangwani commentedon Sep 25, 2026
@MelvinBot implement this
42 remaining items
abbasifaizan70 commentedon Oct 14, 2026
Proposal updated: the earlier guard-only version did not survive tracing the full flow (the onboarding reset is allowed on top of the share and the exit then buries it), so the root cause now covers both paths the share takes and the fix sequences the share with onboarding instead of racing it. Implementation and tests are ready; video attached above. @jayeshmangwani
open5588 commentedon Oct 15, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
When a signed-out user shares an image from the Android device gallery to Expensify and then signs in with a new account (either on a private domain or a public domain), the user is not kept on the sharing flow: on a private domain the app freezes on the sharing page, and on a public domain the share modal briefly appears and disappears when the onboarding modal opens/closes.
What is the root cause of that problem?
When an image is shared while signed out, the native share intent stores the shared payload and opens
ShareModalNavigatoronce authenticated, whileOnboardingGuardsimultaneously redirects the new user intoOnboardingModalNavigator. The share modal is then torn down or buried in three places:RootStackRouterdropsSHARE_MODAL_NAVIGATORduring the modal guard redirect:src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts, whenisModalGuardRedirectis true,firstDismissibleModalIndexslicesstate.routesat the first route inMODAL_ROUTES_TO_DISMISS(which includesNAVIGATORS.SHARE_MODAL_NAVIGATOR) and resets the stack to[underlyingFullScreen, redirectModalWithKey]with hardcodedindex: 1. This stripsShareModalNavigatorfrom the root stack when the onboarding modal opens.dismissOnboardingModalBeforeExitblindly callsNavigation.dismissModalon native:src/libs/Navigation/helpers/OnboardingNavigationUtils/index.native.ts,dismissOnboardingModalBeforeExitunconditionally dispatchesNavigation.dismissModal({afterTransition}). If the onboarding modal has already been replaced orShareModalNavigatoris topmost when the transition callback runs,DISMISS_MODALdismissesShareModalNavigatorinstead ofOnboardingModalNavigator(and on private-domain accounts where onboarding auto-completes, leaves a half-dismissed modal transition that freezes interaction).navigateAfterOnboardingpushesROUTES.HOME(or the post-onboarding report) on top ofShareModalNavigator:src/libs/navigateAfterOnboarding.ts,navigateAfterOnboardingchecks!isReportTopmostSplitNavigator()before callingNavigation.navigate(ROUTES.HOME, navigationOptions)(L100–L102 and L127–L130). BecauseShareModalNavigatoris a modal navigator rather thanReportsSplitNavigator,isReportTopmostSplitNavigator()returnsfalse, soNavigation.navigate(ROUTES.HOME)pushes a new fullscreenTabNavigatoron top ofShareModalNavigator, burying the share flow.What changes do you think we should make in order to solve the problem?
Preserve
SHARE_MODAL_NAVIGATORbeneath the redirect modal inRootStackRouter.ts:src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts, check ifstate.routescontains aNAVIGATORS.SHARE_MODAL_NAVIGATORroute (const shareModal = state.routes.find((r) => r.name === NAVIGATORS.SHARE_MODAL_NAVIGATOR)). When present, constructroutesas[underlyingFullScreen, shareModal, redirectModalWithKey]and setindex: routes.length - 1.Only dismiss the modal in
OnboardingNavigationUtils/index.native.tswhenOnboardingModalNavigatoris actually topmost:src/libs/Navigation/helpers/OnboardingNavigationUtils/index.native.ts, check whether the topmost route onnavigationRef.getRootState()isNAVIGATORS.ONBOARDING_MODAL_NAVIGATORbefore callingNavigation.dismissModal({afterTransition}); if it is not topmost, invokeafterTransition()directly soShareModalNavigatoris not dismissed.Guard
navigateAfterOnboarding.tswhenShareModalNavigatoris active:src/libs/navigateAfterOnboarding.ts, add a helperisShareModalInRootStack()(checking whetherNAVIGATORS.SHARE_MODAL_NAVIGATORis in the root stack) alongsideisReportTopmostSplitNavigator()so completing onboarding reveals the preservedShareModalNavigatoraboveTabNavigatorinstead of pushingROUTES.HOMEon top of it.What alternative solutions did you explore?
NAVIGATORS.SHARE_MODAL_NAVIGATORfromMODAL_ROUTES_TO_DISMISSglobally: Explored and rejected becauseMODAL_ROUTES_TO_DISMISSis also used by normal full-screen resets where stale modals should be cleared. Explicitly preservingshareModalonly in theisModalGuardRedirectbranch ofRootStackRouter.tskeeps normal modal cleanup intact.melvin-bot commentedon Oct 15, 2026
📣 @open5588! 📣
Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
Please follow these steps:
Format:
RajGahoi commentedon Oct 18, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
When a signed-out user shares an image/file from the native Android system share sheet into Expensify, and then signs into an account (e.g. creating a new account or logging in where onboarding is triggered), the application either freezes on the share screen or drops the shared attachment and closes the share modal prematurely when the onboarding modal opens.
What is the root cause of that problem?
s dismissOnboardingModalBeforeExit = (afterTransition) => { Navigation.dismissModal({afterTransition}); }dismissModal is called indiscriminately. If ShareModalNavigator is on top or present when this runs, it dismisses the share modal instead of cleanly finishing onboarding.
Route Overlay in
avigateAfterOnboarding.ts:
avigateAfterOnboarding calls Navigation.navigate(ROUTES.HOME). Because ShareModalNavigator is not recognized by isReportTopmostSplitNavigator(), it pushes ROUTES.HOME on top of the modal stack instead of revealing the underlying share dialog.
What changes do you think we should make in order to solve the problem?
outes = [underlyingFullScreen, shareModalRoute, redirectModalWithKey] and set index = routes.length - 1.
What alternative solutions did you explore? (Optional)
Contributor details
Your Expensify account email: [email protected]
Upwork Profile Link: https://www.upwork.com/freelancers/~01b77415f16d156068
melvin-bot commentedon Oct 18, 2026
Details
RajGahoi commentedon Oct 18, 2026
melvin-bot commentedon Oct 18, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
github-actions commentedon Oct 18, 2026
neerajbachani commentedon Oct 19, 2026
@jayeshmangwani I ask you to review my proposal: #101425 (comment) , when you get a chance.
The app stores a signed-out share as a deep link (a URL that opens a screen). After signup, the first onboarding value is always hasCompletedGuidedSetupFlow: false. Onboarding is the setup flow for a new account. When onboarding ends, Link.ts drops that link because navigateAfterOnboarding chooses the next screen. The share does not open. The same drop happens when the app was already open and when the app was closed.
The freeze happens only on a cold start (the app was closed). The share URL is the launch URL, so the share screen opens before onboarding. The Submit tab shows "Import your contacts". Onboarding covers that screen, and the prompt stays open. When onboarding ends, dismissModal removes the top modal. That modal is the share screen. The contacts prompt stays in the app modal list and blocks touches. If the app was already open, the share screen does not open, and the app does not freeze.
The fix keeps the share and opens it after onboarding. The app remembers the signed-out share. It opens the normal landing screen. It then opens the share on top of that screen. On a cold start, the app removes the early share screen before it adds onboarding, so dismissModal cannot remove the share. The app also closes the contacts prompt when the screen that opened it goes away.
Attaching the videos for scenarios 1 and 2 here as well for convenience.
Videos
Screen.Recording.2026-10-11.at.4.00.00.AM.online-video-cutter.com.mp4
Screen.Recording.2026-10-11.at.3.58.13.AM.online-video-cutter.com.mp4
github-actions commentedon Oct 19, 2026
melvin-bot commentedon Oct 19, 2026
@jayeshmangwani Eep! 4 days overdue now. Issues have feelings too...
melvin-bot commentedon Oct 21, 2026
@jayeshmangwani Still overdue 6 days?! Let's take care of this!
jayeshmangwani commentedon Oct 22, 2026
Melvin, I’m still going through all the proposals.
bconnnnn commentedon Oct 23, 2026
Proposal
What is the root cause of that problem?
The signed-out Android image share intent appears to be lost or raced by login/onboarding navigation, leading to a frozen screen or dismissed share modal. This needs lifecycle tracing.
What changes do you think we should make in order to solve the problem?
Persist the pending share URI and permissions across authentication and onboarding; resume only once the app is ready and clear it after completion or cancellation. Avoid competing modal transitions.
What alternative solutions did you explore? (Optional)
Avoid a UI-only workaround that masks the symptom without fixing the state or lifecycle problem.
Validation: Test cold-start sharing into existing and newly onboarded accounts, including cancellation.
@jayeshmangwani I'm available if you need a contributor for this issue. I'll wait for approval before opening a PR.
SARAMALI15792 commentedon Oct 24, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
A signed-out Android user shares a photo to Expensify, signs up as a new account, and never reaches "Share to Expensify": the share page freezes (Scenario 1) or flashes and is replaced by onboarding (Scenario 2). The expected result is the one asked for on #102319: onboarding first, then the share flow with the photo, without changing where onboarding lands today.
Main already parks a pre-onboarding deep link and replays it after onboarding (#103681). This proposal is about why that does not save a share, and how to make it do so without re-creating the #102954 / #104391 regressions that got the previous attempts reverted.
What is the root cause of that problem?
Replay root cause (
Link.ts): the latch only works for one of two Onyx orderingsThe replay latches the raw onboarding flag once, but fires the handler on the selector:
App/src/libs/actions/Link.ts
Lines 546 to 549 in 0eff104
App/src/libs/actions/Link.ts
Lines 699 to 701 in 0eff104
The selector answers
truefor an empty or flag-less NVP, which is exactly what a fresh sign-up has before OpenApp lands:App/src/selectors/Onboarding.ts
Lines 18 to 29 in 0eff104
undefined, handler runs at once,deferUntilAfterOnboarding()returnsfalsebecauseinitialHasCompletedGuidedSetupFlow !== false(Link.ts:635),openDeepLink()pushesshare/rootbefore onboarding, thenOnyx.disconnectmeans nothing is ever parked. Onboarding then covers the share and it is gone.{hasCompletedGuidedSetupFlow:false}): latch isfalse, link is parked and replayed. This is the only ordering Reapply opening a deep link captured before onboarding, without the Concierge regressions #103681 was exercised for.Navigation root cause (cold start): a zombie share under onboarding
The launch URL makes React Navigation mount
ShareModalNavigatoras soon asAuthScreensregisters it (linkingConfigonly filters OAuth callbacks).startOnboardingFlowthen keeps every existing root route and appends onboarding:App/src/libs/actions/Welcome/OnboardingFlow.ts
Lines 59 to 66 in 0eff104
The share's Submit tab opens the global "Import your contacts" prompt from a mount-only effect, and its cleanup only flips a ref:
App/src/hooks/useContactPermissionModal/index.native.ts
Lines 94 to 105 in 0eff104
So the share is either sliced by the onboarding redirect (
MODAL_ROUTES_TO_DISMISScontainsSHARE_MODAL_NAVIGATOR) or left under onboarding with a prompt that outlives it. This is the freeze/flash.Why "just replay it" is not enough: the landing must not be replaced
Both exits consume the parked link first and return, so the landing never runs:
App/src/libs/navigateAfterOnboarding.ts
Lines 126 to 131 in 0eff104
App/src/libs/navigateAfterOnboarding.ts
Lines 210 to 215 in 0eff104
"Replace the landing" is the shape behind blocker #102954 (Submit users lost Spend > Expenses + the #admins side panel; reverted in #102979) and #104391 (
#adminsmarked read after the re-land). A share is an overlay, so it must open after the landing, not instead of it.What changes do you think we should make in order to solve the problem?
Everything below is gated on
ROUTES.SHARE_ROOT; every other deep link keeps today's exact path.The headline change is steps 1-2: fix the replay's own latch race at its source and reuse the slot #103681 already ships, instead of adding a second parking mechanism.
Link.ts: make the latch deterministic. For a signed-out, non-anonymous share, wait once forHAS_LOADED_APP(set when OpenApp's request queue flushes,queueFlushedData; not inKEYS_TO_PRESERVE, so it isfalseright after sign-in) before subscribing toNVP_ONBOARDING. After OpenApp a flag-less NVP really means "old account", so the selector is trustworthy and Ordering A disappears. If OpenApp never completes (offline/failure), the one-shot connection is released by the existing session cleanup and the share falls back to today's path.Link.ts: park the share as soon as the latch readsfalse(not when onboarding finishes), as an overlay.deferUntilAfterOnboardinggets a share guard so the later=== truepass neither re-parks over the overlay nor callsopenDeepLink():navigateAfterOnboarding.ts: add a placement to the existing slot instead of a new state module. Default'instead'is byte-for-byte today's behaviour (/concierge, report links).'overlay'is not consumed at the top; a thin wrapper runs the landing body unchanged and then opens the share:A wrapper rather than per-branch edits, because both exits have early returns (HOME_PAGE_NO_RHP, TRACK_EXPENSES_WITH_CONCIERGE, RHP variant, no-
policyIDSubmit) that a hook inside would miss. The slot is cleared on consume and by the existing session-cleanup callback.OnboardingFlow.ts: don't carry a cold-start share into the onboarding reset, only when step 2 parked one.indexis recomputed from the filtered routes. No share under onboarding means no sliced route, no strayDISMISS_MODAL, and no leaked contacts prompt. If the park has not happened yet the code behaves exactly as today (fail-safe).Regression check against the reverted attempts:
RHP_ADMINS_ROOMvariant and side panel run first; the share is pushed after.#adminsread state (Chat - #admins room is marked as read after completing onboarding via /concierge link #104391 / [$175] LHN - #admin room already read after signed up as "submit expense to my employer" intention #101182): the Concierge/#admins landing path is untouched for every non-share link.false, the gate is skipped for anonymous,Link.ts:688early return is untouched; no slot is ever set.share/rootis native-only; the web path never reaches this branch.Tests:
LinkOpenReportFromDeepLinkTest(flag-less-then-falseNVP: parked, no early navigate;HAS_LOADED_APPfalse: nothing runs; non-share link unchanged),navigateAfterOnboardingTest(landing navigate, thenshare/root, for the Concierge, RHP, and Submit exits; consumed once; default placement unchanged), and anOnboardingFlowtest that a parked share is dropped from the reset payload.Remaining gap, stated plainly: I traced this statically against
0eff104; I did not run it on an Android device. Two things I could not prove from source: (1) whether the step 2 park always runs beforeuseOnboardingFlowRoutercallsstartOnboardingFlow(both react toHAS_LOADED_APP; the park is an Onyx callback,startOnboardingFlowruns from a React effect). IfstartOnboardingFlowwins, step 4 finds nothing parked and keeps the share in the reset, which is today's behaviour, so it degrades instead of breaking. (2) ThatdismissModalalways firesafterTransitionand thatOnboardingGuardalready sees the completed NVP when the overlay navigates. A signed-in user who quit mid-onboarding and then shares is out of scope here. HybridApp's ownMainActivityis unchecked.What alternative solutions did you explore? (Optional)
useOnboardingFlowRouter/ both exits: works, but duplicates the slot Reapply opening a deep link captured before onboarding, without the Concierge regressions #103681 just shipped and leaves the Ordering A race for every other deep link.RootStackRouter+ targeteddismissModal+ skip Home: rejected, touches shared router behaviour for every modal redirect (Web - Onboarding - Two Expensify logos appear on the modal #86258) when the share can simply not be in the stack.