diff --git a/assets/images/integrationicons/circle-slash.svg b/assets/images/integrationicons/circle-slash.svg new file mode 100644 index 000000000000..ff09f67d3a88 --- /dev/null +++ b/assets/images/integrationicons/circle-slash.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/integrationicons/microsoft-dynamics-icon-square.svg b/assets/images/integrationicons/microsoft-dynamics-icon-square.svg new file mode 100644 index 000000000000..37dc57c5ae66 --- /dev/null +++ b/assets/images/integrationicons/microsoft-dynamics-icon-square.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/integrationicons/oracle-icon-square.svg b/assets/images/integrationicons/oracle-icon-square.svg new file mode 100644 index 000000000000..427e34ad2d69 --- /dev/null +++ b/assets/images/integrationicons/oracle-icon-square.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/integrationicons/sap-icon-square.svg b/assets/images/integrationicons/sap-icon-square.svg new file mode 100644 index 000000000000..6043a3ae07b5 --- /dev/null +++ b/assets/images/integrationicons/sap-icon-square.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index 448af0b44569..92b4d2eaacf5 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -69,6 +69,10 @@ const ONBOARDING_ACCOUNTING_MAPPING = { netsuite: 'NetSuite', intacct: 'Sage Intacct', quickbooksDesktop: 'QuickBooks Desktop', + sap: 'SAP', + oracle: 'Oracle', + microsoftDynamics: 'Microsoft Dynamics', + other: 'Other', }; const connectionsVideoPaths = { @@ -273,7 +277,7 @@ type OnboardingPurpose = ValueOf; type OnboardingCompanySize = ValueOf; -type OnboardingAccounting = ValueOf | null; +type OnboardingAccounting = keyof typeof CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY | null; const onboardingInviteTypes = { IOU: 'iou', @@ -3005,6 +3009,10 @@ const CONST = { financialForce: 'FinancialForce', billCom: 'Bill.com', zenefits: 'Zenefits', + sap: 'SAP', + oracle: 'Oracle', + microsoftDynamics: 'Microsoft Dynamics', + other: 'Other', }, AUTH_HELP_LINKS: { intacct: diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index cc3b7f71e826..4832e71ce248 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -137,7 +137,7 @@ function FeatureTrainingModal({ illustrationAspectRatio: illustrationAspectRatioProp, image, contentFitImage, - width = variables.onboardingModalWidth, + width = variables.featureTrainingModalWidth, title = '', description = '', secondaryDescription = '', diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 28e133620756..ac81fbb3d55c 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -118,11 +118,15 @@ import ImageCropCircleMask from '@assets/images/image-crop-circle-mask.svg'; import ImageCropSquareMask from '@assets/images/image-crop-square-mask.svg'; import Inbox from '@assets/images/inbox.svg'; import Info from '@assets/images/info.svg'; +import CircleSlash from '@assets/images/integrationicons/circle-slash.svg'; +import MicrosoftDynamicsSquare from '@assets/images/integrationicons/microsoft-dynamics-icon-square.svg'; import NetSuiteSquare from '@assets/images/integrationicons/netsuite-icon-square.svg'; +import OracleSquare from '@assets/images/integrationicons/oracle-icon-square.svg'; import QBDSquare from '@assets/images/integrationicons/qbd-icon-square.svg'; import QBOCircle from '@assets/images/integrationicons/qbo-icon-circle.svg'; import QBOSquare from '@assets/images/integrationicons/qbo-icon-square.svg'; import SageIntacctSquare from '@assets/images/integrationicons/sage-intacct-icon-square.svg'; +import SapSquare from '@assets/images/integrationicons/sap-icon-square.svg'; import XeroCircle from '@assets/images/integrationicons/xero-icon-circle.svg'; import XeroSquare from '@assets/images/integrationicons/xero-icon-square.svg'; import InvoiceGeneric from '@assets/images/invoice-generic.svg'; @@ -274,6 +278,7 @@ export { CreditCard, CreditCardHourglass, CreditCardExclamation, + CircleSlash, DeletedRoomAvatar, Document, DocumentSlash, @@ -423,6 +428,9 @@ export { NetSuiteSquare, XeroCircle, QBOCircle, + MicrosoftDynamicsSquare, + OracleSquare, + SapSquare, Filters, CalendarSolid, Filter, diff --git a/src/components/RadioButtonWithLabel.tsx b/src/components/RadioButtonWithLabel.tsx index 43f44065f5d9..d51c8102b0b2 100644 --- a/src/components/RadioButtonWithLabel.tsx +++ b/src/components/RadioButtonWithLabel.tsx @@ -1,4 +1,4 @@ -import type {ComponentType} from 'react'; +import type {ReactNode} from 'react'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; @@ -21,24 +21,27 @@ type RadioButtonWithLabelProps = { /** Text that appears next to check box */ label?: string; - /** Component to display for label */ - LabelComponent?: ComponentType; + /** React element to display for the label */ + labelElement?: ReactNode; - /** Should the input be styled for errors */ + /** Should the input be styled for errors */ hasError?: boolean; /** Error text to display */ errorText?: string; + + /** Additional styles to apply to the wrapper */ + wrapperStyle?: StyleProp; }; const PressableWithFeedback = Pressables.PressableWithFeedback; -function RadioButtonWithLabel({LabelComponent, style, label = '', hasError = false, errorText = '', isChecked, onPress}: RadioButtonWithLabelProps) { +function RadioButtonWithLabel({labelElement, style, label = '', hasError = false, errorText = '', isChecked, onPress, wrapperStyle}: RadioButtonWithLabelProps) { const styles = useThemeStyles(); const defaultStyles = [styles.flexRow, styles.alignItemsCenter]; - if (!label && !LabelComponent) { - throw new Error('Must provide at least label or LabelComponent prop'); + if (!label && !labelElement) { + throw new Error('Must provide at least label or labelComponent prop'); } return ( <> @@ -54,13 +57,13 @@ function RadioButtonWithLabel({LabelComponent, style, label = '', hasError = fal accessible={false} onPress={onPress} style={[styles.flexRow, styles.flexWrap, styles.flexShrink1, styles.alignItemsCenter]} - wrapperStyle={[styles.flex1, styles.ml3, styles.pr2]} + wrapperStyle={[styles.flex1, styles.ml3, styles.pr2, wrapperStyle]} // disable hover style when disabled hoverDimmingValue={0.8} pressDimmingValue={0.5} > {!!label && {label}} - {!!LabelComponent && } + {!!labelElement && labelElement} diff --git a/src/languages/en.ts b/src/languages/en.ts index b5adf3db2d2e..abb72fad0543 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2039,7 +2039,7 @@ const translations = { }, accounting: { title: 'Do you use any accounting software?', - noneOfAbove: 'None of the above', + none: 'None', }, error: { requiredFirstName: 'Please input your first name to continue', @@ -4337,6 +4337,9 @@ const translations = { xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + sap: 'SAP', + oracle: 'Oracle', + microsoftDynamics: 'Microsoft Dynamics', talkYourOnboardingSpecialist: 'Chat with your setup specialist.', talkYourAccountManager: 'Chat with your account manager.', talkToConcierge: 'Chat with Concierge.', @@ -4365,7 +4368,7 @@ const translations = { import: 'Import', export: 'Export', advanced: 'Advanced', - other: 'Other integrations', + other: 'Other', syncNow: 'Sync now', disconnect: 'Disconnect', reinstall: 'Reinstall connector', diff --git a/src/languages/es.ts b/src/languages/es.ts index dc4cdc9fa59a..90409fe5a934 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2043,7 +2043,7 @@ const translations = { }, accounting: { title: '¿Utilizas algún software de contabilidad?', - noneOfAbove: 'Ninguno de los anteriores', + none: 'Ninguno', }, error: { requiredFirstName: 'Introduce tu nombre para continuar', @@ -4348,6 +4348,9 @@ const translations = { xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + sap: 'SAP', + oracle: 'Oracle', + microsoftDynamics: 'Microsoft Dynamics', talkYourOnboardingSpecialist: 'Chatea con tu especialista asignado.', talkYourAccountManager: 'Chatea con tu gestor de cuenta.', talkToConcierge: 'Chatear con Concierge.', @@ -4376,7 +4379,7 @@ const translations = { import: 'Importar', export: 'Exportar', advanced: 'Avanzado', - other: 'Otras integraciones', + other: 'Otro', syncNow: 'Sincronizar ahora', disconnect: 'Desconectar', reinstall: 'Reinstalar el conector', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9d27087b7a1b..0535c1928804 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -9687,7 +9687,7 @@ function prepareOnboardingOnyxData( return; } - const integrationName = userReportedIntegration ? CONST.ONBOARDING_ACCOUNTING_MAPPING[userReportedIntegration] : ''; + const integrationName = userReportedIntegration ? CONST.ONBOARDING_ACCOUNTING_MAPPING[userReportedIntegration as keyof typeof CONST.ONBOARDING_ACCOUNTING_MAPPING] : ''; const assignedGuideEmail = getPolicy(targetChatPolicyID)?.assignedGuide?.email ?? 'Setup Specialist'; const assignedGuidePersonalDetail = Object.values(allPersonalDetails ?? {}).find((personalDetail) => personalDetail?.login === assignedGuideEmail); let assignedGuideAccountID: number; @@ -10076,7 +10076,7 @@ function prepareOnboardingOnyxData( key: `${ONYXKEYS.COLLECTION.POLICY}${onboardingPolicyID}`, value: { areConnectionsEnabled: true, - ...(requiresControlPlan.includes(userReportedIntegration) + ...(requiresControlPlan.includes(userReportedIntegration as AllConnectionName) ? { type: CONST.POLICY.TYPE.CORPORATE, } diff --git a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx index 25bd0b3a31c6..c178947fac9e 100644 --- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx +++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx @@ -1,16 +1,19 @@ import HybridAppModule from '@expensify/react-native-hybrid-app'; -import React, {useContext, useEffect, useMemo, useState} from 'react'; -import {InteractionManager} from 'react-native'; +import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import {InteractionManager, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import type {SvgProps} from 'react-native-svg'; import Button from '@components/Button'; import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; +import FixedFooter from '@components/FixedFooter'; import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithoutFeedback} from '@components/Pressable'; +import RadioButtonWithLabel from '@components/RadioButtonWithLabel'; import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; +import ScrollView from '@components/ScrollView'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; @@ -32,12 +35,61 @@ import Navigation from '@libs/Navigation/Navigation'; import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import variables from '@styles/variables'; import CONFIG from '@src/CONFIG'; -import CONST from '@src/CONST'; import type {OnboardingAccounting} from '@src/CONST'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {} from '@src/types/onyx/Bank'; import type {BaseOnboardingAccountingProps} from './types'; +type Integration = { + key: OnboardingAccounting; + icon: React.FC; + translationKey: TranslationPaths; +}; + +const integrations: Integration[] = [ + { + key: 'quickbooksOnline', + icon: Expensicons.QBOCircle, + translationKey: 'workspace.accounting.qbo', + }, + { + key: 'quickbooksDesktop', + icon: Expensicons.QBDSquare, + translationKey: 'workspace.accounting.qbd', + }, + { + key: 'xero', + icon: Expensicons.XeroCircle, + translationKey: 'workspace.accounting.xero', + }, + { + key: 'netsuite', + icon: Expensicons.NetSuiteSquare, + translationKey: 'workspace.accounting.netsuite', + }, + { + key: 'intacct', + icon: Expensicons.IntacctSquare, + translationKey: 'workspace.accounting.intacct', + }, + { + key: 'sap', + icon: Expensicons.SapSquare, + translationKey: 'workspace.accounting.sap', + }, + { + key: 'oracle', + icon: Expensicons.OracleSquare, + translationKey: 'workspace.accounting.oracle', + }, + { + key: 'microsoftDynamics', + icon: Expensicons.MicrosoftDynamicsSquare, + translationKey: 'workspace.accounting.microsoftDynamics', + }, +]; + type OnboardingListItem = ListItem & { keyForList: OnboardingAccounting; }; @@ -89,177 +141,208 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount }, [isLoading, prevIsLoading, setRootStatusBarEnabled]); const accountingOptions: OnboardingListItem[] = useMemo(() => { - const policyAccountingOptions = Object.values(CONST.POLICY.CONNECTIONS.NAME) - .map((connectionName): OnboardingListItem | undefined => { - let text; - let accountingIcon; - switch (connectionName) { - case CONST.POLICY.CONNECTIONS.NAME.QBO: { - text = translate('workspace.accounting.qbo'); - accountingIcon = Expensicons.QBOCircle; - break; - } - case CONST.POLICY.CONNECTIONS.NAME.QBD: { - text = translate('workspace.accounting.qbd'); - accountingIcon = Expensicons.QBDSquare; - break; - } - case CONST.POLICY.CONNECTIONS.NAME.XERO: { - text = translate('workspace.accounting.xero'); - accountingIcon = Expensicons.XeroCircle; - break; - } - case CONST.POLICY.CONNECTIONS.NAME.NETSUITE: { - text = translate('workspace.accounting.netsuite'); - accountingIcon = Expensicons.NetSuiteSquare; - break; - } - case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: { - text = translate('workspace.accounting.intacct'); - accountingIcon = Expensicons.IntacctSquare; - break; - } - default: { - return; - } - } - return { - keyForList: connectionName, - text, - leftElement: ( - - ), - isSelected: userReportedIntegration === connectionName, - }; - }) - .filter((item): item is OnboardingListItem => !!item); + const createAccountingOption = (integration: Integration): OnboardingListItem => ({ + keyForList: integration.key, + text: translate(integration.translationKey), + leftElement: ( + + ), + isSelected: userReportedIntegration === integration.key, + }); + const noneAccountingOption: OnboardingListItem = { keyForList: null, - text: translate('onboarding.accounting.noneOfAbove'), + text: translate('onboarding.accounting.none'), leftElement: ( ), isSelected: userReportedIntegration === null, }; - return [...policyAccountingOptions, noneAccountingOption]; - }, [StyleUtils, styles.mr3, styles.onboardingSmallIcon, theme.success, translate, userReportedIntegration]); - const footerContent = ( - <> - {!!error && ( - - )} -