You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[$175] Onboarding - Product update / feature announcements still shown to employees invited to a workspace (should be removed) #103244
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:
As an admin, invite a new member to a workspace.
As the employee, open the invitation email and click Get started.
Go through the resulting onboarding flow.
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:
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:
Read NVP_INTRO_SELECTED with a selector that returns inviteType !== undefined && !isInviteOnboardingComplete.
Add its metadata to the loading checks, so the window waits for it.
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.
isInviteOnboardingComplete is set to true only in OpenReport optimistic data: Report/index.ts:1674-1679
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.
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
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?
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).
Both promotional surfaces rely exclusively on hasCompletedGuidedSetupFlow === false to detect active onboarding and latch session suppression:
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?
/** * Selector to check if the user is in a pending invited-member onboarding flow */functionisPendingInviteOnboardingSelector(introSelected: OnyxValue<typeofONYXKEYS.NVP_INTRO_SELECTED>): boolean{returnBoolean(introSelected?.inviteType&&!introSelected?.isInviteOnboardingComplete);}
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)
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.
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.
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?
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.
Both promotional surfaces rely exclusively on hasCompletedGuidedSetupFlow === false to detect active onboarding and latch session suppression:
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?
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. */functionisPendingInviteOnboarding(introSelected: OnyxValue<typeofONYXKEYS.NVP_INTRO_SELECTED>,hasNonPersonalPolicy: boolean|undefined,): boolean{constwasInvited=Boolean(introSelected?.inviteType&&!introSelected?.isInviteOnboardingComplete);constisGroupMemberPending=Boolean(hasNonPersonalPolicy&&!introSelected?.isInviteOnboardingComplete);returnwasInvited||isGroupMemberPending;}
This handles both users with introSelected.inviteType populated and invited members who land with hasNonPersonalPolicy === true (e.g. under choice: 'newDotLookingAround').
In tests/unit/ProductMarketingWindowTest.tsx:
Assert that invited members (with {inviteType: 'workspace'} OR with hasNonPersonalPolicy: true) suppress the marketing window, and remain suppressed throughout the session.
What alternative solutions did you explore? (Optional)
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.
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.
@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!
@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:
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.
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?
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.
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).
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?
In ProductMarketingWindowManager.tsx:
Subscribe to ONYXKEYS.NVP_INTRO_SELECTED using selector isSupportedPendingInviteOnboarding:
Update shouldRecordActiveOnboarding and shouldSuppressForOnboardingSession to rely on isOnboardingInProgress.
In useAIFeaturesPromoModal.ts:
Apply the same isOnboardingInProgress check before scheduling or triggering the AI promo modal.
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:
Verify ProductMarketingWindow does not render when user has isPendingInviteOnboarding: true, even if hasCompletedGuidedSetupFlow is true.
Verify promo modal displays normally once invite onboarding is completed in subsequent sessions.
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.
@shubham1206agra Relying on #104044 would leave this bug partially unresolved for two key reasons:
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.
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!
Broader invite check: the new check is inviteType !== undefined && !isInviteOnboardingComplete, instead of isSupportedPendingInviteOnboarding. It covers every invite choice.
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.
Include introSelectedMetadata in the loading checks to avoid any pop-in flash before hydration.
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!
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:
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.
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.
@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:
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.
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.
@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! 🚀
@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!
Activity
MelvinBot commentedon Oct 6, 2026
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 neverfalsefor 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:NVP_INTRO_SELECTEDwith a selector that returnsinviteType !== undefined && !isInviteOnboardingComplete.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, afterOpenReportsetsisInviteOnboardingCompletetotrue.This uses
inviteTypedirectly instead ofisSupportedPendingInviteOnboarding. That covers every invite choice, which addresses the earlier feedback. Ordinary members never haveinviteType, 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 noinviteType, where the window should still show.Open question for
emkhalid: you reported invitees landing with{choice: 'newDotLookingAround'}and noinviteType. On the client, that state looks the same as a regular workspace member. Can you share thenvp_introSelectedandnvp_onboardingvalues from your repro, right after Get started? IfinviteTypereally is missing, the App can't detect the invitee safely. The backend would need to sendinviteTypeorhasCompletedGuidedSetupFlow: falsefor 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_POLICYis true for anyone in a group workspace, andisInviteOnboardingCompleteis never set for most members. The window would stay hidden for nearly every workspace member, permanently.choice === 'newDotLookingAround'plushasNonPersonalPolicy: I rejected this too. Users who pick that choice in normal onboarding and then join a workspace would never see announcements again.Code references
false:ProductMarketingWindowManager.tsx:62-85selectors/Onboarding.ts:18-29inviteTypeexists:selectors/Onboarding.ts:72-74useOnboardingFlow.ts:117isInviteOnboardingCompleteis set totrueonly inOpenReportoptimistic data:Report/index.ts:1674-1679Confidence: 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.
melvin-bot commentedon Oct 6, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @shubham1206agra (
External)[-]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)[/+]melvin-bot commentedon Oct 6, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022107586630303041825
camesenin commentedon Oct 6, 2026
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?
In NewDot, there are two distinct onboarding tracks:
ONYXKEYS.NVP_ONBOARDING. It begins withhasCompletedGuidedSetupFlow === falseand presents theOnboardingModalNavigator.startOnboardingFlow()andOnboardingModalNavigator(as seen inuseOnboardingFlow.ts:109-112). Their onboarding lifecycle is tracked viaONYXKEYS.NVP_INTRO_SELECTED(inviteType: CONST.ONBOARDING_INVITE_TYPES.WORKSPACEwithisInviteOnboardingComplete: false). For these invited users,NVP_ONBOARDINGis either empty or does not havehasCompletedGuidedSetupFlow === false(hasCompletedGuidedSetupFlowSelectordefaults totrue).Both promotional surfaces rely exclusively on
hasCompletedGuidedSetupFlow === falseto detect active onboarding and latch session suppression:src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:72-86:src/hooks/useAIFeaturesPromoModal.ts:64-69, 82:Because
hasCompletedGuidedSetupFlowis notfalsefor invited members, neither surface ever detects active onboarding nor sets their session suppression latches (accountIDsWithObservedActiveOnboardingandobservedActiveOnboardingThisSession). 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?
Add
isPendingInviteOnboardingSelectorinsrc/selectors/Onboarding.ts:Export
isPendingInviteOnboardingSelector.Update
src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:ONYXKEYS.NVP_INTRO_SELECTED:introSelectedMetadatainisLoadingOnboardingContextandisLoading:shouldRecordActiveOnboardingandshouldSuppressForOnboardingSession:Update
src/hooks/useAIFeaturesPromoModal.ts:ONYXKEYS.NVP_INTRO_SELECTED:introSelectedMetadatainisAllOnyxLoaded:observedActiveOnboardingThisSessionwhen either regular onboarding or invite onboarding is pending:isEligiblewith!isActivelyOnboarding:Tests:
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 afterisInviteOnboardingCompleteflips totrue.tests/unit/hooks/useAIFeaturesPromoModal.test.ts:Add unit test asserting that when
isPendingInviteOnboardingistrue,useAIFeaturesPromoModaldoes not schedule or navigate toAI_FEATURES_PROMO_MODAL.What alternative solutions did you explore? (Optional)
hasCompletedGuidedSetupFlow: falseon the backend for invite-created accounts: Rejected becauseuseOnboardingFlowandOnboardingGuardwould redirect the user into the standard NewDot company-size / purpose onboarding modal, which invited users intentionally bypass.isSupportedPendingInviteOnboardingfromOnboardingUtils: Rejected becauseisSupportedPendingInviteOnboardingstrictly requireschoiceto matchADMIN,SUBMIT, orCHAT_SPLIT. UsingBoolean(introSelected?.inviteType && !introSelected?.isInviteOnboardingComplete)is universal and guarantees suppression across all workspace invite variants regardless of pre-assigned choice state.camesenin commentedon Oct 6, 2026
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?
In NewDot, there are two distinct onboarding tracks:
ONYXKEYS.NVP_ONBOARDING. It begins withhasCompletedGuidedSetupFlow === falseand presents theOnboardingModalNavigator.startOnboardingFlow()andOnboardingModalNavigator(as seen inuseOnboardingFlow.ts:109-112viaisInvitedOrGroupMember = (hasNonPersonalPolicy || wasInvitedToNewDot) ?? false). For invited users,hasCompletedGuidedSetupFlowSelectordefaults totruebecause they skip the guided wizard.Both promotional surfaces rely exclusively on
hasCompletedGuidedSetupFlow === falseto detect active onboarding and latch session suppression:src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:72-86:src/hooks/useAIFeaturesPromoModal.ts:64-69, 82:Because
hasCompletedGuidedSetupFlowis notfalsefor invited members, neither surface ever detects active onboarding nor sets their session suppression latches (accountIDsWithObservedActiveOnboardingandobservedActiveOnboardingThisSession). 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?
Add Canonical Invited/Group Member Guard in
src/selectors/Onboarding.ts:Mirror NewDot's canonical check from
useOnboardingFlow.ts:109:This handles both users with
introSelected.inviteTypepopulated and invited members who land withhasNonPersonalPolicy === true(e.g. underchoice: 'newDotLookingAround').Update
src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:ONYXKEYS.NVP_INTRO_SELECTEDandONYXKEYS.HAS_NON_PERSONAL_POLICY:isLoadingOnboardingContextandisLoading:shouldRecordActiveOnboardingandshouldSuppressForOnboardingSession:Update
src/hooks/useAIFeaturesPromoModal.ts:ONYXKEYS.NVP_INTRO_SELECTEDandONYXKEYS.HAS_NON_PERSONAL_POLICY:isAllOnyxLoaded.observedActiveOnboardingThisSessionwhen either regular onboarding or invite onboarding is active:isEligiblewith!isActivelyOnboarding.Tests:
tests/unit/ProductMarketingWindowTest.tsx:Assert that invited members (with
{inviteType: 'workspace'}OR withhasNonPersonalPolicy: true) suppress the marketing window, and remain suppressed throughout the session.tests/unit/hooks/useAIFeaturesPromoModal.test.ts:Assert that when
isPendingInviteistrue,useAIFeaturesPromoModaldoes not schedule or displayAI_FEATURES_PROMO_MODAL.What alternative solutions did you explore? (Optional)
hasCompletedGuidedSetupFlow: falseon the backend for invite-created accounts: Rejected becauseuseOnboardingFlowandOnboardingGuardwould redirect the user into the standard NewDot company-size / purpose onboarding modal, which invited users intentionally bypass.introSelected.inviteType === 'workspace': Rejected because in production runtime, newly invited members can land underchoice: 'newDotLookingAround'whereinviteTypeis unpopulated, buthasNonPersonalPolicyis present. MirroringuseOnboardingFlow.ts:109covers both representations robustly.melvin-bot commentedon Oct 6, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
emkhalid commentedon Oct 7, 2026
@shubham1206agra I investigated and reproduced this locally, and wanted to share a quick finding regarding Melvin's proposal above:
introSelected.inviteType === 'workspace'. However, in real runtime, newly invited members often land withintroSelected: {choice: 'newDotLookingAround'}whereinviteTypeis completelyundefined. Under Melvin's proposed check, it evaluates tofalseand the popups still appear.hasCompletedGuidedSetupFlow === falseto latch session suppression. Since invited members bypass the NewDot onboarding wizard, their account starts withhasCompletedGuidedSetupFlow: true, so suppression never triggers.Demos
popup-showing-repro.mov
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!
camesenin commentedon Oct 7, 2026
@shubham1206agra Great observation by @emkhalid on Melvin's proposal regarding invited members who land with
choice: 'newDotLookingAround'whereinviteTypeis 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:By subscribing to
ONYXKEYS.HAS_NON_PERSONAL_POLICYalongsideONYXKEYS.NVP_INTRO_SELECTED, both promotional surfaces (ProductMarketingWindowManageranduseAIFeaturesPromoModal) reliably suppress marketing windows for all invited workspace members regardless of whetherinviteTypeis explicitly populated or they land undernewDotLookingAround.The proposal above has been updated with this complete implementation and test coverage.
yuvrajangadsingh commentedon Oct 7, 2026
withdrawing this, it went up before the issue had the
Help Wantedlabel. will come back if the label is added.jloa-dev commentedon Oct 8, 2026
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?
src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:63-76andsrc/hooks/useAIFeaturesPromoModal.ts:64-69, promo displays are gated solely by checking ifhasCompletedGuidedSetupFlow === falseonONYXKEYS.NVP_ONBOARDING.OnboardingGuard.tsanduseOnboardingFlow.tsroute them through the workspace invite onboarding flow (isSupportedPendingInviteOnboardingviaONYXKEYS.NVP_INTRO_SELECTED).hasCompletedGuidedSetupFlowis already treated astrue(or unset), so both promotional managers evaluateisOnboarding = falseon 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?
ProductMarketingWindowManager.tsx:Subscribe to
ONYXKEYS.NVP_INTRO_SELECTEDusing selectorisSupportedPendingInviteOnboarding:shouldRecordActiveOnboardingandshouldSuppressForOnboardingSessionto rely onisOnboardingInProgress.useAIFeaturesPromoModal.ts:Apply the same
isOnboardingInProgresscheck before scheduling or triggering the AI promo modal.What specific scenarios should we cover in automated tests?
In
tests/unit/ProductMarketingWindowTest.tsx:ProductMarketingWindowdoes not render when user hasisPendingInviteOnboarding: true, even ifhasCompletedGuidedSetupFlowistrue.3 remaining items
melvin-bot commentedon Oct 13, 2026
@shubham1206agra Still overdue 6 days?! Let's take care of this!
melvin-bot commentedon Oct 15, 2026
@shubham1206agra Now this issue is 8 days overdue. Are you sure this should be a Daily? Feel free to change it!
shubham1206agra commentedon Oct 18, 2026
@MelvinBot This issue should be solved with #104044, right?
MelvinBot commentedon Oct 18, 2026
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:
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
hasCompletedGuidedSetupFlow === false, and invitees never set that value:ProductMarketingWindowManager.tsx:72-85ProductMarketingWindowUtils.ts:50view run
emkhalid commentedon Oct 19, 2026
@shubham1206agra Relying on #104044 would leave this bug partially unresolved for two key reasons:
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.tsxto 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!
shubham1206agra commentedon Oct 19, 2026
@MelvinBot Update your proposal based on #103244 (comment), #103244 (comment), #103244 (comment)
MelvinBot commentedon Oct 19, 2026
I updated my proposal. Here's what changed:
ProductMarketingWindowManager.tsx.inviteType !== undefined && !isInviteOnboardingComplete, instead ofisSupportedPendingInviteOnboarding. It covers every invite choice.hasNonPersonalPolicy: that check would hide the window for nearly every workspace member.One thing still needs an answer.
emkhalid, can you share thenvp_introSelectedandnvp_onboardingvalues from your repro? If invitees really arrive with noinviteType, the App can't tell them apart from regular members. That part would need a backend change.view run
shubham1206agra commentedon Oct 19, 2026
@emkhalid Please answer #103244 (comment)
emkhalid commentedon Oct 19, 2026
@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
inviteTypeis 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:introSelectedMetadatain the loading checks to avoid any pop-in flash before hydration.currentAccountIDin the existing session latch (accountIDsWithObservedActiveOnboarding) whenisActivelyOnboardingis observed, ensuring announcements stay suppressed for the entire session.Key Advantages:
inviteTypeand users provisioned undernewDotLookingAround.accountExistsis 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!
shubham1206agra commentedon Oct 19, 2026
@MelvinBot Read #103244 (comment)
MelvinBot commentedon Oct 19, 2026
Thanks,
emkhalid. Your values show that invitees can arrive withoutinviteType, so myinviteTypecheck alone won't fix this. I don't recommendaccount.accountExists === falseas the replacement, though, for two reasons:accountExistsonly 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 clearACCOUNT. If it staysfalse, the window would stay hidden on that device until the user signs out, not just during onboarding.falsefor every invitee. I also can't confirm it's ever refreshed totrue. 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 getinviteTypeinnvp_introSelected, and then the App fix in my proposal works as written. IfaccountExiststurns out to be refreshed after sign-in,emkhalid's approach becomes safe. Either way, this needs backend confirmation, not just App code.Code references
accountExistsis used only inSignInPage:SignInPage.tsx:140-143Account.ts:120-122ACCOUNTis cleared byclearSignInData, which only sign-in and error pages call:Session/index.ts:1176-1182view run
emkhalid commentedon Oct 19, 2026
@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
SigninUserWithLinkandOpenApp:{ "key": "nvp_introSelected", "value": { "choice": "newDotSubmit", "inviteType": "workspace", "isInviteOnboardingComplete": false } }, { "key": "nvp_onboarding", "value": {} }Root Cause
nvp_onboardingas{}for invited workspace members.src/selectors/Onboarding.ts,hasCompletedGuidedSetupFlowSelector({})defaults totrue.App/src/selectors/Onboarding.ts
Lines 22 to 29 in 2875826
ProductMarketingWindowManager.tsx:72-85, the active onboarding check strictly relies onhasCompletedGuidedSetupFlow === false. Because it evaluates totrue, the component assumes onboarding is finished and immediately shows the marketing window.App/src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx
Lines 72 to 85 in 2875826
Full Solution (100% Frontend)
src/selectors/Onboarding.ts, export a selector to detect pending invite onboarding:App/src/selectors/Onboarding.ts
Lines 102 to 110 in 2875826
src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx:Subscribe to
ONYXKEYS.NVP_INTRO_SELECTEDusingisPendingInviteOnboardingSelector:App/src/components/ProductMarketingWindow/ProductMarketingWindowManager.tsx
Lines 62 to 85 in 2875826
Define active onboarding:
Include
introSelectedMetadatainisLoadingOnboardingContextandisLoadingto prevent any flash before hydration.Update
shouldRecordActiveOnboardingandshouldSuppressForOnboardingSessionto useisActivelyOnboarding:How It Works:
isPendingInviteOnboardingevaluates totrue. This addscurrentAccountIDtoaccountIDsWithObservedActiveOnboardingand hides the window.OpenReportexecutes and setsisInviteOnboardingComplete: true, the session latch (accountIDsWithObservedActiveOnboarding.has(currentAccountID)) keeps the window suppressed for the remainder of their session.isInviteOnboardingComplete: true),isPendingInviteOnboardingisfalse, so marketing announcements display normally.I have verified this locally and it resolves the issue cleanly with zero backend changes or dependencies.
emkhalid commentedon Oct 19, 2026
@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! 🚀
melvin-bot commentedon Oct 20, 2026
@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!