Skip to content

[$250] Native share - Android - No sharing flow/app freezes when share image signed out #101425

Open

Description

@applause-bot

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:

  1. Select a photo from device gallery
  2. Share it to Expensify app
  3. Verify the login screen for Expensify app displayed
  4. Login with new user with private domain

Scenario 2:

  1. Select a photo from device gallery
  2. Share it to Expensify app
  3. Verify the login screen for Expensify app displayed
  4. 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

View all open jobs on GitHub

Applause Internal Information
Exported by: Svetlana Lazutkina
Bug ID: 7261874

Issue OwnerCurrent Issue Owner: @jayeshmangwani
Upwork Automation - Do Not Edit

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 17, 2026
  2. MelvinBot commented on Sep 17, 2026

    @MelvinBot
    Contributor

    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_NAVIGATOR is only registered in AuthScreens), so the deep link is parked and replayed after sign-in by openReportFromDeepLink. For a brand new user, three separate things then break it:

    1. The share deep link is deliberately dropped for new users. Link.ts:613-617 returns early whenever hasCompletedGuidedSetupFlow was false at sign-in, on the assumption that navigateAfterOnboarding owns the destination. But navigateAfterOnboarding only 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.
    2. If the share modal does get mounted, onboarding amputates it. NAVIGATORS.SHARE_MODAL_NAVIGATOR is listed in MODAL_ROUTES_TO_DISMISS, and RootStackRouter.ts:136-160 slices the stack at the first such route whenever OnboardingGuard issues a redirect. The share flow is deleted, not stacked — "share modal appears and disappears".
    3. The error text on the onboarding modal is set by the deep link handler itself. Link.ts:599-603 sets onboarding.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 on OnboardingWorkEmail, 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: false modal (AuthScreens.tsx:352-363) and renders null while 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?

    1. Don't drop a pending share intent for new users. Exempt ROUTES.SHARE_ROOT from the early return at Link.ts:613-617, and instead persist it as a pending destination that navigateAfterOnboarding consumes once guided setup finishes. This keeps the anti-flash behaviour that early return was added for while making the share flow survive onboarding.
    2. Suppress the misleading onboarding error for this path. At Link.ts:599-603, don't call setOnboardingErrorMessage when the deep link is a share intent — the user never pressed back, so "Please finish the setup questions" is wrong.
    3. Make ShareRootPage fail loudly instead of hanging. Wrap the JSON.parse(processedFiles) at ShareRootPage.tsx:103 in 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, so isFileReady never flips and the page renders TabNavigatorSkeleton forever with no retry.
    4. Add Share_Root to EXCLUDE_FROM_LAST_VISITED_PATH (runtimeDefaults.ts:92-109). /share/root is 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 — startOnboardingFlow deliberately bypasses the router with a raw navigationRef.resetRoot (OnboardingFlow.ts:91-108, called from useOnboardingFlow.ts:135-139) precisely because OnboardingGuard would 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 OnboardingGuard allows navigation while ONYXKEYS.IS_LOADING_APP is true; once OpenApp returns, 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 from Mobile-Expensify, the native half of the intent path should be re-checked there. I could not inspect that repo.

    Possibly related issues


    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

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 17, 2026
  4. melvin-bot commented on Sep 17, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @jayeshmangwani (External)

  5. melvin-bot commented on Sep 17, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  6. melvin-bot commented on Sep 22, 2026

    @melvin-bot

    @jayeshmangwani Huh... This is 4 days overdue. Who can take care of this?

  7. melvin-bot commented on Sep 24, 2026

    @melvin-bot

    @jayeshmangwani Still overdue 6 days?! Let's take care of this!

  8. jayeshmangwani commented on Sep 25, 2026

    @jayeshmangwani
    Contributor

    Melvin, let’s raise a draft PR so I can test the changes on my side and see how they work.

  9. jayeshmangwani commented on Sep 25, 2026

    @jayeshmangwani
    Contributor

    @MelvinBot implement this

  10. 42 remaining items

  11. abbasifaizan70 commented on Oct 14, 2026

    @abbasifaizan70
    Contributor

    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

  12. open5588 commented on Oct 15, 2026

    @open5588

    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 ShareModalNavigator once authenticated, while OnboardingGuard simultaneously redirects the new user into OnboardingModalNavigator. The share modal is then torn down or buried in three places:

    1. RootStackRouter drops SHARE_MODAL_NAVIGATOR during the modal guard redirect:

      • In src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts, when isModalGuardRedirect is true, firstDismissibleModalIndex slices state.routes at the first route in MODAL_ROUTES_TO_DISMISS (which includes NAVIGATORS.SHARE_MODAL_NAVIGATOR) and resets the stack to [underlyingFullScreen, redirectModalWithKey] with hardcoded index: 1. This strips ShareModalNavigator from the root stack when the onboarding modal opens.
    2. dismissOnboardingModalBeforeExit blindly calls Navigation.dismissModal on native:

      • In src/libs/Navigation/helpers/OnboardingNavigationUtils/index.native.ts, dismissOnboardingModalBeforeExit unconditionally dispatches Navigation.dismissModal({afterTransition}). If the onboarding modal has already been replaced or ShareModalNavigator is topmost when the transition callback runs, DISMISS_MODAL dismisses ShareModalNavigator instead of OnboardingModalNavigator (and on private-domain accounts where onboarding auto-completes, leaves a half-dismissed modal transition that freezes interaction).
    3. navigateAfterOnboarding pushes ROUTES.HOME (or the post-onboarding report) on top of ShareModalNavigator:

      • In src/libs/navigateAfterOnboarding.ts, navigateAfterOnboarding checks !isReportTopmostSplitNavigator() before calling Navigation.navigate(ROUTES.HOME, navigationOptions) (L100–L102 and L127–L130). Because ShareModalNavigator is a modal navigator rather than ReportsSplitNavigator, isReportTopmostSplitNavigator() returns false, so Navigation.navigate(ROUTES.HOME) pushes a new fullscreen TabNavigator on top of ShareModalNavigator, burying the share flow.

    What changes do you think we should make in order to solve the problem?

    1. Preserve SHARE_MODAL_NAVIGATOR beneath the redirect modal in RootStackRouter.ts:

    2. Only dismiss the modal in OnboardingNavigationUtils/index.native.ts when OnboardingModalNavigator is actually topmost:

    3. Guard navigateAfterOnboarding.ts when ShareModalNavigator is active:

      • In src/libs/navigateAfterOnboarding.ts, add a helper isShareModalInRootStack() (checking whether NAVIGATORS.SHARE_MODAL_NAVIGATOR is in the root stack) alongside isReportTopmostSplitNavigator() so completing onboarding reveals the preserved ShareModalNavigator above TabNavigator instead of pushing ROUTES.HOME on top of it.

    What alternative solutions did you explore?

    • Removing NAVIGATORS.SHARE_MODAL_NAVIGATOR from MODAL_ROUTES_TO_DISMISS globally: Explored and rejected because MODAL_ROUTES_TO_DISMISS is also used by normal full-screen resets where stale modals should be cleared. Explicitly preserving shareModal only in the isModalGuardRedirect branch of RootStackRouter.ts keeps normal modal cleanup intact.
  13. melvin-bot commented on Oct 15, 2026

    @melvin-bot

    📣 @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:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  14. RajGahoi commented on Oct 18, 2026

    @RajGahoi

    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?

    1. Modal Guard Dismissal Collision in RootStackRouter.ts:
      • In src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts, when isModalGuardRedirect triggers (redirecting a newly signed-in user into OnboardingModalNavigator), the router searches for the first route in MODAL_ROUTES_TO_DISMISS.
      • NAVIGATORS.SHARE_MODAL_NAVIGATOR is included in MODAL_ROUTES_TO_DISMISS. As a result, the router slices the route stack at irstDismissibleModalIndex and reconstructs the stack as [underlyingFullScreen, redirectModalWithKey], completely wiping SHARE_MODAL_NAVIGATOR from the root state.
    2. Unconditional Modal Dismissal in OnboardingNavigationUtils/index.native.ts:
      • In src/libs/Navigation/helpers/OnboardingNavigationUtils/index.native.ts:
        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.
    3. 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?

    1. In src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts:
      • In the isModalGuardRedirect branch, check if state.routes contains NAVIGATORS.SHARE_MODAL_NAVIGATOR.
      • If present, preserve it underneath the redirect modal:
        outes = [underlyingFullScreen, shareModalRoute, redirectModalWithKey] and set index = routes.length - 1.
    2. In src/libs/Navigation/helpers/OnboardingNavigationUtils/index.native.ts:
      • Only call Navigation.dismissModal if the topmost route is actually NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR. If another modal is active, call �fterTransition() directly without dismissing the active share view.
    3. In src/libs/navigateAfterOnboarding.ts:
      • Check if NAVIGATORS.SHARE_MODAL_NAVIGATOR is present in the root navigation stack. If so, avoid forcing a navigation push to ROUTES.HOME, allowing the user to seamlessly resume the share flow after completing or dismissing onboarding.

    What alternative solutions did you explore? (Optional)

    • Removing SHARE_MODAL_NAVIGATOR from MODAL_ROUTES_TO_DISMISS globally was explored, but preserving it conditionally inside isModalGuardRedirect is safer and prevents regressions during standard logout/reset navigation flows.

    Contributor details

    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~01b77415f16d156068

  15. melvin-bot commented on Oct 18, 2026

    @melvin-bot

    ⚠️ Unable to store contributor details

    Details
    Auth SetContributorDetails returned an error
    401 Contributor details already exist
    
  16. RajGahoi commented on Oct 18, 2026

    @RajGahoi
  17. melvin-bot commented on Oct 18, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  18. github-actions commented on Oct 18, 2026

    @github-actions
    Contributor

    ⚠️ @RajGahoi Thanks for your interest. To be considered for this job, please post a proposal following the proposal template (note the mandatory sections). Comments claiming the job without one are not reviewed.

  19. neerajbachani commented on Oct 19, 2026

    @neerajbachani
    Contributor

    @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

    Scenario 1 fix Scenario 2 fix
    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
  20. github-actions commented on Oct 19, 2026

    @github-actions
    Contributor

    ⚠️ @neerajbachani Thanks for your proposal. Please update it to follow the proposal template, as proposals are only reviewed if they follow that format (note the mandatory sections).

  21. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    @jayeshmangwani Eep! 4 days overdue now. Issues have feelings too...

  22. melvin-bot commented on Oct 21, 2026

    @melvin-bot

    @jayeshmangwani Still overdue 6 days?! Let's take care of this!

  23. jayeshmangwani commented on Oct 22, 2026

    @jayeshmangwani
    Contributor

    Melvin, I’m still going through all the proposals.

  24. bconnnnn commented on Oct 23, 2026

    @bconnnnn

    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.

  25. SARAMALI15792 commented on Oct 24, 2026

    @SARAMALI15792

    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 orderings

    The replay latches the raw onboarding flag once, but fires the handler on the selector:

    // Capture once. Use the raw flag, not the selector, which returns `true` for the empty NVP a fresh sign-up briefly has.
    if (!isAuthenticated && initialHasCompletedGuidedSetupFlow === undefined && val && !isAnonymousUser()) {
    initialHasCompletedGuidedSetupFlow = val.hasCompletedGuidedSetupFlow;
    }

    if (hasCompletedGuidedSetupFlowSelector(val) || isAnonymousUser()) {
    handleDeeplinkNavigation();
    }

    The selector answers true for an empty or flag-less NVP, which is exactly what a fresh sign-up has before OpenApp lands:

    function hasCompletedGuidedSetupFlowSelector(onboarding: OnyxValue<typeof ONYXKEYS.NVP_ONBOARDING>): boolean {
    // Onboarding is an empty object for old accounts and accounts migrated from OldDot
    if (isEmptyObject(onboarding)) {
    return true;
    }
    if (!isEmptyObject(onboarding) && onboarding?.hasCompletedGuidedSetupFlow === undefined) {
    return true;
    }
    return onboarding?.hasCompletedGuidedSetupFlow;
    }

    • Ordering A (first NVP is flag-less): latch stays undefined, handler runs at once, deferUntilAfterOnboarding() returns false because initialHasCompletedGuidedSetupFlow !== false (Link.ts:635), openDeepLink() pushes share/root before onboarding, then Onyx.disconnect means nothing is ever parked. Onboarding then covers the share and it is gone.
    • Ordering B (first NVP is {hasCompletedGuidedSetupFlow:false}): latch is false, 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 ShareModalNavigator as soon as AuthScreens registers it (linkingConfig only filters OAuth callbacks). startOnboardingFlow then keeps every existing root route and appends onboarding:

    const rootStateRouteNamesSet = new Set(rootState.routes.map((route) => route.name));
    navigationRef.resetRoot({
    ...rootState,
    ...adaptedState,
    stale: true,
    routes: [...rootState.routes, ...(adaptedState?.routes.filter((route) => !rootStateRouteNamesSet.has(route.name)) ?? [])],
    } as PartialState<NavigationState>);

    The share's Submit tab opens the global "Import your contacts" prompt from a mount-only effect, and its cleanup only flips a ref:

    useEffect(() => {
    if (hasDeniedContactImportPromptMetadata.status === 'loading') {
    return;
    }
    isMountedRef.current = true;
    runContactPermissionFlow();
    return () => {
    isMountedRef.current = false;
    };
    }, [hasDeniedContactImportPrompt, hasDeniedContactImportPromptMetadata.status]);

    So the share is either sliced by the onboarding redirect (MODAL_ROUTES_TO_DISMISS contains SHARE_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:

    setDisableDismissOnEscape(false);
    // A route the user asked for before signing up outranks every default below, including the Side Panel variant.
    if (consumeDeepLinkToOpenAfterOnboarding()) {
    return;
    }

    function navigateToSubmitWorkspaceAfterOnboarding(policyID?: string, shouldUseNarrowLayout = false) {
    setDisableDismissOnEscape(false);
    if (consumeDeepLinkToOpenAfterOnboarding()) {
    return;
    }

    "Replace the landing" is the shape behind blocker #102954 (Submit users lost Spend > Expenses + the #admins side panel; reverted in #102979) and #104391 (#admins marked 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.

    1. Link.ts: make the latch deterministic. For a signed-out, non-anonymous share, wait once for HAS_LOADED_APP (set when OpenApp's request queue flushes, queueFlushedData; not in KEYS_TO_PRESERVE, so it is false right after sign-in) before subscribing to NVP_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.
    if (!isAuthenticated && !isAnonymousUser() && isShareRoute) {
        await waitForHasLoadedApp(); // one-shot Onyx.connectWithoutView on HAS_LOADED_APP === true, then disconnect
    }
    1. Link.ts: park the share as soon as the latch reads false (not when onboarding finishes), as an overlay. deferUntilAfterOnboarding gets a share guard so the later === true pass neither re-parks over the overlay nor calls openDeepLink():
    if (isShareRoute && initialHasCompletedGuidedSetupFlow === false) {
        setDeepLinkToOpenAfterOnboarding(openDeepLink, 'overlay');
    }
    // in deferUntilAfterOnboarding(): share + latch false -> return true without parking
    1. 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:
    function navigateAfterOnboarding(...args) {
        navigateLanding(...args);          // today's body, untouched
        openOverlayAfterLanding();         // Navigation.navigate(ROUTES.SHARE_ROOT, {waitForTransition: true}); no-op if share already in root stack
    }

    A wrapper rather than per-branch edits, because both exits have early returns (HOME_PAGE_NO_RHP, TRACK_EXPENSES_WITH_CONCIERGE, RHP variant, no-policyID Submit) that a hook inside would miss. The slot is cleared on consume and by the existing session-cleanup callback.

    1. OnboardingFlow.ts: don't carry a cold-start share into the onboarding reset, only when step 2 parked one.
    const keptRoutes = hasOverlayParked() ? rootState.routes.filter((r) => r.name !== NAVIGATORS.SHARE_MODAL_NAVIGATOR) : rootState.routes;

    index is recomputed from the filtered routes. No share under onboarding means no sliced route, no stray DISMISS_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:

    Tests: LinkOpenReportFromDeepLinkTest (flag-less-then-false NVP: parked, no early navigate; HAS_LOADED_APP false: nothing runs; non-share link unchanged), navigateAfterOnboardingTest (landing navigate, then share/root, for the Concierge, RHP, and Submit exits; consumed once; default placement unchanged), and an OnboardingFlow test 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 before useOnboardingFlowRouter calls startOnboardingFlow (both react to HAS_LOADED_APP; the park is an Onyx callback, startOnboardingFlow runs from a React effect). If startOnboardingFlow wins, step 4 finds nothing parked and keeps the share in the reset, which is today's behaviour, so it degrades instead of breaking. (2) That dismissModal always fires afterTransition and that OnboardingGuard already 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 own MainActivity is unchecked.

    What alternative solutions did you explore? (Optional)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributorsReviewingHas a PR in review

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions