Skip to content

[Due for payment 2026-10-27] [$175] Android - Concierge opens before onboarding is completed #102956

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.5.2-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: #101558
Email or phone of affected tester (no customers): [email protected]
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: Samsung Galaxy S25 FE / Android 16
App Component: Other

Action Performed:

  1. Go to staging.new.expensify.com/concierge
  2. Log in with new Gmail account.

Expected Result:

Concierge will open after onboarding is completed.

Actual Result:

Concierge opens before onboarding is completed.

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

Bug7277424_1791169057649.Screen_Recording_20261005_105632_Permission_controller.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Mita Rachim
Bug ID: 7277424

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

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 5, 2026
  2. applause-bot commented on Oct 5, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/a00190b2a2ea6dc4fb7d4d44617caeaf74ddcfe5feda70cfb9f45abee3b40c9d

  3. MelvinBot commented on Oct 5, 2026

    @MelvinBot
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Android and iOS, a new user who opens /concierge and then signs up sees the Concierge chat before onboarding is completed.

    What is the root cause of that problem?

    This bug already exists in production. Open a deep link captured before onboarding instead of dropping it did not cause it.

    1. Native keeps the /concierge screen through sign-in. On a native cold start, react-navigation builds its first state from the launch URL. Native loads its screens synchronously, so that state survives sign-in. After sign-up, the signed-in screens rebuild from it with ConciergePage on top. Web loads its screens lazily, which drops that state, so ConciergePage never mounts on web.
    2. ConciergePage never checks onboarding. It opens the Concierge chat with shouldDismissModal: true as soon as report data has loaded. This usually happens before the onboarding flow opens.
    3. HybridApp skips the onboarding guard. OnboardingGuard lets navigation through when isHybridAppOnboardingCompleted !== true. The Concierge navigation is not blocked.
    Code references

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

    In ConciergePage, skip the navigation while onboarding is still pending:

    1. Read the raw NVP_ONBOARDING.hasCompletedGuidedSetupFlow value. Do not use hasCompletedGuidedSetupFlowSelector, which treats an empty value as completed.
    2. In the focus effect, return early when the value is false, and add it to the effect's dependencies.

    After onboarding, the deep link that openReportFromDeepLink holds opens the Concierge chat through navigateAfterOnboarding. If ConciergePage is still mounted, its effect runs again. The "already on this route" check in Link.ts stops the chat from opening twice.

    What alternative solutions did you explore? (Optional)

    • Fix the root cause in navigation. Add a getInitialURL to linkingConfig that returns null when the user is signed out and the route is not public. This also covers other signed-in-only links opened on a cold start, such as /track-expense. It is riskier. Magic links, /transition, SAML, and connection-complete pages need QA.
    • Narrow the OnboardingGuard HybridApp bypass so it does not apply while NewDot onboarding is pending. This change affects more flows.
    Confidence and verification

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102956) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run · no recording available

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

    @melvin-bot

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

  6. changed the title [-]Android & iOS - Concierge opens before onboarding is completed[/-] [+][$175] Android & iOS - Concierge opens before onboarding is completed[/+] on Oct 5, 2026
  7. melvin-bot commented on Oct 5, 2026

    @melvin-bot
  8. bonnhatnguyen commented on Oct 5, 2026

    @bonnhatnguyen
    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Android and iOS native platforms, when a new user opens staging.new.expensify.com/concierge while logged out and completes sign-up/sign-in, the Concierge chat opens prematurely before the onboarding flow is completed.


    What is the root cause of that problem?

    1. Native retains [CONCIERGE] in the initial route state:
      On a native cold start via the /concierge deep link, React Navigation builds its root navigation state from the launch URL. In AppNavigator.native.tsx, screen modules are required synchronously, preserving [CONCIERGE] (ConciergePage) at the top of the stack when transitioning to AuthScreens after authentication.

    2. ConciergePage lacks an onboarding completion guard:
      In src/pages/ConciergePage.tsx:

      useFocusEffect(
          useCallback(() => {
              if (session && 'authToken' in session) {
                  Navigation.isNavigationReady().then(() => {
                      if (isUnmounted.current || isLoadingReportData === undefined || !!isLoadingReportData) {
                          return;
                      }
      
                      navigateToConciergeChat({
                          conciergeReportID,
                          introSelected,
                          currentAccountID: session.accountID ?? CONST.DEFAULT_NUMBER_ID,
                          isSelfTourViewed,
                          shouldDismissModal: true,
                          checkIfCurrentPageActive: () => !isUnmounted.current,
                      });
                  });
              } else {
                  Navigation.navigate(ROUTES.INBOX);
              }
          }, [session, isLoadingReportData, conciergeReportID, introSelected, isSelfTourViewed]),
      );

      ConciergePage only checks session.authToken and !isLoadingReportData. As soon as OpenApp settles report data (isLoadingReportData turns false), the focus effect immediately calls navigateToConciergeChat({ shouldDismissModal: true }).

    3. Premature redirection bypasses onboarding:
      Calling navigateToConciergeChat with shouldDismissModal: true prematurely navigates to the Concierge chat report and dismisses any modal before the user has completed the onboarding flow (hasCompletedGuidedSetupFlow === false).

    4. Deep link replay in Link.ts is already designed to open after onboarding:
      In src/libs/actions/Link.ts:621-643, the deep link handler already captures initialHasCompletedGuidedSetupFlow === false and defers opening the link until onboarding is finished via setDeepLinkToOpenAfterOnboarding(...), which is subsequently replayed by navigateAfterOnboarding. The premature navigation observed in this bug is caused exclusively by the independent useFocusEffect inside ConciergePage.tsx.


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

    1. Add onboarding completion check to ConciergePage.tsx

    In src/pages/ConciergePage.tsx:

    • Subscribe to ONYXKEYS.NVP_ONBOARDING to derive both isSelfTourViewed and the raw hasCompletedGuidedSetupFlow.
    • Note: We read the raw onboarding?.hasCompletedGuidedSetupFlow directly instead of hasCompletedGuidedSetupFlowSelector, because hasCompletedGuidedSetupFlowSelector returns true for empty objects ({}) or legacy accounts where the property is not yet defined, whereas false is the explicit signal that a user is actively pending onboarding.
    • Return early in useFocusEffect if hasCompletedGuidedSetupFlow === false.
    • Include hasCompletedGuidedSetupFlow in the useCallback dependency array so that if ConciergePage remains focused when onboarding completes, it can cleanly proceed.
    function ConciergePage() {
        const styles = useThemeStyles();
        const {contentHeaderHeightStyle} = useContentHeaderHeight();
        const isUnmounted = useRef(false);
        const [session] = useOnyx(ONYXKEYS.SESSION);
        const [isLoadingReportData = true] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA);
        const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
        const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
        const [onboarding] = useOnyx(ONYXKEYS.NVP_ONBOARDING);
        const isSelfTourViewed = hasSeenTourSelector(onboarding);
        const hasCompletedGuidedSetupFlow = onboarding?.hasCompletedGuidedSetupFlow;
    
        useFocusEffect(
            useCallback(() => {
                if (session && 'authToken' in session) {
                    Navigation.isNavigationReady().then(() => {
                        if (isUnmounted.current || isLoadingReportData === undefined || !!isLoadingReportData || hasCompletedGuidedSetupFlow === false) {
                            return;
                        }
    
                        navigateToConciergeChat({
                            conciergeReportID,
                            introSelected,
                            currentAccountID: session.accountID ?? CONST.DEFAULT_NUMBER_ID,
                            isSelfTourViewed,
                            shouldDismissModal: true,
                            checkIfCurrentPageActive: () => !isUnmounted.current,
                        });
                    });
                } else {
                    Navigation.navigate(ROUTES.INBOX);
                }
            }, [session, isLoadingReportData, conciergeReportID, introSelected, isSelfTourViewed, hasCompletedGuidedSetupFlow]),
        );

    2. Unit Test Coverage

    Add unit tests in tests/unit/ConciergePageTest.tsx:

    1. Verify that when session.authToken exists, isLoadingReportData is false, and hasCompletedGuidedSetupFlow === false, navigateToConciergeChat is not called.
    2. Verify that when hasCompletedGuidedSetupFlow === true (or undefined for legacy accounts), navigateToConciergeChat is called as expected.
    3. Verify that when hasCompletedGuidedSetupFlow updates from false to true, the focus effect triggers navigation.

    What alternative solutions did you explore? (Optional)

    1. Using hasCompletedGuidedSetupFlowSelector:
      Rejected. As documented in src/selectors/Onboarding.ts and src/libs/actions/Link.ts:543-545, hasCompletedGuidedSetupFlowSelector returns true for empty objects ({}) which can exist transiently on fresh sign-ups before the server responds. Checking hasCompletedGuidedSetupFlow === false on the raw NVP guarantees that pending onboarding is never falsely evaluated as complete.

    2. Intercepting or blocking navigation in OnboardingGuard:
      Rejected. navigateToConciergeChat is a direct action called from components. Intercepting it in router guards increases coupling and risks edge-case regressions across other modals or deeplink handlers. Keeping the guard inside ConciergePage.tsx addresses the root cause directly at the trigger site.

    3. Dropping /concierge route on cold start when logged out:
      Rejected. As established in PR Open a deep link captured before onboarding instead of dropping it #101558, the intended product behavior is to preserve deeplinks opened prior to onboarding so the user reaches their destination immediately after completing setup, rather than discarding the route.

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~0139ed4098bdc50182

  9. melvin-bot commented on Oct 5, 2026

    @melvin-bot

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

  10. amar-sheikh commented on Oct 5, 2026

    @amar-sheikh

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Android and iOS, a new user who opens /concierge and then signs up sees the Concierge chat open before the onboarding flow is completed.

    What is the root cause of that problem?

    I agree with the mechanism already laid out on this thread: native preserves [CONCIERGE] in the initial route stack through sign-in (web lazy-loads its screens and drops it), and ConciergePage's focus effect navigates to the Concierge chat as soon as report data has loaded, with no onboarding check at all:

    useFocusEffect(
    useCallback(() => {
    if (session && 'authToken' in session) {
    Navigation.isNavigationReady().then(() => {
    if (isUnmounted.current || isLoadingReportData === undefined || !!isLoadingReportData) {
    return;
    }
    navigateToConciergeChat({
    conciergeReportID,
    introSelected,
    currentUserAccountID: session.accountID ?? CONST.DEFAULT_NUMBER_ID,
    isSelfTourViewed,
    shouldDismissModal: true,
    checkIfCurrentPageActive: () => !isUnmounted.current,
    });
    });
    } else {
    Navigation.navigate(ROUTES.INBOX);
    }
    }, [session, isLoadingReportData, conciergeReportID, introSelected, isSelfTourViewed]),
    );

    I want to flag a gap in the fix already proposed on this thread (checking the raw NVP_ONBOARDING.hasCompletedGuidedSetupFlow === false), because I think it can still race for exactly the account in this report — a brand-new sign-up.

    The existing hasCompletedGuidedSetupFlowSelector has a documented reason for treating an unloaded NVP as "completed" — it literally can't tell "not loaded yet" apart from "already onboarded":

    /**
    * Selector to get the value of hasCompletedGuidedSetupFlow from the Onyx store
    *
    * `true` means the user has completed the NewDot onboarding flow
    * `false` means the user has not completed the NewDot onboarding flow
    *
    * An unloaded NVP answers `true`, so `false` is the only value a caller can trust, because "not loaded yet"
    * and "onboarded" are indistinguishable.
    */
    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;
    }

    Reading the raw field and checking === false only fixes half of that ambiguity. It correctly stops treating "loaded but legacy/empty" as safe, but it does nothing for the case where NVP_ONBOARDING hasn't synced down from the server at all yet — there, hasCompletedGuidedSetupFlow is undefined, not false, so the proposed guard does not block navigation.

    This isn't hypothetical for this bug. The codebase already has a comment acknowledging the server can come back from openApp() with an empty nvp_onboarding even for a user who is genuinely mid-onboarding, which is why a different onboarding-resume path explicitly re-asserts it client-side after the fact:

    /**
    * After required-2FA setup during incomplete guided setup, reload app data then resume onboarding.
    * Re-asserts incomplete onboarding after openApp (server may return empty nvp_onboarding) so AuthScreens
    * mounts ONBOARDING_MODAL_NAVIGATOR, then starts the flow at the resolved resume path.
    */
    async function resumeOnboardingAfterRequired2FASetup(onboardingFlowParams: GetOnboardingInitialPathParamsType) {
    await openApp();
    await Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: false});

    For the exact repro here — a freshly created account, right after sign-up — NVP_ONBOARDING syncing in after isLoadingReportData has already flipped to false is plausible, not an edge case. If that happens, the proposed fix's guard never triggers, and navigateToConciergeChat still fires early.

    The code that actually, correctly decides whether to mount the onboarding modal (AuthScreens.tsx's isOnboardingCompleted === false check) gets its value from useOnboardingFlowRouter, which tracks this exact loading ambiguity separately from the value itself, via the Onyx result's own loading metadata:

    const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP);
    const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI();
    const [onboardingValues, isOnboardingCompletedMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING);
    const [account] = useOnyx(ONYXKEYS.ACCOUNT);
    const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
    const isLoggingInAsNewSessionUser = isLoggingInAsNewUser(currentUrl, sessionEmail);
    // A user arriving via a Submit-via-PDF secure access link should land directly on the shared report, not onboarding.
    // The signal must survive the whole session and not depend on navigation timing, so we read the captured deep-link URL
    // (set on cold launch and on warm url events for secure links) alongside the web URL and the active route. getCurrentUrl()
    // is empty on native, and getActiveRoute() serializes the secureKey query param on every platform via getPathFromState.
    const {initialURL} = useInitialURLState();
    const getIsVisitingSecureLink = useCallback(() => hasSecureLinkKey(getCurrentUrl()) || hasSecureLinkKey(Navigation.getActiveRoute()) || hasSecureLinkKey(initialURL), [initialURL]);
    const isVisitingSecureLink = getIsVisitingSecureLink();
    const [tryNewDot, tryNewDotMetadata] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {
    selector: tryNewDotOnyxSelector,
    });
    const {isHybridAppOnboardingCompleted, hasBeenAddedToNudgeMigration} = tryNewDot ?? {};
    const isOnboardingLoading = isLoadingOnyxValue(isOnboardingCompletedMetadata, tryNewDotMetadata);

    import type {ResultMetadata} from 'react-native-onyx';
    function isLoadingOnyxValue(...results: ResultMetadata[]): boolean {
    return results.some((result) => result.status === 'loading');
    }

    ConciergePage's proposed fix doesn't use this metadata at all — it only looks at the resolved value.

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

    In ConciergePage, read NVP_ONBOARDING with its metadata (useOnyx(ONYXKEYS.NVP_ONBOARDING) returns [value, metadata]), and block navigation while that key is still loading, not just when it has already resolved to hasCompletedGuidedSetupFlow === false:

    • Block when isLoadingOnyxValue(onboardingMetadata) is true (NVP not synced yet — don't guess).
    • Block when the resolved value's hasCompletedGuidedSetupFlow === false (onboarding confirmed incomplete).
    • Otherwise, proceed as today.

    This mirrors the same loading-vs-value distinction useOnboardingFlowRouter already relies on to correctly gate the onboarding modal itself, instead of re-deriving a second, less careful check inside ConciergePage.

    What alternative solutions did you explore? (Optional)

    Checking only the raw hasCompletedGuidedSetupFlow === false value, as already proposed on this thread, fixes the case hasCompletedGuidedSetupFlowSelector itself warns about (empty/legacy onboarding treated as complete), but not the case where the key hasn't loaded at all yet — which the codebase has already had to work around once, in OnboardingFlow.ts's 2FA-resume path. I'd rather reuse the loading-metadata pattern already proven in useOnboardingFlowRouter than add a second place that can be wrong in the same way.

  11. changed the title [-][$175] Android & iOS - Concierge opens before onboarding is completed[/-] [+][$175] Android - Concierge opens before onboarding is completed[/+] on Oct 5, 2026
  12. kushu7 commented on Oct 5, 2026

    @kushu7
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Android (and iOS), when a user opens staging.new.expensify.com/concierge while logged out (which opens the native app via deep link) and signs up with a new account, the Concierge chat opens immediately and force-dismisses the onboarding flow before the user can complete onboarding.


    What is the root cause of that problem?

    1. Native retains /concierge on the stack: Opening staging.new.expensify.com/concierge triggers the Android App Link intent filter in android/app/src/main/AndroidManifest.xml#L108 (pathPrefix="/concierge"). Unlike Web, Native loads screens synchronously in src/libs/Navigation/AppNavigator/AppNavigator.native.tsx#L25-L30 and preserves SCREENS.CONCIERGE (src/pages/ConciergePage.tsx) at the top of the stack across authentication.

    2. ConciergePage force-dismisses the onboarding modal: src/pages/ConciergePage.tsx#L36-L56 only checks !isLoadingReportData and lacks guards for isLoadingApp or onboarding completion. As soon as report data loads, it calls navigateToConciergeChat({ shouldDismissModal: true }), which force-dismisses the active OnboardingModalNavigator and jumps directly into Concierge chat.

    3. Race condition on fresh sign-ups: Checking only hasCompletedGuidedSetupFlow === false fails because NVP_ONBOARDING is initially undefined before OpenApp resolves. ConciergePage must wait for isLoadingApp === false before evaluating onboarding state.


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

    1. Update src/pages/ConciergePage.tsx:

      • Connect to ONYXKEYS.IS_LOADING_APP to track OpenApp initialization.
      • Connect to ONYXKEYS.NVP_ONBOARDING to read onboarding?.hasCompletedGuidedSetupFlow.
      • In useFocusEffect (src/pages/ConciergePage.tsx#L36-L56), block navigation if:
        • isLoadingApp !== false (app/session data is still loading from the server)
        • isLoadingReportData !== false
        • onboarding?.hasCompletedGuidedSetupFlow === false (user has not completed onboarding)
      • Add isLoadingApp and hasCompletedGuidedSetupFlow to the useCallback dependency array.
    2. Preserve post-onboarding deep link replay:
      Once onboarding completes, navigateAfterOnboarding (src/libs/navigateAfterOnboarding.ts#L120-L122) will automatically replay the parked deep link (deferred via Link.ts / PR #101558) and open Concierge cleanly.

    3. Add unit test coverage in tests/unit/ConciergePageTest.tsx:

      • Verify that navigateToConciergeChat is not called while isLoadingApp is true.
      • Verify that navigateToConciergeChat is not called when hasCompletedGuidedSetupFlow === false.
      • Verify that navigation proceeds once isLoadingApp is false and onboarding is completed (true or legacy accounts).

    What alternative solutions did you explore? (Optional)

    None

  13. 7 remaining items

  14. mukhrr commented on Oct 10, 2026

    @mukhrr
    Contributor

    well, I think even though this is related to #99940, not regression from our PR.
    This issue happens on staging even now (after we reverted PR #101558) and bug in #99940 was a bit different from here.

    The issue (#99940) we were fixing on #101558:

    Expected Result:
    The user is navigated to Concierge chat

    Actual Result:
    The user is navigated to the homepage and the Concierge chat does not open

    This issue (#102956):

    Expected Result:
    Concierge will open after onboarding is completed.

    Actual Result:
    Concierge opens before onboarding is completed.

    But I beleive it's worth fixing this in new PR which we are going to address other regressions from #101558 cc @pecanoro @eh2077

  15. jponikarchuk commented on Oct 19, 2026

    @jponikarchuk

    Deploy Blocker #104391 was identified to be related to the PR #103681 that attempted to fix this issue.

  16. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    ⚠️ Looks like this issue was linked to a Deploy Blocker here

    If you are the assigned CME please investigate whether the linked PR caused a regression and leave a comment with the results.

    If a regression has occurred and you are the assigned CM follow the instructions here.

    If this regression could have been avoided please consider also proposing a recommendation to the PR checklist so that we can avoid it in the future.

  17. changed the title [-][$175] Android - Concierge opens before onboarding is completed[/-] [+][Due for payment 2026-10-27] [$175] Android - Concierge opens before onboarding is completed[/+] on Oct 20, 2026
  18. melvin-bot commented on Oct 20, 2026

    @melvin-bot

    @QichenZhu / @eh2077

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.14-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-10-27. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

  19. melvin-bot commented on Oct 23, 2026

    @melvin-bot

    @QichenZhu Whoops! This issue is 2 days overdue. Let's get this updated quick!

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 contributorOverdue

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions