diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 4949274983be..c7614d23ae54 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -516,6 +516,9 @@ const ONYXKEYS = { /** Onboarding user reported integration selected by the user during Onboarding flow */ ONBOARDING_USER_REPORTED_INTEGRATION: 'onboardingUserReportedIntegration', + /** Onboarding user reported integration name entered when Other is selected */ + ONBOARDING_USER_REPORTED_INTEGRATION_NAME: 'onboardingUserReportedIntegrationName', + /** Onboarding Purpose selected by the user during Onboarding flow */ ONBOARDING_ADMINS_CHAT_REPORT_ID: 'onboardingAdminsChatReportID', @@ -1714,6 +1717,7 @@ type OnyxValuesMapping = { [ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_THREAD_REPORT_IDS]: Record; [ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES]: OnyxTypes.ExportTemplate[]; [ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION]: OnboardingAccounting; + [ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION_NAME]: string; [ONYXKEYS.HYBRID_APP]: OnyxTypes.HybridApp; [ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS]: Record; [ONYXKEYS.NVP_EXPENSE_RULES]: OnyxTypes.ExpenseRule[]; diff --git a/src/languages/de.ts b/src/languages/de.ts index 6b8c6f2eadb6..5b524f21debd 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3148,6 +3148,7 @@ ${amount} für ${merchant} – ${date}`, accounting: { title: 'Verwendest du eine Buchhaltungssoftware?', none: 'Keine', + otherAccountingSoftware: 'Deine Buchhaltungssoftware', }, interestedFeatures: { title: 'An welchen Funktionen bist du interessiert?', diff --git a/src/languages/en.ts b/src/languages/en.ts index 216a3d60c119..de7f9136fb6a 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3231,6 +3231,7 @@ const translations = { accounting: { title: 'Do you use any accounting software?', none: 'None', + otherAccountingSoftware: 'Your accounting software', }, interestedFeatures: { title: 'What features are you interested in?', diff --git a/src/languages/es.ts b/src/languages/es.ts index 9bf1359733ee..58c6e6659e2c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3010,6 +3010,7 @@ ${amount} para ${merchant} - ${date}`, accounting: { title: '¿Utilizas algún software de contabilidad?', none: 'Ninguno', + otherAccountingSoftware: 'Tu software de contabilidad', }, interestedFeatures: { title: '¿Qué funciones te interesan?', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 2f9ae12fa815..30e0c84869d5 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3155,6 +3155,7 @@ ${amount} pour ${merchant} - ${date}`, accounting: { title: 'Utilisez-vous un logiciel de comptabilité ?', none: 'Aucun', + otherAccountingSoftware: 'Votre logiciel de comptabilité', }, interestedFeatures: { title: 'Quelles fonctionnalités vous intéressent ?', diff --git a/src/languages/it.ts b/src/languages/it.ts index 6e49ff0d79c0..93ec80e26b6a 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3140,6 +3140,7 @@ ${amount} per ${merchant} - ${date}`, accounting: { title: 'Usi un software di contabilità?', none: 'Nessuno', + otherAccountingSoftware: 'Il tuo software di contabilità', }, interestedFeatures: { title: 'A quali funzionalità sei interessato?', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2bde735c0fe4..3dbfceab5057 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3109,6 +3109,7 @@ ${date} の ${merchant} への ${amount}`, accounting: { title: '会計ソフトを利用していますか?', none: 'なし', + otherAccountingSoftware: 'ご利用の会計ソフト', }, interestedFeatures: { title: 'どの機能にご興味がありますか?', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 3bf61f9c238e..0fd6e24df059 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3138,6 +3138,7 @@ ${amount} voor ${merchant} - ${date}`, accounting: { title: 'Gebruikt u een boekhoudprogramma?', none: 'Geen', + otherAccountingSoftware: 'Je boekhoudsoftware', }, interestedFeatures: { title: 'In welke functies ben je geïnteresseerd?', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index b4c34c53be25..38f76094da59 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3124,6 +3124,7 @@ ${amount} dla ${merchant} - ${date}`, accounting: { title: 'Czy korzystasz z jakiegoś oprogramowania księgowego?', none: 'Brak', + otherAccountingSoftware: 'Twoje oprogramowanie księgowe', }, interestedFeatures: { title: 'Jakie funkcje Cię interesują?', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 429816f1f138..19699af23685 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3127,6 +3127,7 @@ ${amount} para ${merchant} - ${date}`, accounting: { title: 'Você usa algum software de contabilidade?', none: 'Nenhum', + otherAccountingSoftware: 'Seu software de contabilidade', }, interestedFeatures: { title: 'Em quais recursos você tem interesse?', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 286066528d33..7a6b9c1c219e 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3039,6 +3039,7 @@ ${amount},商户:${merchant} - 日期:${date}`, accounting: { title: '你是否使用任何会计软件?', none: '无', + otherAccountingSoftware: '你的会计软件', }, interestedFeatures: { title: '你对哪些功能感兴趣?', diff --git a/src/libs/API/parameters/CompleteGuidedSetupParams.ts b/src/libs/API/parameters/CompleteGuidedSetupParams.ts index 248abbefa982..ba78a9689fb6 100644 --- a/src/libs/API/parameters/CompleteGuidedSetupParams.ts +++ b/src/libs/API/parameters/CompleteGuidedSetupParams.ts @@ -12,6 +12,7 @@ type CompleteGuidedSetupParams = { paymentSelected?: string; companySize?: OnboardingCompanySize; userReportedIntegration?: OnboardingAccounting; + userReportedIntegrationName?: string; policyID?: string; selfDMReportID?: string; selfDMCreatedReportActionID?: string; diff --git a/src/libs/API/parameters/CreateWorkspaceParams.ts b/src/libs/API/parameters/CreateWorkspaceParams.ts index 5f778fc61620..d8ef5c72edec 100644 --- a/src/libs/API/parameters/CreateWorkspaceParams.ts +++ b/src/libs/API/parameters/CreateWorkspaceParams.ts @@ -16,6 +16,7 @@ type CreateWorkspaceParams = { file?: File; companySize?: string; userReportedIntegration?: string; + userReportedIntegrationName?: string; memberData?: string; features?: string; shouldAddGuideWelcomeMessage?: boolean; diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts index cf341eacc182..2391803f8a20 100644 --- a/src/libs/ExportOnyxState/common.ts +++ b/src/libs/ExportOnyxState/common.ts @@ -405,6 +405,7 @@ const onyxKeysToMaskFragileData = new Set([ // Same as LAST_VISITED_PATH above, just limited to onboarding routes ONYXKEYS.ONBOARDING_LAST_VISITED_PATH, ONYXKEYS.ONBOARDING_PERSONAL_TRACK_GOAL, + ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION_NAME, ONYXKEYS.ONYX_UPDATES_FROM_SERVER, ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, ONYXKEYS.PENDING_CONTACT_ACTION, diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 8f26550baaf0..7d8ea714f44a 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -251,6 +251,7 @@ type BuildPolicyDataOptions = { shouldAddOnboardingTasks?: boolean; companySize?: OnboardingCompanySize; userReportedIntegration?: OnboardingAccounting; + userReportedIntegrationName?: string; isAnnualSubscription?: boolean; featuresMap?: Array>; lastUsedPaymentMethod?: LastPaymentMethodType; @@ -2617,6 +2618,7 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData(onboardingUserReportedIntegration ?? undefined); + const [userReportedIntegrationName, setUserReportedIntegrationName] = useState(onboardingUserReportedIntegrationName ?? ''); const [error, setError] = useState(''); + const isOtherSelected = userReportedIntegration === 'other'; const paidGroupPolicy = Object.values(allPolicies ?? {}).find((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, session?.email)); // Set onboardingPolicyID and onboardingAdminsChatReportID if a workspace is created by the backend for OD signup @@ -174,7 +178,7 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount additionalStyles={[StyleUtils.getAvatarBorderStyle(CONST.AVATAR_SIZE.DEFAULT, CONST.ICON_TYPE_AVATAR), styles.mr3, styles.onboardingSmallIcon]} /> ), - isSelected: userReportedIntegration === 'other', + isSelected: isOtherSelected, }; const accountingOptions: OnboardingListItem[] = [...integrations.map(createAccountingOption), othersAccountingOption, noneAccountingOption]; @@ -186,6 +190,8 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount } setOnboardingUserReportedIntegration(userReportedIntegration); + const trimmedIntegrationName = userReportedIntegrationName.trim(); + setOnboardingUserReportedIntegrationName(userReportedIntegration === 'other' && trimmedIntegrationName ? trimmedIntegrationName : undefined); // Navigate to the next onboarding step interested features with the selected integration Navigation.navigate(ROUTES.ONBOARDING_INTERESTED_FEATURES.getRoute()); @@ -193,6 +199,9 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount const handleIntegrationSelect = (integrationKey: OnboardingListItem['keyForList']) => { setUserReportedIntegration(integrationKey === 'none' ? null : integrationKey); + if (integrationKey !== 'other') { + setUserReportedIntegrationName(''); + } setError(''); }; @@ -246,8 +255,26 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount {translate('onboarding.accounting.title')} - - {accountingOptions.map(renderOption)} + + {accountingOptions.map(renderOption)} + {isOtherSelected && ( + + { + setUserReportedIntegrationName(text); + setError(''); + }} + /> + + )} {!!error && ( diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx index 40bc170d944f..a4e3def579af 100644 --- a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -62,6 +62,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID); const [onboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE); const [userReportedIntegration] = useOnyx(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION); + const [userReportedIntegrationName] = useOnyx(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION_NAME); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [betas] = useOnyx(ONYXKEYS.BETAS); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); @@ -192,6 +193,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin const shouldCreateWorkspace = !onboardingPolicyID && !paidGroupPolicy; const newUserReportedIntegration = selectedFeatures.some((feature) => feature === CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED) ? userReportedIntegration : undefined; + const newUserReportedIntegrationName = newUserReportedIntegration === 'other' && userReportedIntegrationName?.trim() ? userReportedIntegrationName.trim() : undefined; const featuresMap = features.map((feature) => ({ ...feature, enabled: selectedFeatures.includes(feature.id), @@ -212,6 +214,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin shouldAddOnboardingTasks: false, companySize: onboardingCompanySize, userReportedIntegration: newUserReportedIntegration, + userReportedIntegrationName: newUserReportedIntegrationName, featuresMap, introSelected, activePolicy, @@ -236,6 +239,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin onboardingPolicyID: policyID, companySize: onboardingCompanySize, userReportedIntegration: newUserReportedIntegration, + userReportedIntegrationName: newUserReportedIntegrationName, firstName: currentUserPersonalDetails?.firstName, lastName: currentUserPersonalDetails?.lastName, selectedInterestedFeatures: featuresMap.filter((feature) => feature.enabled).map((feature) => feature.id), @@ -286,6 +290,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin paidGroupPolicy, session?.email, userReportedIntegration, + userReportedIntegrationName, features, selectedFeatures, currentUserPersonalDetails?.firstName, diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 48ebe6d43851..e1481f03ef7f 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -77,6 +77,67 @@ describe('actions/Policy', () => { mockFetch?.resume?.(); }); + it('forwards the user-reported integration name to CreateWorkspace', async () => { + const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID: Policy.generatePolicyID(), + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}, + currentUserAccountIDParam: ESH_ACCOUNT_ID, + currentUserEmailParam: ESH_EMAIL, + currency: CONST.CURRENCY.USD, + userReportedIntegration: 'other', + userReportedIntegrationName: 'Acme Books', + isSelfTourViewed: false, + betas: undefined, + hasActiveAdminPolicies: false, + activePolicy: undefined, + }); + await waitForBatchedUpdates(); + + expect(apiWriteSpy).toHaveBeenCalledWith( + WRITE_COMMANDS.CREATE_WORKSPACE, + expect.objectContaining({ + userReportedIntegration: 'other', + userReportedIntegrationName: 'Acme Books', + }), + expect.anything(), + ); + + apiWriteSpy.mockRestore(); + }); + + it('omits the user-reported integration name from CreateWorkspace when it is undefined', async () => { + Policy.createWorkspace({ + policyOwnerEmail: ESH_EMAIL, + makeMeAdmin: true, + policyName: WORKSPACE_NAME, + policyID: Policy.generatePolicyID(), + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}, + currentUserAccountIDParam: ESH_ACCOUNT_ID, + currentUserEmailParam: ESH_EMAIL, + currency: CONST.CURRENCY.USD, + userReportedIntegration: 'other', + isSelfTourViewed: false, + betas: undefined, + hasActiveAdminPolicies: false, + activePolicy: undefined, + }); + await waitForBatchedUpdates(); + + const calls = TestHelper.getFetchMockCalls(WRITE_COMMANDS.CREATE_WORKSPACE); + expect(calls.length).toBeGreaterThan(0); + const body = calls.at(-1)?.[1]?.body; + expect(body).toBeInstanceOf(FormData); + const formEntries = body instanceof FormData ? Object.fromEntries(body) : {}; + expect(formEntries.userReportedIntegrationName).toBeUndefined(); + }); + it('creates a new workspace', async () => { (fetch as MockFetch)?.pause?.(); await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index b8bdf6016a06..b3d81742ebfc 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -2923,7 +2923,6 @@ describe('actions/Report', () => { it('should forward selectedInterestedFeatures to the CompleteGuidedSetup API call as a JSON-encoded array', async () => { await Onyx.set(ONYXKEYS.SESSION, {email: TEST_USER_LOGIN, accountID: TEST_USER_ACCOUNT_ID}); - (global.fetch as jest.Mock).mockClear(); await waitForBatchedUpdates(); const engagementChoice = CONST.INTRO_CHOICES.MANAGE_TEAM; @@ -2948,10 +2947,68 @@ describe('actions/Report', () => { expect(calls.length).toBeGreaterThan(0); const body = calls.at(-1)?.[1]?.body; expect(body).toBeInstanceOf(FormData); - const formEntries = Object.fromEntries(body as FormData); + const formEntries = body instanceof FormData ? Object.fromEntries(body) : {}; expect(formEntries.selectedInterestedFeatures).toBe(JSON.stringify(selectedInterestedFeatures)); }); + it('should forward the user-reported integration name to CompleteGuidedSetup', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: TEST_USER_LOGIN, accountID: TEST_USER_ACCOUNT_ID}); + await waitForBatchedUpdates(); + + const engagementChoice = CONST.INTRO_CHOICES.MANAGE_TEAM; + const {onboardingMessages} = getOnboardingMessages(); + + Report.completeOnboarding({ + engagementChoice, + onboardingMessage: onboardingMessages[engagementChoice], + adminsChatReportID: '7957055873634069', + onboardingPolicyID: 'A70D00C752416809', + companySize: CONST.ONBOARDING_COMPANY_SIZE.MICRO, + userReportedIntegration: 'other', + userReportedIntegrationName: 'Acme Books', + introSelected: {choice: engagementChoice}, + isSelfTourViewed: false, + }); + + await waitForBatchedUpdates(); + + const calls = TestHelper.getFetchMockCalls(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP); + expect(calls.length).toBeGreaterThan(0); + const body = calls.at(-1)?.[1]?.body; + expect(body).toBeInstanceOf(FormData); + const formEntries = body instanceof FormData ? Object.fromEntries(body) : {}; + expect(formEntries.userReportedIntegration).toBe('other'); + expect(formEntries.userReportedIntegrationName).toBe('Acme Books'); + }); + + it('should omit the user-reported integration name from CompleteGuidedSetup when it is undefined', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: TEST_USER_LOGIN, accountID: TEST_USER_ACCOUNT_ID}); + await waitForBatchedUpdates(); + + const engagementChoice = CONST.INTRO_CHOICES.MANAGE_TEAM; + const {onboardingMessages} = getOnboardingMessages(); + + Report.completeOnboarding({ + engagementChoice, + onboardingMessage: onboardingMessages[engagementChoice], + adminsChatReportID: '7957055873634069', + onboardingPolicyID: 'A70D00C752416809', + companySize: CONST.ONBOARDING_COMPANY_SIZE.MICRO, + userReportedIntegration: 'other', + introSelected: {choice: engagementChoice}, + isSelfTourViewed: false, + }); + + await waitForBatchedUpdates(); + + const calls = TestHelper.getFetchMockCalls(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP); + expect(calls.length).toBeGreaterThan(0); + const body = calls.at(-1)?.[1]?.body; + expect(body).toBeInstanceOf(FormData); + const formEntries = body instanceof FormData ? Object.fromEntries(body) : {}; + expect(formEntries.userReportedIntegrationName).toBeUndefined(); + }); + it('should post onboarding tasks to the existing Concierge chat', async () => { await Onyx.set(ONYXKEYS.SESSION, {email: TEST_USER_LOGIN, accountID: TEST_USER_ACCOUNT_ID}); await waitForBatchedUpdates(); diff --git a/tests/ui/OnboardingAccounting.tsx b/tests/ui/OnboardingAccounting.tsx new file mode 100644 index 000000000000..123cf38133b8 --- /dev/null +++ b/tests/ui/OnboardingAccounting.tsx @@ -0,0 +1,247 @@ +import {act, render, screen, userEvent, waitFor} from '@testing-library/react-native'; + +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 OnboardingAccounting from '@pages/OnboardingAccounting'; +import OnboardingInterestedFeatures from '@pages/OnboardingInterestedFeatures'; + +import {createWorkspace} from '@userActions/Policy/Policy'; +import {completeOnboarding} from '@userActions/Report'; + +import type {OnboardingAccounting as OnboardingAccountingValue} from '@src/CONST'; +import CONST from '@src/CONST'; +import IntlStore from '@src/languages/IntlStore'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; + +import {PortalProvider} from '@gorhom/portal'; +import {NavigationContainer} from '@react-navigation/native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; + +import getOnyxValue from '../utils/getOnyxValue'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +const mockCreateWorkspace = jest.mocked(createWorkspace); +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(), + }; +}); + +jest.mock('@userActions/Policy/Policy', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const actual = jest.requireActual('@userActions/Policy/Policy'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...actual, + createWorkspace: jest.fn().mockReturnValue({ + policyID: 'test-policy-id', + adminsChatReportID: 'test-admins-report-id', + }), + }; +}); + +jest.mock('@libs/navigateAfterOnboarding', () => ({ + navigateAfterOnboardingWithMicrotaskQueue: jest.fn(), +})); + +jest.mock('@libs/Navigation/TransitionTracker', () => ({ + __esModule: true, + default: { + runAfterTransitions: jest.fn(), + }, +})); + +TestHelper.setupGlobalFetchMock(); + +const Stack = createPlatformStackNavigator(); +const navigate = jest.spyOn(Navigation, 'navigate'); + +function renderOnboardingPage(screenName: typeof SCREENS.ONBOARDING.ACCOUNTING | typeof SCREENS.ONBOARDING.INTERESTED_FEATURES) { + return render( + + + + + {screenName === SCREENS.ONBOARDING.ACCOUNTING ? ( + + ) : ( + + )} + + + + , + ); +} + +async function setInterestedFeaturesOnyxState(integration: OnboardingAccountingValue | null, integrationName?: string) { + await TestHelper.signInWithTestUser(); + await act(async () => { + await Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.MANAGE_TEAM); + await Onyx.set(ONYXKEYS.ONBOARDING_COMPANY_SIZE, CONST.ONBOARDING_COMPANY_SIZE.MICRO); + await Onyx.set(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION, integration); + await Onyx.set(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION_NAME, integrationName ?? null); + await Onyx.set(ONYXKEYS.NVP_INTRO_SELECTED, {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM}); + }); +} + +describe('Onboarding accounting software name', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + return IntlStore.load(CONST.LOCALES.EN); + }); + + beforeEach(() => { + jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({ + isSmallScreenWidth: false, + shouldUseNarrowLayout: false, + isInNarrowPaneModal: false, + isExtraSmallScreenHeight: false, + isMediumScreenWidth: false, + isLargeScreenWidth: false, + isExtraLargeScreenWidth: false, + isExtraSmallScreenWidth: false, + isSmallScreen: false, + onboardingIsMediumOrLargerScreenWidth: false, + isInLandscapeMode: false, + } satisfies ResponsiveLayoutResult); + }); + + afterEach(async () => { + await act(async () => { + await Onyx.clear(); + }); + jest.clearAllMocks(); + }); + + it('shows the input for Other and saves a trimmed name separately from the integration value', async () => { + await TestHelper.signInWithTestUser(); + const {unmount} = renderOnboardingPage(SCREENS.ONBOARDING.ACCOUNTING); + await waitForBatchedUpdatesWithAct(); + + const user = userEvent.setup(); + await user.press(screen.getByText(TestHelper.translateLocal('workspace.accounting.other'))); + + const inputLabel = TestHelper.translateLocal('onboarding.accounting.otherAccountingSoftware'); + const input = await screen.findByLabelText(inputLabel); + await user.type(input, ' Acme Books '); + await user.press(screen.getByText(TestHelper.translateLocal('common.continue'))); + + await waitFor(() => { + expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_INTERESTED_FEATURES.getRoute()); + }); + expect(await getOnyxValue(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION)).toBe('other'); + expect(await getOnyxValue(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION_NAME)).toBe('Acme Books'); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('clears the custom name when a supported integration is selected', async () => { + await TestHelper.signInWithTestUser(); + const {unmount} = renderOnboardingPage(SCREENS.ONBOARDING.ACCOUNTING); + await waitForBatchedUpdatesWithAct(); + + const user = userEvent.setup(); + await user.press(screen.getByText(TestHelper.translateLocal('workspace.accounting.other'))); + const inputLabel = TestHelper.translateLocal('onboarding.accounting.otherAccountingSoftware'); + await user.type(await screen.findByLabelText(inputLabel), 'Acme Books'); + await user.press(screen.getByText(TestHelper.translateLocal('workspace.accounting.qbo'))); + expect(screen.queryByLabelText(inputLabel)).not.toBeOnTheScreen(); + await user.press(screen.getByText(TestHelper.translateLocal('common.continue'))); + + expect(await getOnyxValue(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION)).toBe('quickbooksOnline'); + expect(await getOnyxValue(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION_NAME)).toBeUndefined(); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it.each([ + {integration: 'other' as const, integrationName: ' Acme Books ', expectedIntegration: 'other', expectedName: 'Acme Books'}, + {integration: 'other' as const, integrationName: ' ', expectedIntegration: 'other', expectedName: undefined}, + {integration: 'quickbooksOnline' as const, integrationName: 'Stale name', expectedIntegration: 'quickbooksOnline', expectedName: undefined}, + {integration: null, integrationName: 'Stale name', expectedIntegration: undefined, expectedName: undefined}, + ])('sends the custom name only for a non-empty Other selection: $integration / "$integrationName"', async ({integration, integrationName, expectedIntegration, expectedName}) => { + await setInterestedFeaturesOnyxState(integration, integrationName); + const {unmount} = renderOnboardingPage(SCREENS.ONBOARDING.INTERESTED_FEATURES); + await waitForBatchedUpdatesWithAct(); + + const user = userEvent.setup(); + await user.press(screen.getByText(TestHelper.translateLocal('common.continue'))); + + await waitFor(() => { + expect(mockCreateWorkspace).toHaveBeenCalledWith( + expect.objectContaining({ + userReportedIntegration: expectedIntegration, + userReportedIntegrationName: expectedName, + }), + ); + expect(mockCompleteOnboarding).toHaveBeenCalledWith( + expect.objectContaining({ + userReportedIntegration: expectedIntegration, + userReportedIntegrationName: expectedName, + }), + ); + }); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('omits the integration and custom name when Connections is disabled', async () => { + await setInterestedFeaturesOnyxState('other', 'Acme Books'); + const {unmount} = renderOnboardingPage(SCREENS.ONBOARDING.INTERESTED_FEATURES); + await waitForBatchedUpdatesWithAct(); + + const user = userEvent.setup(); + await user.press(screen.getByText(TestHelper.translateLocal('workspace.moreFeatures.connections.title'))); + await user.press(screen.getByText(TestHelper.translateLocal('common.continue'))); + + await waitFor(() => { + expect(mockCreateWorkspace).toHaveBeenCalledWith( + expect.objectContaining({ + userReportedIntegration: undefined, + userReportedIntegrationName: undefined, + }), + ); + expect(mockCompleteOnboarding).toHaveBeenCalledWith( + expect.objectContaining({ + userReportedIntegration: undefined, + userReportedIntegrationName: undefined, + }), + ); + }); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); +});