Skip to content

[$175] Onboarding - Product update / feature announcements still shown to employees invited to a workspace (should be removed) #103244

Open

Description

@m-natarajan

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.3-4
Reproducible in staging?: Y
Reproducible in production?: Y
If this was caught during regression testing, add the test name, ID and link from BrowserStack:
Email or phone of affected tester (no customers):
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: @flaviadefaria
Slack conversation (hyperlinked to channel name): #Expensify Bugs

Action Performed:

  1. As an admin, invite a new member to a workspace.
  2. As the employee, open the invitation email and click Get started.
  3. Go through the resulting onboarding flow.
  4. Observe whether any product update / feature announcement screens appear.

Expected Result:

No product update / feature announcement screens should appear anywhere in this onboarding flow, consistent with their removal from other onboarding paths.

Actual Result:

Product feature announcements still appear for employees going through the "invited to workspace" onboarding flow.

Workaround:

Unknown

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

2026-10-06_18-35-21.3.mp4
Recording.3656.mp4

View all open jobs on GitHub

Upwork Automation - Do Not Edit

Activity

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

    @MelvinBot
    Contributor

    Proposal

    Updated 2026-10-19: Revised based on feedback from emkhalid, camesenin, and the Hide product announcements from new accounts for their first 7 days discussion.

    What is the root cause of that problem?

    Remove the centered AI features promo modal removed the "New to Concierge AI" modal. The only announcement left is the bottom-right product marketing window.

    That window hides during onboarding only when nvp_onboarding.hasCompletedGuidedSetupFlow === false. Invited members skip the NewDot onboarding modal, so that value is never false for them. The window shows over their onboarding.

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

    Base the check on onboarding state, not on account age. That also closes the gap #104044 leaves when someone clicks Get started more than 7 days after the invite.

    In ProductMarketingWindowManager.tsx:

    1. Read NVP_INTRO_SELECTED with a selector that returns inviteType !== undefined && !isInviteOnboardingComplete.
    2. Add its metadata to the loading checks, so the window waits for it.
    3. Treat that flag like hasCompletedGuidedSetupFlow === false. It should record the account in the existing per-session latch and hide the window. The latch keeps the window hidden for the rest of the session, after OpenReport sets isInviteOnboardingComplete to true.

    This uses inviteType directly instead of isSupportedPendingInviteOnboarding. That covers every invite choice, which addresses the earlier feedback. Ordinary members never have inviteType, so they still see the window.

    Tests: In ProductMarketingWindowTest.tsx, add a case for a workspace invitee who still has invite onboarding pending and whose onboarding NVP is complete or empty. The window should stay hidden for the whole session. Add a second case for a member with no inviteType, where the window should still show.

    Open question for emkhalid: you reported invitees landing with {choice: 'newDotLookingAround'} and no inviteType. On the client, that state looks the same as a regular workspace member. Can you share the nvp_introSelected and nvp_onboarding values from your repro, right after Get started? If inviteType really is missing, the App can't detect the invitee safely. The backend would need to send inviteType or hasCompletedGuidedSetupFlow: false for these accounts, and an internal engineer would need to handle that part.

    What alternative solutions did you explore? (Optional)

    • hasNonPersonalPolicy && !isInviteOnboardingComplete: I rejected this. HAS_NON_PERSONAL_POLICY is true for anyone in a group workspace, and isInviteOnboardingComplete is never set for most members. The window would stay hidden for nearly every workspace member, permanently.
    • choice === 'newDotLookingAround' plus hasNonPersonalPolicy: I rejected this too. Users who pick that choice in normal onboarding and then join a workspace would never see announcements again.
    • Waiting for Hide product announcements from new accounts for their first 7 days #104044: that only checks account age. It misses invitees who click Get started more than 7 days after the invite.
    Code references

    Confidence: medium. The App code path is clear. I couldn't confirm which onboarding values the backend sends for invitees.


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

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

    @melvin-bot

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

  5. changed the title [-]Onboarding - Product update / feature announcements still shown to employees invited to a workspace (should be removed)[/-] [+][$175] Onboarding - Product update / feature announcements still shown to employees invited to a workspace (should be removed)[/+] on Oct 6, 2026
  6. melvin-bot commented on Oct 6, 2026

    @melvin-bot
  7. camesenin commented on Oct 6, 2026

    @camesenin
    Proposal

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

    When an employee is invited to a workspace, opens the invitation email, and clicks "Get started", product feature announcements and marketing surfaces (specifically the centered "New to Concierge AI" promo modal and the bottom-right persistent "Connect Expensify to Claude" product marketing window) still appear over their workspace onboarding flow instead of being suppressed.

    What is the root cause of that problem?

    1. In NewDot, there are two distinct onboarding tracks:

      • Organic Sign-up Onboarding: Tracked via ONYXKEYS.NVP_ONBOARDING. It begins with hasCompletedGuidedSetupFlow === false and presents the OnboardingModalNavigator.
      • Invited Member Onboarding (Workspace Invites): Invited users intentionally bypass startOnboardingFlow() and OnboardingModalNavigator (as seen in useOnboardingFlow.ts:109-112). Their onboarding lifecycle is tracked via ONYXKEYS.NVP_INTRO_SELECTED (inviteType: CONST.ONBOARDING_INVITE_TYPES.WORKSPACE with isInviteOnboardingComplete: false). For these invited users, NVP_ONBOARDING is either empty or does not have hasCompletedGuidedSetupFlow === false (hasCompletedGuidedSetupFlowSelector defaults to true).
    2. Both promotional surfaces rely exclusively on hasCompletedGuidedSetupFlow === false to detect active onboarding and latch session suppression:

      • In src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:72-86:
        const shouldRecordActiveOnboarding =
            !isLoadingOnboardingContext && !isLoadingApp && !isActingAsDelegate && !isSwitchingToDelegator && currentAccountID !== undefined && hasCompletedGuidedSetupFlow === false;
        ...
        const shouldSuppressForOnboardingSession = hasCompletedGuidedSetupFlow === false || (currentAccountID !== undefined && accountIDsWithObservedActiveOnboarding.has(currentAccountID));
      • In src/hooks/useAIFeaturesPromoModal.ts:64-69, 82:
        useEffect(() => {
            if (hasCompletedOnboarding !== false) {
                return;
            }
            observedActiveOnboardingThisSession = true;
        }, [hasCompletedOnboarding]);
    3. Because hasCompletedGuidedSetupFlow is not false for invited members, neither surface ever detects active onboarding nor sets their session suppression latches (accountIDsWithObservedActiveOnboarding and observedActiveOnboardingThisSession). Consequently, upon landing in the workspace chat, both the persistent bottom-right Claude window and the centered Concierge AI promo modal render immediately.

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

    1. Add isPendingInviteOnboardingSelector in src/selectors/Onboarding.ts:

      /**
       * Selector to check if the user is in a pending invited-member onboarding flow
       */
      function isPendingInviteOnboardingSelector(introSelected: OnyxValue<typeof ONYXKEYS.NVP_INTRO_SELECTED>): boolean {
          return Boolean(introSelected?.inviteType && !introSelected?.isInviteOnboardingComplete);
      }

      Export isPendingInviteOnboardingSelector.

    2. Update src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:

      • Subscribe to ONYXKEYS.NVP_INTRO_SELECTED:
        const [isPendingInviteOnboarding = false, introSelectedMetadata] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {
            selector: isPendingInviteOnboardingSelector,
        });
      • Include introSelectedMetadata in isLoadingOnboardingContext and isLoading:
        const isLoadingOnboardingContext = isLoadingOnyxValue(currentAccountIDMetadata, stashedAccountIDMetadata, accountMetadata, onboardingMetadata, introSelectedMetadata, isLoadingAppMetadata);
      • Update shouldRecordActiveOnboarding and shouldSuppressForOnboardingSession:
        const isActivelyOnboarding = hasCompletedGuidedSetupFlow === false || isPendingInviteOnboarding;
        
        const shouldRecordActiveOnboarding =
            !isLoadingOnboardingContext && !isLoadingApp && !isActingAsDelegate && !isSwitchingToDelegator && currentAccountID !== undefined && isActivelyOnboarding;
        if (shouldRecordActiveOnboarding && !accountIDsWithObservedActiveOnboarding.has(currentAccountID)) {
            setAccountIDsWithObservedActiveOnboarding((accountIDs) => new Set(accountIDs).add(currentAccountID));
        }
        ...
        const shouldSuppressForOnboardingSession = isActivelyOnboarding || (currentAccountID !== undefined && accountIDsWithObservedActiveOnboarding.has(currentAccountID));
    3. Update src/hooks/useAIFeaturesPromoModal.ts:

      • Subscribe to ONYXKEYS.NVP_INTRO_SELECTED:
        const [isPendingInviteOnboarding = false, introSelectedMetadata] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {
            selector: isPendingInviteOnboardingSelector,
        });
      • Include introSelectedMetadata in isAllOnyxLoaded:
        const isAllOnyxLoaded = !isLoadingOnyxValue(isLoadingAppMetadata, dismissedProductTrainingMetadata, tryNewDotMetadata, onboardingMetadata, introSelectedMetadata);
      • Latch observedActiveOnboardingThisSession when either regular onboarding or invite onboarding is pending:
        const isActivelyOnboarding = hasCompletedOnboarding === false || isPendingInviteOnboarding;
        
        useEffect(() => {
            if (!isActivelyOnboarding) {
                return;
            }
            observedActiveOnboardingThisSession = true;
        }, [isActivelyOnboarding]);
      • Guard isEligible with !isActivelyOnboarding:
        const isEligible =
            isAllOnyxLoaded &&
            !!session?.authToken &&
            !isLoadingApp &&
            !shouldSuppressPromotionalUI &&
            !hasRedirectedToAIFeaturesPromoModal &&
            !isAIPromoModalDismissed &&
            !isMigrationModalPending &&
            !observedActiveMigrationModalThisSession &&
            !observedActiveOnboardingThisSession &&
            !isActivelyOnboarding;
    4. Tests:

      • In tests/unit/ProductMarketingWindowTest.tsx:
        Add unit test asserting that an invited member with {inviteType: 'workspace', isInviteOnboardingComplete: false} suppresses the marketing window, and remains suppressed throughout the session even after isInviteOnboardingComplete flips to true.
      • In tests/unit/hooks/useAIFeaturesPromoModal.test.ts:
        Add unit test asserting that when isPendingInviteOnboarding is true, useAIFeaturesPromoModal does not schedule or navigate to AI_FEATURES_PROMO_MODAL.

    What alternative solutions did you explore? (Optional)

    1. Setting hasCompletedGuidedSetupFlow: false on the backend for invite-created accounts: Rejected because useOnboardingFlow and OnboardingGuard would redirect the user into the standard NewDot company-size / purpose onboarding modal, which invited users intentionally bypass.
    2. Checking isSupportedPendingInviteOnboarding from OnboardingUtils: Rejected because isSupportedPendingInviteOnboarding strictly requires choice to match ADMIN, SUBMIT, or CHAT_SPLIT. Using Boolean(introSelected?.inviteType && !introSelected?.isInviteOnboardingComplete) is universal and guarantees suppression across all workspace invite variants regardless of pre-assigned choice state.
  8. camesenin commented on Oct 6, 2026

    @camesenin
    Proposal (Updated)

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

    When an employee is invited to a workspace, opens the invitation email, and clicks "Get started", product feature announcements and marketing surfaces (specifically the centered "New to Concierge AI" promo modal and the bottom-right persistent "Connect Expensify to Claude" product marketing window) still appear over their workspace onboarding flow instead of being suppressed.

    What is the root cause of that problem?

    1. In NewDot, there are two distinct onboarding tracks:

      • Organic Sign-up Onboarding: Tracked via ONYXKEYS.NVP_ONBOARDING. It begins with hasCompletedGuidedSetupFlow === false and presents the OnboardingModalNavigator.
      • Invited Member Onboarding (Workspace Invites): Invited users intentionally bypass startOnboardingFlow() and OnboardingModalNavigator (as seen in useOnboardingFlow.ts:109-112 via isInvitedOrGroupMember = (hasNonPersonalPolicy || wasInvitedToNewDot) ?? false). For invited users, hasCompletedGuidedSetupFlowSelector defaults to true because they skip the guided wizard.
    2. Both promotional surfaces rely exclusively on hasCompletedGuidedSetupFlow === false to detect active onboarding and latch session suppression:

      • In src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:72-86:
        const shouldRecordActiveOnboarding =
            !isLoadingOnboardingContext && !isLoadingApp && !isActingAsDelegate && !isSwitchingToDelegator && currentAccountID !== undefined && hasCompletedGuidedSetupFlow === false;
        ...
        const shouldSuppressForOnboardingSession = hasCompletedGuidedSetupFlow === false || (currentAccountID !== undefined && accountIDsWithObservedActiveOnboarding.has(currentAccountID));
      • In src/hooks/useAIFeaturesPromoModal.ts:64-69, 82:
        useEffect(() => {
            if (hasCompletedOnboarding !== false) {
                return;
            }
            observedActiveOnboardingThisSession = true;
        }, [hasCompletedOnboarding]);
    3. Because hasCompletedGuidedSetupFlow is not false for invited members, neither surface ever detects active onboarding nor sets their session suppression latches (accountIDsWithObservedActiveOnboarding and observedActiveOnboardingThisSession). Consequently, upon landing in the workspace chat, both the persistent bottom-right Claude window and the centered Concierge AI promo modal render immediately.

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

    1. Add Canonical Invited/Group Member Guard in src/selectors/Onboarding.ts:
      Mirror NewDot's canonical check from useOnboardingFlow.ts:109:

      /**
       * Selector to check if a user is an invited or group workspace member,
       * covering both explicit inviteType and users landing with non-personal policies.
       */
      function isPendingInviteOnboarding(
          introSelected: OnyxValue<typeof ONYXKEYS.NVP_INTRO_SELECTED>,
          hasNonPersonalPolicy: boolean | undefined,
      ): boolean {
          const wasInvited = Boolean(introSelected?.inviteType && !introSelected?.isInviteOnboardingComplete);
          const isGroupMemberPending = Boolean(hasNonPersonalPolicy && !introSelected?.isInviteOnboardingComplete);
          return wasInvited || isGroupMemberPending;
      }

      This handles both users with introSelected.inviteType populated and invited members who land with hasNonPersonalPolicy === true (e.g. under choice: 'newDotLookingAround').

    2. Update src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:

      • Subscribe to ONYXKEYS.NVP_INTRO_SELECTED and ONYXKEYS.HAS_NON_PERSONAL_POLICY:
        const [introSelected, introSelectedMetadata] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
        const [hasNonPersonalPolicy, hasNonPersonalPolicyMetadata] = useOnyx(ONYXKEYS.HAS_NON_PERSONAL_POLICY);
      • Include metadata in isLoadingOnboardingContext and isLoading:
        const isLoadingOnboardingContext = isLoadingOnyxValue(
            currentAccountIDMetadata,
            stashedAccountIDMetadata,
            accountMetadata,
            onboardingMetadata,
            introSelectedMetadata,
            hasNonPersonalPolicyMetadata,
            isLoadingAppMetadata,
        );
      • Update shouldRecordActiveOnboarding and shouldSuppressForOnboardingSession:
        const isPendingInvite = isPendingInviteOnboarding(introSelected, hasNonPersonalPolicy);
        const isActivelyOnboarding = hasCompletedGuidedSetupFlow === false || isPendingInvite;
        
        const shouldRecordActiveOnboarding =
            !isLoadingOnboardingContext && !isLoadingApp && !isActingAsDelegate && !isSwitchingToDelegator && currentAccountID !== undefined && isActivelyOnboarding;
        if (shouldRecordActiveOnboarding && !accountIDsWithObservedActiveOnboarding.has(currentAccountID)) {
            setAccountIDsWithObservedActiveOnboarding((accountIDs) => new Set(accountIDs).add(currentAccountID));
        }
        ...
        const shouldSuppressForOnboardingSession = isActivelyOnboarding || (currentAccountID !== undefined && accountIDsWithObservedActiveOnboarding.has(currentAccountID));
    3. Update src/hooks/useAIFeaturesPromoModal.ts:

      • Subscribe to ONYXKEYS.NVP_INTRO_SELECTED and ONYXKEYS.HAS_NON_PERSONAL_POLICY:
        const [introSelected, introSelectedMetadata] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
        const [hasNonPersonalPolicy, hasNonPersonalPolicyMetadata] = useOnyx(ONYXKEYS.HAS_NON_PERSONAL_POLICY);
      • Include metadata in isAllOnyxLoaded.
      • Latch observedActiveOnboardingThisSession when either regular onboarding or invite onboarding is active:
        const isPendingInvite = isPendingInviteOnboarding(introSelected, hasNonPersonalPolicy);
        const isActivelyOnboarding = hasCompletedOnboarding === false || isPendingInvite;
        
        useEffect(() => {
            if (!isActivelyOnboarding) {
                return;
            }
            observedActiveOnboardingThisSession = true;
        }, [isActivelyOnboarding]);
      • Guard isEligible with !isActivelyOnboarding.
    4. Tests:

    What alternative solutions did you explore? (Optional)

    1. Setting hasCompletedGuidedSetupFlow: false on the backend for invite-created accounts: Rejected because useOnboardingFlow and OnboardingGuard would redirect the user into the standard NewDot company-size / purpose onboarding modal, which invited users intentionally bypass.
    2. Checking only introSelected.inviteType === 'workspace': Rejected because in production runtime, newly invited members can land under choice: 'newDotLookingAround' where inviteType is unpopulated, but hasNonPersonalPolicy is present. Mirroring useOnboardingFlow.ts:109 covers both representations robustly.
  9. melvin-bot commented on Oct 6, 2026

    @melvin-bot

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

  10. emkhalid commented on Oct 7, 2026

    @emkhalid
    Contributor

    @shubham1206agra I investigated and reproduced this locally, and wanted to share a quick finding regarding Melvin's proposal above:

    • Why Melvin's proposal is incomplete: Melvin assumes invited workspace members always have introSelected.inviteType === 'workspace'. However, in real runtime, newly invited members often land with introSelected: {choice: 'newDotLookingAround'} where inviteType is completely undefined. Under Melvin's proposed check, it evaluates to false and the popups still appear.
    • Root cause: Both promotional surfaces strictly rely on hasCompletedGuidedSetupFlow === false to latch session suppression. Since invited members bypass the NewDot onboarding wizard, their account starts with hasCompletedGuidedSetupFlow: true, so suppression never triggers.

    Demos

    • Reproduction Demo:
    popup-showing-repro.mov
    • Fix Demo:
    popup-showing-fix.mov

    I have a working fix that reliably suppresses the promotional surfaces for all newly invited members. If you'd like, I can submit the full proposal for your review!

  11. camesenin commented on Oct 7, 2026

    @camesenin

    @shubham1206agra Great observation by @emkhalid on Melvin's proposal regarding invited members who land with choice: 'newDotLookingAround' where inviteType is undefined.

    In my proposal above, to make the suppression 100% robust against all invitation landing states, I've updated the check to mirror NewDot's canonical invited-member guard from useOnboardingFlow.ts:109:

    const isInvitedOrGroupMember = (hasNonPersonalPolicy || wasInvitedToNewDot) ?? false;

    By subscribing to ONYXKEYS.HAS_NON_PERSONAL_POLICY alongside ONYXKEYS.NVP_INTRO_SELECTED, both promotional surfaces (ProductMarketingWindowManager and useAIFeaturesPromoModal) reliably suppress marketing windows for all invited workspace members regardless of whether inviteType is explicitly populated or they land under newDotLookingAround.

    The proposal above has been updated with this complete implementation and test coverage.

  12. yuvrajangadsingh commented on Oct 7, 2026

    @yuvrajangadsingh
    Contributor

    withdrawing this, it went up before the issue had the Help Wanted label. will come back if the label is added.

  13. jloa-dev commented on Oct 8, 2026

    @jloa-dev

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

    Proposal

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

    When a new employee joins via a workspace invite email, promotional product feature announcements (e.g. the "Connect Expensify to Claude" marketing window and the "New to Concierge AI" promo modal) are still displayed during their initial session instead of being suppressed.

    What is the root cause of that problem?

    1. In src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:63-76 and src/hooks/useAIFeaturesPromoModal.ts:64-69, promo displays are gated solely by checking if hasCompletedGuidedSetupFlow === false on ONYXKEYS.NVP_ONBOARDING.
    2. Invited workspace members do not undergo the standard consumer guided-setup flow. Instead, OnboardingGuard.ts and useOnboardingFlow.ts route them through the workspace invite onboarding flow (isSupportedPendingInviteOnboarding via ONYXKEYS.NVP_INTRO_SELECTED).
    3. For invited members, hasCompletedGuidedSetupFlow is already treated as true (or unset), so both promotional managers evaluate isOnboarding = false on first render and prematurely display marketing windows over the invite experience.

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

    1. In ProductMarketingWindowManager.tsx:
      Subscribe to ONYXKEYS.NVP_INTRO_SELECTED using selector isSupportedPendingInviteOnboarding:
      const [isPendingInviteOnboarding = false, introSelectedMetadata] = useOnyx(
          ONYXKEYS.NVP_INTRO_SELECTED,
          {selector: isSupportedPendingInviteOnboarding}
      );
      const isOnboardingInProgress = hasCompletedGuidedSetupFlow === false || isPendingInviteOnboarding;
      Update shouldRecordActiveOnboarding and shouldSuppressForOnboardingSession to rely on isOnboardingInProgress.
    2. In useAIFeaturesPromoModal.ts:
      Apply the same isOnboardingInProgress check before scheduling or triggering the AI promo modal.
    3. This reliably suppresses product update announcements for employees invited to a workspace during their onboarding session without affecting standard onboarded users.

    What specific scenarios should we cover in automated tests?

    In tests/unit/ProductMarketingWindowTest.tsx:

    1. Verify ProductMarketingWindow does not render when user has isPendingInviteOnboarding: true, even if hasCompletedGuidedSetupFlow is true.
    2. Verify promo modal displays normally once invite onboarding is completed in subsequent sessions.
  14. 3 remaining items

  15. melvin-bot commented on Oct 13, 2026

    @melvin-bot

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

  16. melvin-bot commented on Oct 15, 2026

    @melvin-bot

    @shubham1206agra Now this issue is 8 days overdue. Are you sure this should be a Daily? Feel free to change it!

  17. shubham1206agra commented on Oct 18, 2026

    @shubham1206agra
    Contributor

    @MelvinBot This issue should be solved with #104044, right?

  18. MelvinBot commented on Oct 18, 2026

    @MelvinBot
    Contributor

    Mostly yes. Once Hide product announcements from new accounts for their first 7 days ships, it should cover this issue. Two things have changed since my proposal:

    1. Remove the centered AI features promo modal merged on 2026-10-16. That removed the "New to Concierge AI" modal, so only the bottom-right marketing window is left.
    2. Hide product announcements from new accounts for their first 7 days #104044 hides that window for any account less than 7 days old. It also never shows an announcement released before the account was created. Invited employees going through "Get started" have new accounts, so they're covered.

    There's one gap. Say an admin invites someone, and the person clicks Get started more than 7 days later. If an announcement was released after the invite, they would still see it during onboarding. This is because #104044 checks account age, not onboarding state. If that's acceptable, you can put this issue on hold for #104044 or close it.

    Code references

    view run

  19. emkhalid commented on Oct 19, 2026

    @emkhalid
    Contributor

    @shubham1206agra Relying on #104044 would leave this bug partially unresolved for two key reasons:

    1. Account age vs. Onboarding state: Hide product announcements from new accounts for their first 7 days #104044 only checks if the account is < 7 days old. If an employee clicks "Get started" more than 7 days after the invite is sent, or if an existing user is invited to a new workspace, their account is > 7 days old, so the announcement window will still appear over their workspace onboarding flow.
    2. Hide product announcements from new accounts for their first 7 days #104044 is an unstarted internal feature: It requires backend API changes (account creation date) and announcement release dates, whereas [$175] Onboarding - Product update / feature announcements still shown to employees invited to a workspace (should be removed) #103244 is an open bug with a clean, FE-only fix.

    Also, since #104177 recently removed the AI promo modal, the fix for this issue is now even simpler—it only requires a small change in ProductMarketingWindowManager.tsx to ensure the existing onboarding session latch respects invited members.

    I have this tested and ready to go. Happy to post the updated proposal if you'd like!

  20. shubham1206agra commented on Oct 19, 2026

    @shubham1206agra
    Contributor
  21. MelvinBot commented on Oct 19, 2026

    @MelvinBot
    Contributor

    I updated my proposal. Here's what changed:

    • Smaller scope: Remove the centered AI features promo modal already removed the AI promo modal. The fix now touches only ProductMarketingWindowManager.tsx.
    • Broader invite check: the new check is inviteType !== undefined && !isInviteOnboardingComplete, instead of isSupportedPendingInviteOnboarding. It covers every invite choice.
    • Based on onboarding state, not account age: that closes the late Get started gap that Hide product announcements from new accounts for their first 7 days #104044 leaves.
    • Rejected hasNonPersonalPolicy: that check would hide the window for nearly every workspace member.

    One thing still needs an answer. emkhalid, can you share the nvp_introSelected and nvp_onboarding values from your repro? If invitees really arrive with no inviteType, the App can't tell them apart from regular members. That part would need a backend change.


    view run

  22. shubham1206agra commented on Oct 19, 2026

    @shubham1206agra
    Contributor
  23. emkhalid commented on Oct 19, 2026

    @emkhalid
    Contributor

    @MelvinBot @shubham1206agra Here are the exact Onyx values from the local reproduction when an invited workspace member lands via the invitation link:

    ONYXKEYS.NVP_INTRO_SELECTED:

    {
      "choice": "newDotLookingAround"
    }

    ONYXKEYS.NVP_ONBOARDING:

    {
      "hasCompletedGuidedSetupFlow": true
    }

    Why a Backend Change is NOT Needed

    Melvin mentioned that if inviteType is undefined, the client cannot differentiate new invitees from regular members without a backend change.

    However, the client already has a reliable signal in ONYXKEYS.ACCOUNT:

    {
      "accountExists": false,
      "validated": false,
      "activePolicyID": "0C24F127B3E6EBE4"
    }

    When an account is newly provisioned from an invite email, the backend sets account.accountExists === false.

    Our Solution Summary

    In ProductMarketingWindowManager.tsx:

    1. Check both pending invite onboarding and newly provisioned accounts:
      const isActivelyOnboarding = hasCompletedGuidedSetupFlow === false || isPendingInviteOnboarding || account?.accountExists === false;
    2. Include introSelectedMetadata in the loading checks to avoid any pop-in flash before hydration.
    3. Record currentAccountID in the existing session latch (accountIDsWithObservedActiveOnboarding) when isActivelyOnboarding is observed, ensuring announcements stay suppressed for the entire session.

    Key Advantages:

    • Zero backend dependency: Solves the problem 100% on the frontend.
    • Complete coverage: Protects both users with explicit inviteType and users provisioned under newDotLookingAround.
    • Safe: Existing workspace members already have established accounts (accountExists is true/undefined), so announcements continue to display for them normally.

    @shubham1206agra I have already verified and tested this locally with full repro and fix demos. If you'd like, let me know and I'll be happy to submit the full proposal!

  24. shubham1206agra commented on Oct 19, 2026

    @shubham1206agra
    Contributor
  25. MelvinBot commented on Oct 19, 2026

    @MelvinBot
    Contributor

    Thanks, emkhalid. Your values show that invitees can arrive without inviteType, so my inviteType check alone won't fix this. I don't recommend account.accountExists === false as the replacement, though, for two reasons:

    1. It doesn't track onboarding. The App reads accountExists only on the sign-in page, to choose between sign-in and sign-up. Nothing in the App resets it after sign-in. Only sign-in-page actions and sign-out clear ACCOUNT. If it stays false, the window would stay hidden on that device until the user signs out, not just during onboarding.
    2. It depends on the device and the sign-in path. The value comes from the sign-in response for the email typed on that device. Your values come from a local backend. I can't confirm that production sends false for every invitee. I also can't confirm it's ever refreshed to true. If the user signs in on a second device, the value could differ there.

    shubham1206agra, an internal engineer needs to confirm what production sends for a workspace invitee. Ideally, invitees should get inviteType in nvp_introSelected, and then the App fix in my proposal works as written. If accountExists turns out to be refreshed after sign-in, emkhalid's approach becomes safe. Either way, this needs backend confirmation, not just App code.

    Code references

    view run

  26. emkhalid commented on Oct 19, 2026

    @emkhalid
    Contributor

    @shubham1206agra @MelvinBot I ran a fresh reproduction following the exact issue steps (Admin invites a brand-new email to a workspace -> employee clicks the invitation link in email):

    I am pleased to confirm that the backend already sends the complete invite data in production/staging. There is no need for an Internal Engineer (IE) review or backend changes.

    Here is the exact server response received from both SigninUserWithLink and OpenApp:

    {
      "key": "nvp_introSelected",
      "value": {
        "choice": "newDotSubmit",
        "inviteType": "workspace",
        "isInviteOnboardingComplete": false
      }
    },
    {
      "key": "nvp_onboarding",
      "value": {}
    }
    Image

    Root Cause

    1. The backend provisions nvp_onboarding as {} for invited workspace members.
    2. In src/selectors/Onboarding.ts, hasCompletedGuidedSetupFlowSelector({}) defaults to true.

    }
    if (!isEmptyObject(onboarding) && onboarding?.hasCompletedGuidedSetupFlow === undefined) {
    return true;
    }
    return onboarding?.hasCompletedGuidedSetupFlow;
    }

    1. In ProductMarketingWindowManager.tsx:72-85, the active onboarding check strictly relies on hasCompletedGuidedSetupFlow === false. Because it evaluates to true, the component assumes onboarding is finished and immediately shows the marketing window.

    !isLoadingOnboardingContext && !isLoadingApp && !isActingAsDelegate && !isSwitchingToDelegator && currentAccountID !== undefined && hasCompletedGuidedSetupFlow === false;
    if (shouldRecordActiveOnboarding && !accountIDsWithObservedActiveOnboarding.has(currentAccountID)) {
    setAccountIDsWithObservedActiveOnboarding((accountIDs) => new Set(accountIDs).add(currentAccountID));
    }
    const announcement = ACTIVE_PRODUCT_MARKETING_ANNOUNCEMENT;
    const illustrationNames = announcement?.visual.type === 'illustration' ? [announcement.visual.name] : [];
    const illustrations = useMemoizedLazyIllustrations(illustrationNames);
    const isDismissed = isProductMarketingAnnouncementDismissed(announcement, lastDismissedMarketingWindow);
    const isCoveredByCenteredModalScreen = !!topmostRouteName && CENTERED_MODAL_SCREEN_NAVIGATORS.has(topmostRouteName);
    const isLoading =
    isLoadingOnyxValue(lastDismissedMarketingWindowMetadata, isLoadingAppMetadata, currentAccountIDMetadata, stashedAccountIDMetadata, accountMetadata, onboardingMetadata) ||
    isLoadingApp;
    const shouldSuppressForOnboardingSession = hasCompletedGuidedSetupFlow === false || (currentAccountID !== undefined && accountIDsWithObservedActiveOnboarding.has(currentAccountID));


    Full Solution (100% Frontend)

    1. In src/selectors/Onboarding.ts, export a selector to detect pending invite onboarding:
      export {
      hasCompletedGuidedSetupFlowSelector,
      tryNewDotOnyxSelector,
      hasSeenTourSelector,
      wasInvitedToNewDotSelector,
      guidedSetupAndTourStatusSelector,
      isTrackIntentUserSelector,
      isLookingAroundUserSelector,
      };
    function isPendingInviteOnboardingSelector(introSelected: OnyxValue<typeof ONYXKEYS.NVP_INTRO_SELECTED>): boolean {
        return Boolean(introSelected?.inviteType && !introSelected?.isInviteOnboardingComplete);
    }
    1. In src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:
    • Subscribe to ONYXKEYS.NVP_INTRO_SELECTED using isPendingInviteOnboardingSelector:

      const [hasCompletedGuidedSetupFlow, onboardingMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasCompletedGuidedSetupFlowSelector});
      const [accountIDsWithObservedActiveOnboarding, setAccountIDsWithObservedActiveOnboarding] = useState<ReadonlySet<number>>(() => new Set());
      // OpenApp provides the dismissal data; wait for it to avoid a startup flash of an already-dismissed window.
      const [isLoadingApp = true, isLoadingAppMetadata] = useOnyx(ONYXKEYS.IS_LOADING_APP);
      // The session changes before loading/delegate data during Copilot entry. A failed connection keeps the original account ID.
      // Supportal also stashes sessions, but its existing marketing eligibility should remain unchanged.
      const isSwitchingToDelegator = !isSupportalSession && stashedAccountID !== undefined && stashedAccountID !== currentAccountID;
      const isLoadingOnboardingContext = isLoadingOnyxValue(currentAccountIDMetadata, stashedAccountIDMetadata, accountMetadata, onboardingMetadata, isLoadingAppMetadata);
      const shouldRecordActiveOnboarding =
      !isLoadingOnboardingContext && !isLoadingApp && !isActingAsDelegate && !isSwitchingToDelegator && currentAccountID !== undefined && hasCompletedGuidedSetupFlow === false;
      if (shouldRecordActiveOnboarding && !accountIDsWithObservedActiveOnboarding.has(currentAccountID)) {
      setAccountIDsWithObservedActiveOnboarding((accountIDs) => new Set(accountIDs).add(currentAccountID));
      }
      const announcement = ACTIVE_PRODUCT_MARKETING_ANNOUNCEMENT;
      const illustrationNames = announcement?.visual.type === 'illustration' ? [announcement.visual.name] : [];
      const illustrations = useMemoizedLazyIllustrations(illustrationNames);
      const isDismissed = isProductMarketingAnnouncementDismissed(announcement, lastDismissedMarketingWindow);
      const isCoveredByCenteredModalScreen = !!topmostRouteName && CENTERED_MODAL_SCREEN_NAVIGATORS.has(topmostRouteName);
      const isLoading =
      isLoadingOnyxValue(lastDismissedMarketingWindowMetadata, isLoadingAppMetadata, currentAccountIDMetadata, stashedAccountIDMetadata, accountMetadata, onboardingMetadata) ||
      isLoadingApp;
      const shouldSuppressForOnboardingSession = hasCompletedGuidedSetupFlow === false || (currentAccountID !== undefined && accountIDsWithObservedActiveOnboarding.has(currentAccountID));

      const [isPendingInviteOnboarding = false, introSelectedMetadata] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {
          selector: isPendingInviteOnboardingSelector,
      });
    • Define active onboarding:

      const isActivelyOnboarding = hasCompletedGuidedSetupFlow === false || isPendingInviteOnboarding;
    • Include introSelectedMetadata in isLoadingOnboardingContext and isLoading to prevent any flash before hydration.

    • Update shouldRecordActiveOnboarding and shouldSuppressForOnboardingSession to use isActivelyOnboarding:

      const shouldRecordActiveOnboarding =
          !isLoadingOnboardingContext && !isLoadingApp && !isActingAsDelegate && !isSwitchingToDelegator && currentAccountID !== undefined && isActivelyOnboarding;
      if (shouldRecordActiveOnboarding && !accountIDsWithObservedActiveOnboarding.has(currentAccountID)) {
          setAccountIDsWithObservedActiveOnboarding((accountIDs) => new Set(accountIDs).add(currentAccountID));
      }
      ...
      const shouldSuppressForOnboardingSession = isActivelyOnboarding || (currentAccountID !== undefined && accountIDsWithObservedActiveOnboarding.has(currentAccountID));

    How It Works:

    • During initial landing: isPendingInviteOnboarding evaluates to true. This adds currentAccountID to accountIDsWithObservedActiveOnboarding and hides the window.
    • After report opens: When OpenReport executes and sets isInviteOnboardingComplete: true, the session latch (accountIDsWithObservedActiveOnboarding.has(currentAccountID)) keeps the window suppressed for the remainder of their session.
    • Existing workspace members: For existing members who already completed invite onboarding (isInviteOnboardingComplete: true), isPendingInviteOnboarding is false, so marketing announcements display normally.

    I have verified this locally and it resolves the issue cleanly with zero backend changes or dependencies.

  27. emkhalid commented on Oct 19, 2026

    @emkhalid
    Contributor

    @shubham1206agra Following up on the above, I've implemented and thoroughly verified the fix locally. Here is the video demo showing the fix in action:

    Screen.Recording.2026-10-19.at.11.58.38.PM.mp4

    We both put a lot of time into investigating this, proving the exact root cause, and confirming that the backend was working properly all along. The fix is clean, minimal, 100% frontend, and completely safe for all existing members.

    If you agree with this approach, I’d love to be assigned to this issue so I can open the PR right away! 🚀

  28. melvin-bot commented on Oct 20, 2026

    @melvin-bot

    @shubham1206agra this issue was created 2 weeks ago. Are we close to approving a proposal? If not, what's blocking us from getting this issue assigned? Don't hesitate to create a thread in #expensify-open-source to align faster in real time. Thanks!

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 contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions