From bfec03d5630c113959de333f1e119facdbaf1cfb Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 10 Jul 2025 17:04:26 +0700 Subject: [PATCH 01/10] implement onboarding flow --- src/ONYXKEYS.ts | 5 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/languages/en.ts | 5 + .../Navigators/OnboardingModalNavigator.tsx | 5 + src/libs/Navigation/linkingConfig/config.ts | 4 + src/libs/Navigation/types.ts | 3 + src/libs/actions/Policy/Policy.ts | 66 ++++ src/libs/actions/Welcome/index.ts | 6 + .../BaseOnboardingAccounting.tsx | 84 +---- .../BaseOnboardingInterestedFeatures.tsx | 355 ++++++++++++++++++ .../index.native.tsx | 17 + .../OnboardingInterestedFeatures/index.tsx | 20 + .../OnboardingInterestedFeatures/types.ts | 30 ++ src/styles/index.ts | 13 + 15 files changed, 544 insertions(+), 74 deletions(-) create mode 100644 src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx create mode 100644 src/pages/OnboardingInterestedFeatures/index.native.tsx create mode 100644 src/pages/OnboardingInterestedFeatures/index.tsx create mode 100644 src/pages/OnboardingInterestedFeatures/types.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 89b73fd157b1..9692379a271a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,6 +1,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; +import type {OnboardingAccounting} from './CONST'; import type {OnboardingCompanySize} from './libs/actions/Welcome/OnboardingFlow'; import type Platform from './libs/getPlatform/types'; import type * as FormTypes from './types/form'; @@ -377,6 +378,9 @@ const ONYXKEYS = { /** Onboarding company size selected by the user during Onboarding flow */ ONBOARDING_COMPANY_SIZE: 'onboardingCompanySize', + /** Onboarding user reported integration selected by the user during Onboarding flow */ + ONBOARDING_USER_REPORTED_INTEGRATION: 'onboardingUserReportedIntegration', + /** Onboarding Purpose selected by the user during Onboarding flow */ ONBOARDING_ADMINS_CHAT_REPORT_ID: 'onboardingAdminsChatReportID', @@ -1199,6 +1203,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_LAST_IPHONE_LOGIN]: string; [ONYXKEYS.NVP_LAST_ANDROID_LOGIN]: string; [ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_REPORT_IDS]: string[]; + [ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION]: OnboardingAccounting; }; type OnyxDerivedValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7fc8e1366a3c..d1c8e607ffea 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1982,6 +1982,10 @@ const ROUTES = { route: 'onboarding/accounting', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/accounting`, backTo), }, + ONBOARDING_INTERESTED_FEATURES: { + route: 'onboarding/interested-features', + getRoute: (userReportedIntegration?: string, backTo?: string) => getUrlWithBackToParam(`onboarding/interested-features?userReportedIntegration=${userReportedIntegration}`, backTo), + }, ONBOARDING_PURPOSE: { route: 'onboarding/purpose', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding/purpose`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3d5c5bed63cd..0b774a9d5b48 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -662,6 +662,7 @@ const SCREENS = { PRIVATE_DOMAIN: 'Onboarding_Private_Domain', EMPLOYEES: 'Onboarding_Employees', ACCOUNTING: 'Onboarding_Accounting', + INTERESTED_FEATURES: 'Onboarding_Interested_Features', WORKSPACES: 'Onboarding_Workspaces', WORK_EMAIL: 'Onboarding_Work_Email', WORK_EMAIL_VALIDATION: 'Onboarding_Work_Email_Validation', diff --git a/src/languages/en.ts b/src/languages/en.ts index 362abadf8a4e..a70b0f23e98f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2168,6 +2168,11 @@ const translations = { title: 'Do you use any accounting software?', none: 'None', }, + interestedFeatures: { + title: 'What features are you interested in?', + featuresAlreadyEnabled: 'Your workspace already has the following enabled:', + featureYouMayBeInterestedIn: 'Enable additional features you may be interested in:', + }, error: { requiredFirstName: 'Please input your first name to continue', }, diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index 43921519f909..69a18086cb94 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -15,6 +15,7 @@ import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types'; import OnboardingRefManager from '@libs/OnboardingRefManager'; import OnboardingAccounting from '@pages/OnboardingAccounting'; import OnboardingEmployees from '@pages/OnboardingEmployees'; +import OnboardingInterestedFeatures from '@pages/OnboardingInterestedFeatures'; import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails'; import OnboardingPrivateDomain from '@pages/OnboardingPrivateDomain'; import OnboardingPurpose from '@pages/OnboardingPurpose'; @@ -140,6 +141,10 @@ function OnboardingModalNavigator() { name={SCREENS.ONBOARDING.ACCOUNTING} component={OnboardingAccounting} /> + ['config'] = { path: ROUTES.ONBOARDING_ACCOUNTING.route, exact: true, }, + [SCREENS.ONBOARDING.INTERESTED_FEATURES]: { + path: ROUTES.ONBOARDING_INTERESTED_FEATURES.route, + exact: true, + }, [SCREENS.ONBOARDING.WORKSPACES]: { path: ROUTES.ONBOARDING_WORKSPACES.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 0a8db96592b0..61cc5e414c31 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1924,6 +1924,9 @@ type OnboardingModalNavigatorParamList = { [SCREENS.ONBOARDING.ACCOUNTING]: { backTo?: string; }; + [SCREENS.ONBOARDING.INTERESTED_FEATURES]: { + backTo?: string; + }; [SCREENS.ONBOARDING.WORK_EMAIL]: { backTo?: string; }; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 61ff0823a49e..ed6e3217ce10 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -60,6 +60,7 @@ import type { UpgradeToCorporateParams, } from '@libs/API/parameters'; import type UpdatePolicyMembersCustomFieldsParams from '@libs/API/parameters/UpdatePolicyMembersCustomFieldsParams'; +import type {ApiRequestCommandParameters} from '@libs/API/types'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; @@ -77,8 +78,10 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import {goBackWhenEnableFeature, navigateToExpensifyCardPage} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import type {PolicySelector} from '@pages/home/sidebar/FloatingActionButtonAndPopover'; +import type {Feature} from '@pages/OnboardingInterestedFeatures/types'; import * as PaymentMethods from '@userActions/PaymentMethods'; import * as PersistedRequests from '@userActions/PersistedRequests'; +import type {EnablePolicyFeatureCommand} from '@userActions/RequestConflictUtils'; import {buildTaskData} from '@userActions/Task'; import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow'; import type {OnboardingCompanySize, OnboardingPurpose} from '@userActions/Welcome/OnboardingFlow'; @@ -5419,6 +5422,68 @@ function setIsComingFromGlobalReimbursementsFlow(value: boolean) { Onyx.set(ONYXKEYS.IS_COMING_FROM_GLOBAL_REIMBURSEMENTS_FLOW, value); } +function updateFeature( + request: { + endpoint: EnablePolicyFeatureCommand | typeof WRITE_COMMANDS.TOGGLE_POLICY_PER_DIEM; + parameters: ApiRequestCommandParameters[EnablePolicyFeatureCommand | typeof WRITE_COMMANDS.TOGGLE_POLICY_PER_DIEM]; + }, + policyID: string, +) { + if (request.endpoint === WRITE_COMMANDS.TOGGLE_POLICY_PER_DIEM) { + API.write(WRITE_COMMANDS.TOGGLE_POLICY_PER_DIEM, { + policyID, + enabled: request.parameters.enabled, + customUnitID: generateCustomUnitID(), + }); + return; + } + // eslint-disable-next-line rulesdir/no-multiple-api-calls + API.writeWithNoDuplicatesEnableFeatureConflicts(request.endpoint, request.parameters); +} + +function updateInterestedFeatures(features: Feature[], policyID: string) { + let shouldUpgradeToCorporate = false; + + const requests: Array<{ + endpoint: EnablePolicyFeatureCommand | typeof WRITE_COMMANDS.TOGGLE_POLICY_PER_DIEM; + parameters: ApiRequestCommandParameters[EnablePolicyFeatureCommand | typeof WRITE_COMMANDS.TOGGLE_POLICY_PER_DIEM]; + }> = []; + + features.forEach((feature) => { + // If the feature is not enabled by default and it's programmaticaly enabled, we need to enable it + if (!feature.enabledByDefault && feature.programmaticalyEnabled) { + if (feature.requiresUpdate && !shouldUpgradeToCorporate) { + shouldUpgradeToCorporate = true; + } + requests.push({ + endpoint: feature.apiEndpoint, + parameters: { + policyID, + enabled: true, + }, + }); + } + // If the feature is enabled by default and it's programmaticaly disabled, we need to disable it + if (feature.enabledByDefault && !feature.programmaticalyEnabled) { + requests.push({ + endpoint: feature.apiEndpoint, + parameters: { + policyID, + enabled: false, + }, + }); + } + }); + + if (shouldUpgradeToCorporate) { + API.write(WRITE_COMMANDS.UPGRADE_TO_CORPORATE, {policyID}); + } + + requests.forEach((request) => { + updateFeature(request, policyID); + }); +} + export { leaveWorkspace, addBillingCardAndRequestPolicyOwnerChange, @@ -5527,4 +5592,5 @@ export { setIsForcedToChangeCurrency, setIsComingFromGlobalReimbursementsFlow, setPolicyAttendeeTrackingEnabled, + updateInterestedFeatures, }; diff --git a/src/libs/actions/Welcome/index.ts b/src/libs/actions/Welcome/index.ts index aac0228ce450..5565c9910642 100644 --- a/src/libs/actions/Welcome/index.ts +++ b/src/libs/actions/Welcome/index.ts @@ -7,6 +7,7 @@ import DateUtils from '@libs/DateUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import CONFIG from '@src/CONFIG'; +import type {OnboardingAccounting} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {OnboardingPurpose} from '@src/types/onyx'; @@ -103,6 +104,10 @@ function setOnboardingCompanySize(value: OnboardingCompanySize) { Onyx.set(ONYXKEYS.ONBOARDING_COMPANY_SIZE, value); } +function setOnboardingUserReportedIntegration(value: OnboardingAccounting | null) { + Onyx.set(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION, value); +} + function setOnboardingErrorMessage(value: string) { Onyx.set(ONYXKEYS.ONBOARDING_ERROR_MESSAGE, value ?? null); } @@ -250,4 +255,5 @@ export { setSelfTourViewed, setOnboardingMergeAccountStepValue, updateOnboardingValuesAndNavigation, + setOnboardingUserReportedIntegration, }; diff --git a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx index 1a311b20cb84..7e3b1ad3f22d 100644 --- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx +++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx @@ -1,6 +1,6 @@ import HybridAppModule from '@expensify/react-native-hybrid-app'; import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import type {SvgProps} from 'react-native-svg'; import Button from '@components/Button'; import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; @@ -17,19 +17,14 @@ import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useOnboardingMessages from '@hooks/useOnboardingMessages'; import useOnyx from '@hooks/useOnyx'; -import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {openOldDotLink} from '@libs/actions/Link'; -import {createWorkspace, generatePolicyID} from '@libs/actions/Policy/Policy'; -import {completeOnboarding} from '@libs/actions/Report'; -import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@libs/actions/Welcome'; -import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; +import {setOnboardingAdminsChatReportID, setOnboardingPolicyID, setOnboardingUserReportedIntegration} from '@libs/actions/Welcome'; import Navigation from '@libs/Navigation/Navigation'; import {waitForIdle} from '@libs/Network/SequentialQueue'; import {shouldOnboardingRedirectToOldDot} from '@libs/OnboardingUtils'; @@ -96,26 +91,24 @@ type OnboardingListItem = ListItem & { keyForList: OnboardingAccounting; }; -function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccountingProps) { +function BaseOnboardingAccounting({shouldUseNativeStyles, route}: BaseOnboardingAccountingProps) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const {onboardingMessages} = useOnboardingMessages(); const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); // We need to use isSmallScreenWidth, see navigateAfterOnboarding function comment // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {onboardingIsMediumOrLargerScreenWidth, isSmallScreenWidth} = useResponsiveLayout(); - const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, {canBeMissing: true}); const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID, {canBeMissing: true}); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); - const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, {canBeMissing: true}); const [onboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE, {canBeMissing: true}); - const {isBetaEnabled} = usePermissions(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); - const [userReportedIntegration, setUserReportedIntegration] = useState(undefined); + const [onboardingUserReportedIntegration] = useOnyx(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION, {canBeMissing: true}); + + const [userReportedIntegration, setUserReportedIntegration] = useState(onboardingUserReportedIntegration ?? undefined); const [error, setError] = useState(''); const paidGroupPolicy = Object.values(allPolicies ?? {}).find((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, session?.email)); @@ -202,68 +195,11 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount return; } - if (!onboardingPurposeSelected || !onboardingCompanySize) { - return; - } - - const shouldCreateWorkspace = !onboardingPolicyID && !paidGroupPolicy; + setOnboardingUserReportedIntegration(userReportedIntegration); - // We need `adminsChatReportID` for `completeOnboarding`, but at the same time, we don't want to call `createWorkspace` more than once. - // If we have already created a workspace, we want to reuse the `onboardingAdminsChatReportID` and `onboardingPolicyID`. - const {adminsChatReportID, policyID} = shouldCreateWorkspace - ? createWorkspace(undefined, true, '', generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM, '', undefined, false, onboardingCompanySize, userReportedIntegration) - : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; - - if (shouldCreateWorkspace) { - setOnboardingAdminsChatReportID(adminsChatReportID); - setOnboardingPolicyID(policyID); - } - - completeOnboarding({ - engagementChoice: onboardingPurposeSelected, - onboardingMessage: onboardingMessages[onboardingPurposeSelected], - adminsChatReportID, - onboardingPolicyID: policyID, - companySize: onboardingCompanySize, - userReportedIntegration, - }); - - if (shouldOnboardingRedirectToOldDot(onboardingCompanySize, userReportedIntegration)) { - if (CONFIG.IS_HYBRID_APP) { - return; - } - openOldDotLink(CONST.OLDDOT_URLS.INBOX, true); - } - - // Avoid creating new WS because onboardingPolicyID is cleared before unmounting - InteractionManager.runAfterInteractions(() => { - setOnboardingAdminsChatReportID(); - setOnboardingPolicyID(); - }); - - // We need to wait the policy is created before navigating out the onboarding flow - navigateAfterOnboardingWithMicrotaskQueue( - isSmallScreenWidth, - isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), - policyID, - adminsChatReportID, - // Onboarding tasks would show in Concierge instead of admins room for testing accounts, we should open where onboarding tasks are located - // See https://github.com/Expensify/App/issues/57167 for more details - (session?.email ?? '').includes('+'), - ); - }, [ - isBetaEnabled, - isSmallScreenWidth, - onboardingAdminsChatReportID, - onboardingCompanySize, - onboardingMessages, - onboardingPolicyID, - onboardingPurposeSelected, - paidGroupPolicy, - session?.email, - translate, - userReportedIntegration, - ]); + // Navigate to the next onboarding step with the selected integration + Navigation.navigate(ROUTES.ONBOARDING_INTERESTED_FEATURES.getRoute(route.params?.backTo)); + }, [translate, userReportedIntegration, route.params?.backTo]); const handleIntegrationSelect = useCallback((integrationKey: OnboardingAccounting | null) => { setUserReportedIntegration(integrationKey); diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx new file mode 100644 index 000000000000..a36e6bf15a9c --- /dev/null +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -0,0 +1,355 @@ +import HybridAppModule from '@expensify/react-native-hybrid-app'; +import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import {InteractionManager, View} from 'react-native'; +import Button from '@components/Button'; +import Checkbox from '@components/Checkbox'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; +import FixedFooter from '@components/FixedFooter'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import * as Illustrations from '@components/Icon/Illustrations'; +import {PressableWithoutFeedback} from '@components/Pressable'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnboardingMessages from '@hooks/useOnboardingMessages'; +import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; +import usePrevious from '@hooks/usePrevious'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {openOldDotLink} from '@libs/actions/Link'; +import {createWorkspace, generatePolicyID, updateInterestedFeatures} from '@libs/actions/Policy/Policy'; +import {completeOnboarding} from '@libs/actions/Report'; +import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@libs/actions/Welcome'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; +import Navigation from '@libs/Navigation/Navigation'; +import {waitForIdle} from '@libs/Network/SequentialQueue'; +import {shouldOnboardingRedirectToOldDot} from '@libs/OnboardingUtils'; +import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {BaseOnboardingInterestedFeaturesProps, Feature, SectionObject} from './types'; + +function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardingInterestedFeaturesProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {onboardingMessages} = useOnboardingMessages(); + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); + + // We need to use isSmallScreenWidth, see navigateAfterOnboarding function comment + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {onboardingIsMediumOrLargerScreenWidth, isSmallScreenWidth} = useResponsiveLayout(); + const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, {canBeMissing: true}); + const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID, {canBeMissing: true}); + const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: false}); + const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, {canBeMissing: true}); + const [onboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE, {canBeMissing: true}); + const [userReportedIntegration] = useOnyx(ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION, {canBeMissing: true}); + + const {isBetaEnabled} = usePermissions(); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + + const paidGroupPolicy = Object.values(allPolicies ?? {}).find((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, session?.email)); + const [onboarding] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {canBeMissing: true}); + const {isOffline} = useNetwork(); + const isLoading = onboarding?.isLoading; + const prevIsLoading = usePrevious(isLoading); + + const features: Feature[] = useMemo(() => { + return [ + { + id: 'categories', + title: translate('workspace.moreFeatures.categories.title'), + icon: Illustrations.FolderOpen, + enabledByDefault: true, + apiEndpoint: WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, + }, + { + id: 'accounting', + title: translate('workspace.moreFeatures.connections.title'), + icon: Illustrations.Accounting, + enabledByDefault: true, + apiEndpoint: WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS, + }, + { + id: 'company-cards', + title: translate('workspace.moreFeatures.companyCards.title'), + icon: Illustrations.CompanyCard, + enabledByDefault: true, + apiEndpoint: WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS, + }, + { + id: 'workflows', + title: translate('workspace.moreFeatures.workflows.title'), + icon: Illustrations.Workflows, + enabledByDefault: true, + apiEndpoint: WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, + }, + { + id: 'invoices', + title: translate('workspace.moreFeatures.invoices.title'), + icon: Illustrations.InvoiceBlue, + apiEndpoint: WRITE_COMMANDS.ENABLE_POLICY_INVOICING, + }, + { + id: 'rules', + title: translate('workspace.moreFeatures.rules.title'), + icon: Illustrations.Rules, + apiEndpoint: WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, + requiresUpdate: true, + }, + { + id: 'distance-rates', + title: translate('workspace.moreFeatures.distanceRates.title'), + icon: Illustrations.Car, + apiEndpoint: WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, + }, + { + id: 'expensify-card', + title: translate('workspace.moreFeatures.expensifyCard.title'), + icon: Illustrations.HandCard, + apiEndpoint: WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS, + }, + { + id: 'tags', + title: translate('workspace.moreFeatures.tags.title'), + icon: Illustrations.Tag, + apiEndpoint: WRITE_COMMANDS.ENABLE_POLICY_TAGS, + }, + { + id: 'per-diem', + title: translate('workspace.moreFeatures.perDiem.title'), + icon: Illustrations.PerDiem, + apiEndpoint: WRITE_COMMANDS.TOGGLE_POLICY_PER_DIEM, + requiresUpdate: true, + }, + ]; + }, [translate]); + + const [selectedFeatures, setSelectedFeatures] = useState(() => features.filter((feature) => feature.enabledByDefault).map((feature) => feature.id)); + + // Set onboardingPolicyID and onboardingAdminsChatReportID if a workspace is created by the backend for OD signup + useEffect(() => { + if (!paidGroupPolicy || onboardingPolicyID) { + return; + } + setOnboardingAdminsChatReportID(paidGroupPolicy.chatReportIDAdmins?.toString()); + setOnboardingPolicyID(paidGroupPolicy.id); + }, [paidGroupPolicy, onboardingPolicyID]); + + useEffect(() => { + if (!!isLoading || !prevIsLoading) { + return; + } + + if (CONFIG.IS_HYBRID_APP) { + HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true}); + setRootStatusBarEnabled(false); + return; + } + waitForIdle().then(() => { + openOldDotLink(CONST.OLDDOT_URLS.INBOX, true); + }); + }, [isLoading, prevIsLoading, setRootStatusBarEnabled]); + + const handleContinue = useCallback(() => { + if (!onboardingPurposeSelected || !onboardingCompanySize) { + return; + } + + const shouldCreateWorkspace = !onboardingPolicyID && !paidGroupPolicy; + + // We need `adminsChatReportID` for `completeOnboarding`, but at the same time, we don't want to call `createWorkspace` more than once. + // If we have already created a workspace, we want to reuse the `onboardingAdminsChatReportID` and `onboardingPolicyID`. + const {adminsChatReportID, policyID} = shouldCreateWorkspace + ? createWorkspace(undefined, true, '', generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM, '', undefined, false, onboardingCompanySize, userReportedIntegration) + : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; + + updateInterestedFeatures( + features.map((feature) => ({ + ...feature, + programmaticalyEnabled: selectedFeatures.includes(feature.id), + })), + policyID ?? '', + ); + + if (shouldCreateWorkspace) { + setOnboardingAdminsChatReportID(adminsChatReportID); + setOnboardingPolicyID(policyID); + } + + completeOnboarding({ + engagementChoice: onboardingPurposeSelected, + onboardingMessage: onboardingMessages[onboardingPurposeSelected], + adminsChatReportID, + onboardingPolicyID: policyID, + companySize: onboardingCompanySize, + userReportedIntegration, + }); + + if (shouldOnboardingRedirectToOldDot(onboardingCompanySize, userReportedIntegration)) { + if (CONFIG.IS_HYBRID_APP) { + return; + } + openOldDotLink(CONST.OLDDOT_URLS.INBOX, true); + } + + // Avoid creating new WS because onboardingPolicyID is cleared before unmounting + InteractionManager.runAfterInteractions(() => { + setOnboardingAdminsChatReportID(); + setOnboardingPolicyID(); + }); + + // We need to wait the policy is created before navigating out the onboarding flow + navigateAfterOnboardingWithMicrotaskQueue( + isSmallScreenWidth, + isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), + policyID, + adminsChatReportID, + // Onboarding tasks would show in Concierge instead of admins room for testing accounts, we should open where onboarding tasks are located + // See https://github.com/Expensify/App/issues/57167 for more details + (session?.email ?? '').includes('+'), + ); + }, [ + isBetaEnabled, + isSmallScreenWidth, + onboardingAdminsChatReportID, + onboardingCompanySize, + onboardingMessages, + onboardingPolicyID, + onboardingPurposeSelected, + paidGroupPolicy, + session?.email, + userReportedIntegration, + features, + selectedFeatures, + ]); + + // Create items for enabled features + const enabledFeatures: Feature[] = features + .filter((feature) => feature.enabledByDefault) + .map((feature) => ({ + ...feature, + })); + + // Create items for features they may be interested in + const mayBeInterestedFeatures: Feature[] = features + .filter((feature) => !feature.enabledByDefault) + .map((feature) => ({ + ...feature, + })); + + // Define sections + const sections: SectionObject[] = [ + { + titleTranslationKey: 'onboarding.interestedFeatures.featuresAlreadyEnabled', + items: enabledFeatures, + }, + { + titleTranslationKey: 'onboarding.interestedFeatures.featureYouMayBeInterestedIn', + items: mayBeInterestedFeatures, + }, + ]; + + const handleFeatureSelect = useCallback((featureId: string) => { + setSelectedFeatures((prev) => { + if (prev.includes(featureId)) { + return prev.filter((id) => id !== featureId); + } + return [...prev, featureId]; + }); + }, []); + + const renderItem = useCallback( + (item: Feature) => { + const isSelected = selectedFeatures.includes(item.id); + return ( + { + handleFeatureSelect(item.id); + }} + accessibilityLabel={item.title} + accessible={false} + hoverStyle={!isSelected ? styles.hoveredComponentBG : undefined} + style={[styles.onboardingInterestedFeaturesItem, isSmallScreenWidth && styles.flexBasis100, isSelected && styles.activeComponentBG]} + > + + + {item.title} + + { + handleFeatureSelect(item.id); + }} + /> + + ); + }, + [styles, isSmallScreenWidth, selectedFeatures, handleFeatureSelect], + ); + + const renderSection = useCallback( + (section: SectionObject) => ( +
{translate(section.titleTranslationKey as TranslationPaths)}} + subtitleMuted + > + {section.items.map(renderItem)} +
+ ), + [styles, renderItem, translate], + ); + + return ( + + Navigation.goBack(ROUTES.ONBOARDING_ACCOUNTING.getRoute())} + /> + + {translate('onboarding.interestedFeatures.title')} + + + {sections.map(renderSection)} + + +