From 0df2915b6d5a3bca023fae517e1d39b1f5270375 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 16 Jan 2026 18:40:03 +0700 Subject: [PATCH 1/5] refactor completeOnboarding to use introSelected from onyx --- src/libs/ReportActionsUtils.ts | 2 +- src/libs/actions/IOU/index.ts | 1 + src/libs/actions/Report.ts | 2 ++ .../BaseOnboardingInterestedFeatures.tsx | 1 + .../BaseOnboardingPersonalDetails.tsx | 4 +++- src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx | 2 ++ .../BaseOnboardingWorkspaceInvite.tsx | 3 +++ .../BaseOnboardingWorkspaceOptional.tsx | 3 +++ src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx | 4 +++- tests/actions/ReportTest.ts | 2 ++ 10 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index d27f09edce994..92bad71a29e40 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -7,7 +7,6 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import usePrevious from '@hooks/usePrevious'; -import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import type {TranslationPaths} from '@src/languages/types'; @@ -43,6 +42,7 @@ import getReportURLForCurrentContext from './Navigation/helpers/getReportURLForC import Parser from './Parser'; import {arePersonalDetailsMissing, getEffectiveDisplayName, getPersonalDetailByEmail, getPersonalDetailsByIDs} from './PersonalDetailsUtils'; import {getPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils} from './PolicyUtils'; +import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from './ReportUtils'; import type {getReportName, OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; import {getReportFieldTypeTranslationKey} from './WorkspaceReportFieldUtils'; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 086c8b635334d..26ccba4e3496b 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -11419,6 +11419,7 @@ function completePaymentOnboarding( wasInvited: true, shouldSkipTestDriveModal: true, companySize: introSelected?.companySize as OnboardingCompanySize, + introSelected, }); } function payMoneyRequest( diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index c3cca5397a335..4b32c12f25c2e 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -4384,6 +4384,7 @@ type CompleteOnboardingProps = { isInvitedAccountant?: boolean; onboardingPurposeSelected?: OnboardingPurpose; shouldWaitForRHPVariantInitialization?: boolean; + introSelected: OnyxEntry; }; async function completeOnboarding({ @@ -4402,6 +4403,7 @@ async function completeOnboarding({ isInvitedAccountant, onboardingPurposeSelected, shouldWaitForRHPVariantInitialization = false, + introSelected, }: CompleteOnboardingProps) { const onboardingData = prepareOnboardingOnyxData({ introSelected, diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx index 49121eef6597d..de7cc803db5ce 100644 --- a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -210,6 +210,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin selectedInterestedFeatures: featuresMap.filter((feature) => feature.enabled).map((feature) => feature.id), shouldSkipTestDriveModal: !!policyID && !adminsChatReportID, shouldWaitForRHPVariantInitialization: isSidePanelReportSupported, + introSelected, }); // Avoid creating new WS because onboardingPolicyID is cleared before unmounting diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index 4b6d5718671e1..ac94e3abb6251 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -38,6 +38,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID, {canBeMissing: true}); const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, {canBeMissing: true}); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {canBeMissing: true}); const [conciergeChatReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true}); @@ -78,6 +79,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat adminsChatReportID: onboardingAdminsChatReportID, onboardingPolicyID, shouldSkipTestDriveModal: !!onboardingPolicyID && !mergedAccountConciergeReportID, + introSelected, }); setOnboardingAdminsChatReportID(); @@ -85,7 +87,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat navigateAfterOnboardingWithMicrotaskQueue(isSmallScreenWidth, isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), onboardingPolicyID, mergedAccountConciergeReportID); }, - [onboardingPurposeSelected, onboardingAdminsChatReportID, onboardingMessages, onboardingPolicyID, isBetaEnabled, isSmallScreenWidth, mergedAccountConciergeReportID], + [onboardingPurposeSelected, onboardingAdminsChatReportID, onboardingMessages, onboardingPolicyID, isBetaEnabled, isSmallScreenWidth, mergedAccountConciergeReportID, introSelected], ); const handleSubmit = useCallback( diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index 18fcb3f9f670d..690cbc0d5f081 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -66,6 +66,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, ro const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, {canBeMissing: true}); const [personalDetailsForm] = useOnyx(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM, {canBeMissing: true}); const [onboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE, {canBeMissing: true}); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const paddingHorizontal = onboardingIsMediumOrLargerScreenWidth ? styles.ph8 : styles.ph5; const [customChoices = getEmptyArray()] = useOnyx(ONYXKEYS.ONBOARDING_CUSTOM_CHOICES, {canBeMissing: true}); @@ -105,6 +106,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, ro adminsChatReportID: onboardingAdminsChatReportID ?? undefined, onboardingPolicyID, companySize: onboardingCompanySize, + introSelected, }); // eslint-disable-next-line @typescript-eslint/no-deprecated diff --git a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx index f247ef6fa75da..4f52cbe4eb6df 100644 --- a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx +++ b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx @@ -45,6 +45,7 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID, {canBeMissing: true}); const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, {canBeMissing: true}); const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, {canBeMissing: true}); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const policy = usePolicy(onboardingPolicyID); const {onboardingMessages} = useOnboardingMessages(); // We need to use isSmallScreenWidth, see navigateAfterOnboarding function comment @@ -141,6 +142,7 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo shouldSkipTestDriveModal: !!onboardingPolicyID && !onboardingAdminsChatReportID, isInvitedAccountant, onboardingPurposeSelected, + introSelected, }); setOnboardingAdminsChatReportID(); @@ -166,6 +168,7 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo isSmallScreenWidth, isBetaEnabled, session?.email, + introSelected, ], ); diff --git a/src/pages/OnboardingWorkspaceOptional/BaseOnboardingWorkspaceOptional.tsx b/src/pages/OnboardingWorkspaceOptional/BaseOnboardingWorkspaceOptional.tsx index d2ba9b3a78cf7..ad016de66e483 100644 --- a/src/pages/OnboardingWorkspaceOptional/BaseOnboardingWorkspaceOptional.tsx +++ b/src/pages/OnboardingWorkspaceOptional/BaseOnboardingWorkspaceOptional.tsx @@ -39,6 +39,7 @@ function BaseOnboardingWorkspaceOptional({shouldUseNativeStyles}: BaseOnboarding const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID, {canBeMissing: true}); const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, {canBeMissing: true}); const [conciergeChatReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true}); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const {onboardingMessages} = useOnboardingMessages(); const {isRestrictedPolicyCreation} = usePreferredPolicy(); // When we merge public email with work email, we now want to navigate to the @@ -86,6 +87,7 @@ function BaseOnboardingWorkspaceOptional({shouldUseNativeStyles}: BaseOnboarding adminsChatReportID: onboardingAdminsChatReportID, onboardingPolicyID, shouldSkipTestDriveModal: !!onboardingPolicyID && !onboardingAdminsChatReportID, + introSelected, }); setOnboardingAdminsChatReportID(); @@ -102,6 +104,7 @@ function BaseOnboardingWorkspaceOptional({shouldUseNativeStyles}: BaseOnboarding isSmallScreenWidth, isBetaEnabled, mergedAccountConciergeReportID, + introSelected, ]); return ( diff --git a/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx b/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx index 4bbf5c377f080..f6f987ab09417 100644 --- a/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx +++ b/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx @@ -49,6 +49,7 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding const [onboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE, {canBeMissing: true}); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true}); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const isValidated = isCurrentUserValidated(loginList, session?.email); @@ -72,13 +73,14 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding lastName: onboardingPersonalDetails?.lastName ?? '', shouldSkipTestDriveModal: !!(policy.automaticJoiningEnabled ? policy.policyID : undefined), companySize: onboardingCompanySize, + introSelected, }); setOnboardingAdminsChatReportID(); setOnboardingPolicyID(policy.policyID); navigateAfterOnboardingWithMicrotaskQueue(isSmallScreenWidth, isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), policy.automaticJoiningEnabled ? policy.policyID : undefined); }, - [onboardingMessages, onboardingPersonalDetails?.firstName, onboardingPersonalDetails?.lastName, isSmallScreenWidth, isBetaEnabled, onboardingCompanySize], + [onboardingMessages, onboardingPersonalDetails?.firstName, onboardingPersonalDetails?.lastName, isSmallScreenWidth, isBetaEnabled, onboardingCompanySize, introSelected], ); const policyIDItems = useMemo(() => { diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 690c2977ec05f..6c366dc6be6f9 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -2045,6 +2045,7 @@ describe('actions/Report', () => { onboardingPolicyID, companySize: CONST.ONBOARDING_COMPANY_SIZE.MICRO, userReportedIntegration: null, + introSelected: {choice: engagementChoice}, }); await waitForBatchedUpdates(); @@ -3112,6 +3113,7 @@ describe('actions/Report', () => { onboardingPolicyID, companySize: CONST.ONBOARDING_COMPANY_SIZE.MICRO, userReportedIntegration: null, + introSelected: {choice: engagementChoice}, }); await waitForBatchedUpdates(); From 6689d5e8044f21c97509bb58a1649f025b0e2082 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 19 Jan 2026 11:29:56 +0700 Subject: [PATCH 2/5] lint fix --- src/libs/ReportActionsUtils.ts | 2 +- src/libs/actions/Report.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 5168a2ef59c2a..624637f50e23a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -7,6 +7,7 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import usePrevious from '@hooks/usePrevious'; +import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import type {TranslationPaths} from '@src/languages/types'; @@ -42,7 +43,6 @@ import getReportURLForCurrentContext from './Navigation/helpers/getReportURLForC import Parser from './Parser'; import {arePersonalDetailsMissing, getEffectiveDisplayName, getPersonalDetailByEmail, getPersonalDetailsByIDs} from './PersonalDetailsUtils'; import {getPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils} from './PolicyUtils'; -import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from './ReportUtils'; import type {getReportName, OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; import {getReportFieldTypeTranslationKey} from './WorkspaceReportFieldUtils'; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 913f7aaec3885..9f73661e93140 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -352,10 +352,10 @@ Onyx.connect({ }, }); -let introSelected: OnyxEntry = {}; +let introSelectedOnyx: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.NVP_INTRO_SELECTED, - callback: (val) => (introSelected = val), + callback: (val) => (introSelectedOnyx = val), }); let allReportDraftComments: Record = {}; @@ -1080,7 +1080,7 @@ function openReport( }); } - const isInviteOnboardingComplete = introSelected?.isInviteOnboardingComplete ?? false; + const isInviteOnboardingComplete = introSelectedOnyx?.isInviteOnboardingComplete ?? false; const isOnboardingCompleted = onboarding?.hasCompletedGuidedSetupFlow ?? false; // Some cases we can have two open report requests with guide setup data because isInviteOnboardingComplete is not updated completely. @@ -1090,8 +1090,8 @@ function openReport( // Prepare guided setup data only when nvp_introSelected is set and onboarding is not completed // OldDot users will never have nvp_introSelected set, so they will not see guided setup messages - if (introSelected && !isOnboardingCompleted && !isInviteOnboardingComplete && !hasOpenReportWithGuidedSetupData) { - const {choice, inviteType} = introSelected; + if (introSelectedOnyx && !isOnboardingCompleted && !isInviteOnboardingComplete && !hasOpenReportWithGuidedSetupData) { + const {choice, inviteType} = introSelectedOnyx; const isInviteIOUorInvoice = inviteType === CONST.ONBOARDING_INVITE_TYPES.IOU || inviteType === CONST.ONBOARDING_INVITE_TYPES.INVOICE; const isInviteChoiceCorrect = choice === CONST.ONBOARDING_CHOICES.ADMIN || choice === CONST.ONBOARDING_CHOICES.SUBMIT || choice === CONST.ONBOARDING_CHOICES.CHAT_SPLIT; @@ -1103,10 +1103,10 @@ function openReport( } const onboardingData = prepareOnboardingOnyxData({ - introSelected, + introSelected: introSelectedOnyx, engagementChoice: choice, onboardingMessage, - companySize: introSelected?.companySize as OnboardingCompanySize, + companySize: introSelectedOnyx?.companySize as OnboardingCompanySize, }); if (onboardingData) { From 0f8297cf37401813e8475490eb0a689a3dc5efeb Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 12 Feb 2026 17:01:48 +0700 Subject: [PATCH 3/5] lint fix --- src/libs/actions/Report/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 4f071cbc9794b..eabb7950b78a6 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4559,7 +4559,7 @@ async function completeOnboarding({ introSelected, }: CompleteOnboardingProps) { const onboardingData = prepareOnboardingOnyxData({ - introSelected: deprecatedIntroSelected, + introSelected, engagementChoice, onboardingMessage, adminsChatReportID, From c49a7bb4b9d79cd35b11dbc17e3d407c022d6269 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 13 Feb 2026 09:31:52 +0700 Subject: [PATCH 4/5] add tests --- tests/actions/IOUTest.ts | 139 ++++++++++++++++++++++++++ tests/ui/OnboardingPurpose.tsx | 175 +++++++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 tests/ui/OnboardingPurpose.tsx diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 55dfdaac81cf6..750a5446b635b 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -18,6 +18,7 @@ import { cancelPayment, canIOUBePaid, canUnapproveIOU, + completePaymentOnboarding, convertBulkTrackedExpensesToIOU, createDistanceRequest, deleteMoneyRequest, @@ -14955,4 +14956,142 @@ describe('actions/IOU', () => { }).not.toThrow(); }); }); + + describe('completePaymentOnboarding', () => { + let completeOnboardingSpy: jest.SpyInstance; + + beforeEach(async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + completeOnboardingSpy = jest.spyOn(require('@libs/actions/Report'), 'completeOnboarding').mockImplementation(jest.fn()); + await Onyx.set(ONYXKEYS.SESSION, {email: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [CARLOS_ACCOUNT_ID]: { + accountID: CARLOS_ACCOUNT_ID, + firstName: 'Carlos', + lastName: 'Test', + }, + }); + await waitForBatchedUpdates(); + }); + + afterEach(() => { + completeOnboardingSpy.mockRestore(); + }); + + it('should not call completeOnboarding when introSelected is undefined', () => { + completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, undefined); + expect(completeOnboardingSpy).not.toHaveBeenCalled(); + }); + + it('should not call completeOnboarding when isInviteOnboardingComplete is true', () => { + completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, { + choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + inviteType: CONST.ONBOARDING_INVITE_TYPES.IOU, + isInviteOnboardingComplete: true, + }); + expect(completeOnboardingSpy).not.toHaveBeenCalled(); + }); + + it('should not call completeOnboarding when choice is missing', () => { + completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, { + inviteType: CONST.ONBOARDING_INVITE_TYPES.IOU, + }); + expect(completeOnboardingSpy).not.toHaveBeenCalled(); + }); + + it('should not call completeOnboarding when inviteType is missing', () => { + completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, { + choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + }); + expect(completeOnboardingSpy).not.toHaveBeenCalled(); + }); + + it('should override purpose to MANAGE_TEAM for IOU invite with BBA payment', () => { + const introSelected: IntroSelected = { + choice: CONST.ONBOARDING_CHOICES.SUBMIT, + inviteType: CONST.ONBOARDING_INVITE_TYPES.IOU, + companySize: CONST.ONBOARDING_COMPANY_SIZE.MICRO, + }; + completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, introSelected); + + expect(completeOnboardingSpy).toHaveBeenCalledWith( + expect.objectContaining({ + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + paymentSelected: CONST.PAYMENT_SELECTED.BBA, + wasInvited: true, + shouldSkipTestDriveModal: true, + companySize: CONST.ONBOARDING_COMPANY_SIZE.MICRO, + introSelected, + }), + ); + }); + + it('should override purpose to CHAT_SPLIT for INVOICE invite with PBA payment', () => { + const introSelected: IntroSelected = { + choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + inviteType: CONST.ONBOARDING_INVITE_TYPES.INVOICE, + companySize: CONST.ONBOARDING_COMPANY_SIZE.SMALL, + }; + completePaymentOnboarding(CONST.PAYMENT_SELECTED.PBA, introSelected); + + expect(completeOnboardingSpy).toHaveBeenCalledWith( + expect.objectContaining({ + engagementChoice: CONST.ONBOARDING_CHOICES.CHAT_SPLIT, + paymentSelected: CONST.PAYMENT_SELECTED.PBA, + wasInvited: true, + shouldSkipTestDriveModal: true, + companySize: CONST.ONBOARDING_COMPANY_SIZE.SMALL, + introSelected, + }), + ); + }); + + it('should keep original purpose for INVOICE invite with BBA payment', () => { + const introSelected: IntroSelected = { + choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + inviteType: CONST.ONBOARDING_INVITE_TYPES.INVOICE, + }; + completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, introSelected); + + expect(completeOnboardingSpy).toHaveBeenCalledWith( + expect.objectContaining({ + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + introSelected, + }), + ); + }); + + it('should keep original purpose for IOU invite with PBA payment', () => { + const introSelected: IntroSelected = { + choice: CONST.ONBOARDING_CHOICES.SUBMIT, + inviteType: CONST.ONBOARDING_INVITE_TYPES.IOU, + }; + completePaymentOnboarding(CONST.PAYMENT_SELECTED.PBA, introSelected); + + expect(completeOnboardingSpy).toHaveBeenCalledWith( + expect.objectContaining({ + engagementChoice: CONST.ONBOARDING_CHOICES.SUBMIT, + introSelected, + }), + ); + }); + + it('should pass introSelected and optional params through to completeOnboarding', () => { + const introSelected: IntroSelected = { + choice: CONST.ONBOARDING_CHOICES.CHAT_SPLIT, + inviteType: CONST.ONBOARDING_INVITE_TYPES.CHAT, + companySize: CONST.ONBOARDING_COMPANY_SIZE.MEDIUM, + }; + completePaymentOnboarding(CONST.PAYMENT_SELECTED.PBA, introSelected, 'adminsChatReport123', 'policyID456'); + + expect(completeOnboardingSpy).toHaveBeenCalledWith( + expect.objectContaining({ + engagementChoice: CONST.ONBOARDING_CHOICES.CHAT_SPLIT, + adminsChatReportID: 'adminsChatReport123', + onboardingPolicyID: 'policyID456', + introSelected, + }), + ); + }); + }); }); diff --git a/tests/ui/OnboardingPurpose.tsx b/tests/ui/OnboardingPurpose.tsx new file mode 100644 index 0000000000000..91db15c8fb8bf --- /dev/null +++ b/tests/ui/OnboardingPurpose.tsx @@ -0,0 +1,175 @@ +import {PortalProvider} from '@gorhom/portal'; +import {NavigationContainer} from '@react-navigation/native'; +import {act, render, screen, userEvent, waitFor} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import ComposeProviders from '@components/ComposeProviders'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; +import * as useResponsiveLayoutModule from '@hooks/useResponsiveLayout'; +import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; +import Navigation from '@libs/Navigation/Navigation'; +import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; +import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types'; +import OnboardingPurpose from '@pages/OnboardingPurpose'; +import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +TestHelper.setupGlobalFetchMock(); + +// Helper to translate onboarding purpose keys that use dynamic CONST values +const translatePurpose = (choice: string) => TestHelper.translateLocal(`onboarding.purpose.${choice}` as TranslationPaths); + +const Stack = createPlatformStackNavigator(); + +const navigate = jest.spyOn(Navigation, 'navigate'); + +const renderOnboardingPurposePage = (initialRouteName: typeof SCREENS.ONBOARDING.PURPOSE, initialParams: OnboardingModalNavigatorParamList[typeof SCREENS.ONBOARDING.PURPOSE]) => { + return render( + + + + + + + + + , + ); +}; + +describe('OnboardingPurpose Page', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({ + isSmallScreenWidth: false, + shouldUseNarrowLayout: false, + } as ResponsiveLayoutResult); + }); + + afterEach(async () => { + await act(async () => { + await Onyx.clear(); + }); + jest.clearAllMocks(); + }); + + it('should navigate to personal details page when user selects a purpose and is from public domain', async () => { + await TestHelper.signInWithTestUser(); + + await act(async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, { + isFromPublicDomain: true, + hasAccessibleDomainPolicies: false, + }); + }); + + const {unmount} = renderOnboardingPurposePage(SCREENS.ONBOARDING.PURPOSE, {backTo: ''}); + + await waitForBatchedUpdatesWithAct(); + + const user = userEvent.setup(); + const chatSplitLabel = translatePurpose(CONST.ONBOARDING_CHOICES.CHAT_SPLIT); + const chatSplitOption = screen.getByLabelText(chatSplitLabel); + await user.press(chatSplitOption); + + await waitFor(() => { + expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_PERSONAL_DETAILS.getRoute('')); + }); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('should navigate to employees page when user selects MANAGE_TEAM', async () => { + await TestHelper.signInWithTestUser(); + + await act(async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, { + isFromPublicDomain: false, + hasAccessibleDomainPolicies: true, + }); + }); + + const {unmount} = renderOnboardingPurposePage(SCREENS.ONBOARDING.PURPOSE, {backTo: ''}); + + await waitForBatchedUpdatesWithAct(); + + const user = userEvent.setup(); + const manageTeamLabel = translatePurpose(CONST.ONBOARDING_CHOICES.MANAGE_TEAM); + const manageTeamOption = screen.getByLabelText(manageTeamLabel); + await user.press(manageTeamOption); + + await waitFor(() => { + expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_EMPLOYEES.getRoute('')); + }); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('should call completeOnboarding with introSelected when user is from private domain and selects a direct-complete choice', async () => { + const completeOnboardingSpy = jest.spyOn(Report, 'completeOnboarding').mockImplementation(jest.fn()); + + await TestHelper.signInWithTestUser(); + + const introSelectedValue = { + choice: CONST.ONBOARDING_CHOICES.CHAT_SPLIT, + inviteType: CONST.ONBOARDING_INVITE_TYPES.CHAT, + companySize: CONST.ONBOARDING_COMPANY_SIZE.MICRO, + }; + + await act(async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, { + isFromPublicDomain: false, + hasAccessibleDomainPolicies: true, + }); + await Onyx.merge(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM, { + firstName: 'Test', + lastName: 'User', + }); + await Onyx.set(ONYXKEYS.NVP_INTRO_SELECTED, introSelectedValue); + }); + + const {unmount} = renderOnboardingPurposePage(SCREENS.ONBOARDING.PURPOSE, {backTo: ''}); + + await waitForBatchedUpdatesWithAct(); + + // Select CHAT_SPLIT which triggers completeOnboarding directly for private domain users + const user = userEvent.setup(); + const chatSplitLabel = translatePurpose(CONST.ONBOARDING_CHOICES.CHAT_SPLIT); + const chatSplitOption = screen.getByLabelText(chatSplitLabel); + await user.press(chatSplitOption); + + await waitFor(() => { + expect(completeOnboardingSpy).toHaveBeenCalledWith( + expect.objectContaining({ + engagementChoice: CONST.ONBOARDING_CHOICES.CHAT_SPLIT, + firstName: 'Test', + lastName: 'User', + introSelected: introSelectedValue, + }), + ); + }); + + completeOnboardingSpy.mockRestore(); + unmount(); + await waitForBatchedUpdatesWithAct(); + }); +}); From 853622c2897736bc3cad99fa3c553396ccde2062 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 13 Feb 2026 10:22:59 +0700 Subject: [PATCH 5/5] lint fix --- tests/ui/OnboardingPurpose.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/ui/OnboardingPurpose.tsx b/tests/ui/OnboardingPurpose.tsx index 91db15c8fb8bf..610713e635d4d 100644 --- a/tests/ui/OnboardingPurpose.tsx +++ b/tests/ui/OnboardingPurpose.tsx @@ -13,7 +13,7 @@ import Navigation from '@libs/Navigation/Navigation'; import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types'; import OnboardingPurpose from '@pages/OnboardingPurpose'; -import * as Report from '@userActions/Report'; +import {completeOnboarding} from '@userActions/Report'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -22,6 +22,18 @@ import SCREENS from '@src/SCREENS'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; +const mockCompleteOnboarding = jest.mocked(completeOnboarding); + +jest.mock('@userActions/Report', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@userActions/Report'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + completeOnboarding: jest.fn(), + }; +}); + TestHelper.setupGlobalFetchMock(); // Helper to translate onboarding purpose keys that use dynamic CONST values @@ -125,8 +137,6 @@ describe('OnboardingPurpose Page', () => { }); it('should call completeOnboarding with introSelected when user is from private domain and selects a direct-complete choice', async () => { - const completeOnboardingSpy = jest.spyOn(Report, 'completeOnboarding').mockImplementation(jest.fn()); - await TestHelper.signInWithTestUser(); const introSelectedValue = { @@ -158,7 +168,7 @@ describe('OnboardingPurpose Page', () => { await user.press(chatSplitOption); await waitFor(() => { - expect(completeOnboardingSpy).toHaveBeenCalledWith( + expect(mockCompleteOnboarding).toHaveBeenCalledWith( expect.objectContaining({ engagementChoice: CONST.ONBOARDING_CHOICES.CHAT_SPLIT, firstName: 'Test', @@ -168,7 +178,7 @@ describe('OnboardingPurpose Page', () => { ); }); - completeOnboardingSpy.mockRestore(); + mockCompleteOnboarding.mockClear(); unmount(); await waitForBatchedUpdatesWithAct(); });