diff --git a/src/CONST/index.ts b/src/CONST/index.ts index bcdd4c83849d..3685c5d6f98d 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -103,9 +103,12 @@ const chatTypes = { const ONBOARDING_ACCOUNTING_MAPPING = { quickbooksOnline: 'QuickBooks Online', + intuitEnterpriseSuite: 'Intuit Enterprise Suite', xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + financialforce: 'Certinia', + rillet: 'Rillet', quickbooksDesktop: 'QuickBooks Desktop', sap: 'SAP', oracle: 'Oracle', diff --git a/src/languages/de.ts b/src/languages/de.ts index c3f2566239ed..955d61a1e28d 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3346,8 +3346,7 @@ ${amount} für ${merchant} – ${date}`, }, accounting: { title: 'Verwendest du eine Buchhaltungssoftware?', - none: 'Keine', - otherAccountingSoftware: 'Deine Buchhaltungssoftware', + otherAccountingSoftware: 'Name der Software', }, interestedFeatures: { title: 'An welchen Funktionen bist du interessiert?', diff --git a/src/languages/el.ts b/src/languages/el.ts index 8f7c78a93910..422a52f3f6e4 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -3399,8 +3399,7 @@ ${amount} για ${merchant} - ${date}`, }, accounting: { title: 'Χρησιμοποιείτε κάποιο λογιστικό λογισμικό;', - none: 'Κανένα', - otherAccountingSoftware: 'Το λογιστικό σας λογισμικό', + otherAccountingSoftware: 'Όνομα λογισμικού', }, interestedFeatures: { title: 'Σε ποιες δυνατότητες ενδιαφέρεστε;', diff --git a/src/languages/en.ts b/src/languages/en.ts index 0ab827f6b67a..aaa67f9dcbb0 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3440,8 +3440,7 @@ const translations = { }, accounting: { title: 'Do you use any accounting software?', - none: 'None', - otherAccountingSoftware: 'Your accounting software', + otherAccountingSoftware: 'Name of software', }, interestedFeatures: { title: 'What features are you interested in?', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4eaad6f99764..530be84f1235 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3254,8 +3254,7 @@ ${amount} para ${merchant} - ${date}`, }, accounting: { title: '¿Utilizas algún software de contabilidad?', - none: 'Ninguno', - otherAccountingSoftware: 'Tu software de contabilidad', + otherAccountingSoftware: 'Nombre del software', }, interestedFeatures: { title: '¿Qué funciones te interesan?', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index cb157ffffb4c..70bf85ce6f6a 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3354,8 +3354,7 @@ ${amount} pour ${merchant} - ${date}`, }, accounting: { title: 'Utilisez-vous un logiciel de comptabilité ?', - none: 'Aucun', - otherAccountingSoftware: 'Votre logiciel de comptabilité', + otherAccountingSoftware: 'Nom du logiciel', }, interestedFeatures: { title: 'Quelles fonctionnalités vous intéressent ?', diff --git a/src/languages/it.ts b/src/languages/it.ts index 8276553f8ee2..3ddfad9c1063 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3334,8 +3334,7 @@ ${amount} per ${merchant} - ${date}`, }, accounting: { title: 'Usi un software di contabilità?', - none: 'Nessuno', - otherAccountingSoftware: 'Il tuo software di contabilità', + otherAccountingSoftware: 'Nome del software', }, interestedFeatures: { title: 'A quali funzionalità sei interessato?', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index a00d7c5b0eb8..9afe4766c6e7 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3306,8 +3306,7 @@ ${date} の ${merchant} への ${amount}`, }, accounting: { title: '会計ソフトを利用していますか?', - none: 'なし', - otherAccountingSoftware: 'ご利用の会計ソフト', + otherAccountingSoftware: 'ソフトウェア名', }, interestedFeatures: { title: 'どの機能にご興味がありますか?', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index c30e3f205c6d..eaa666012078 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3336,8 +3336,7 @@ ${amount} voor ${merchant} - ${date}`, }, accounting: { title: 'Gebruikt u een boekhoudprogramma?', - none: 'Geen', - otherAccountingSoftware: 'Je boekhoudsoftware', + otherAccountingSoftware: 'Naam van de software', }, interestedFeatures: { title: 'In welke functies ben je geïnteresseerd?', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 885864be4592..2e9366674c0d 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3366,8 +3366,7 @@ ${amount} dla ${merchant} - ${date}`, }, accounting: { title: 'Czy korzystasz z jakiegoś oprogramowania księgowego?', - none: 'Brak', - otherAccountingSoftware: 'Twoje oprogramowanie księgowe', + otherAccountingSoftware: 'Nazwa oprogramowania', }, interestedFeatures: { title: 'Jakie funkcje Cię interesują?', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 3a44db440220..bb25b9d5c3ea 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3325,8 +3325,7 @@ ${amount} para ${merchant} - ${date}`, }, accounting: { title: 'Você usa algum software de contabilidade?', - none: 'Nenhum', - otherAccountingSoftware: 'Seu software de contabilidade', + otherAccountingSoftware: 'Nome do software', }, interestedFeatures: { title: 'Em quais recursos você tem interesse?', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 0519fa4dfff9..424eac3f1365 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3223,8 +3223,7 @@ ${amount},商户:${merchant} - 日期:${date}`, }, accounting: { title: '你是否使用任何会计软件?', - none: '无', - otherAccountingSoftware: '你的会计软件', + otherAccountingSoftware: '软件名称', }, interestedFeatures: { title: '你对哪些功能感兴趣?', diff --git a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx index 7e592572ff88..d1b27cc9c325 100644 --- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx +++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx @@ -6,7 +6,7 @@ import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import OnboardingHeader from '@components/OnboardingHeader'; import {PressableWithoutFeedback} from '@components/Pressable'; -import RadioButtonWithLabel from '@components/RadioButtonWithLabel'; +import RadioButton from '@components/RadioButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import type {ListItem} from '@components/SelectionList/types'; @@ -15,12 +15,11 @@ import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useCompleteOnboarding from '@hooks/useCompleteOnboarding'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {setOnboardingAccountingEnabled, setOnboardingAdminsChatReportID, setOnboardingPolicyID, setOnboardingUserReportedIntegration} from '@libs/actions/Welcome'; @@ -47,19 +46,37 @@ import {View} from 'react-native'; import type {BaseOnboardingAccountingProps} from './types'; type Integration = { - key: keyof typeof CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY; - iconName: 'QBOCircle' | 'QBDSquare' | 'XeroCircle' | 'NetSuiteSquare' | 'IntacctSquare' | 'SapSquare' | 'OracleSquare' | 'MicrosoftDynamicsSquare'; + key: keyof typeof CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY | typeof CONST.POLICY.CONNECTIONS.ACCOUNTING_INTEGRATION_ALIASES.INTUIT_ENTERPRISE_SUITE; + iconName: + | 'QBOCircle' + | 'IntuitSquare' + | 'QBDSquare' + | 'XeroCircle' + | 'NetSuiteSquare' + | 'IntacctSquare' + | 'CertiniaSquare' + | 'RilletSquare' + | 'SapSquare' + | 'OracleSquare' + | 'MicrosoftDynamicsSquare'; translationKey: TranslationPaths; }; type AccountingOptionKey = Integration['key'] | 'other'; +// Ordered to match the design mocks, which lay the tiles out row by row in three columns on wide layouts and two on +// narrow ones. DualEntry and Campfire are connections we support but deliberately do not offer here yet. const integrations: Integration[] = [ { key: 'quickbooksOnline', iconName: 'QBOCircle', translationKey: 'workspace.accounting.qbo', }, + { + key: CONST.POLICY.CONNECTIONS.ACCOUNTING_INTEGRATION_ALIASES.INTUIT_ENTERPRISE_SUITE, + iconName: 'IntuitSquare', + translationKey: 'workspace.accounting.intuitEnterpriseSuite', + }, { key: 'quickbooksDesktop', iconName: 'QBDSquare', @@ -80,6 +97,17 @@ const integrations: Integration[] = [ iconName: 'IntacctSquare', translationKey: 'workspace.accounting.intacct', }, + { + key: 'financialforce', + iconName: 'CertiniaSquare', + // Certinia has no `workspace.accounting` entry, but its connection page already translates the brand name. + translationKey: 'workspace.certinia.title', + }, + { + key: 'rillet', + iconName: 'RilletSquare', + translationKey: 'workspace.accounting.rillet', + }, { key: 'sap', iconName: 'SapSquare', @@ -107,20 +135,22 @@ type OnboardingListItem = ListItem & { function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccountingProps) { const styles = useThemeStyles(); - const theme = useTheme(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const expensifyIcons = useMemoizedLazyExpensifyIcons([ - 'Connect', 'QBOCircle', + 'IntuitSquare', 'QBDSquare', 'XeroCircle', 'NetSuiteSquare', 'IntacctSquare', + 'CertiniaSquare', + 'RilletSquare', 'SapSquare', 'OracleSquare', 'MicrosoftDynamicsSquare', ]); + const illustrations = useMemoizedLazyIllustrations(['Pencil']); // We need to use isSmallScreenWidth, see navigateAfterOnboarding function comment // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {onboardingIsMediumOrLargerScreenWidth, isSmallScreenWidth, isInLandscapeMode} = useResponsiveLayout(); @@ -167,7 +197,7 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount src={icon} width={variables.iconSizeExtraLarge} height={variables.iconSizeExtraLarge} - additionalStyles={[StyleUtils.getAvatarBorderStyle(CONST.AVATAR_SIZE.DEFAULT, CONST.AVATAR_SHAPE.CIRCLE), styles.mr3]} + additionalStyles={[StyleUtils.getAvatarBorderStyle(CONST.AVATAR_SIZE.DEFAULT, CONST.AVATAR_SHAPE.CIRCLE)]} /> ), isSelected: selectedIntegration === integration.key, @@ -179,11 +209,9 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount text: translate('workspace.accounting.other'), leftElement: ( ), isSelected: isOtherSelected, @@ -242,22 +270,24 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount sentryLabel={CONST.SENTRY_LABEL.ONBOARDING.ACCOUNTING_SELECT_INTEGRATION} accessible={false} hoverStyle={styles.hoveredComponentBG} - style={[styles.onboardingAccountingItem, isSmallScreenWidth && styles.flexBasis100]} + style={[ + styles.onboardingAccountingItem, + isSmallScreenWidth ? styles.onboardingAccountingItemNarrow : styles.onboardingAccountingItemWide, + !!item.isSelected && styles.onboardingAccountingItemSelected, + ]} > - handleIntegrationSelect(item.keyForList)} - accessibilityLabel={item.text} - style={[styles.flexRowReverse]} - wrapperStyle={[styles.ml0]} - labelElement={ - - {item.leftElement} - {item.text} - - } - shouldBlendOpacity + accessibilityLabel={item.text ?? ''} + containerBorderRadius={variables.componentBorderRadiusSmall} + style={styles.onboardingAccountingItemRadioButton} /> + + {item.leftElement} + {item.text} + ); } @@ -310,16 +340,7 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount onContentSizeChange={handleContentSizeChange} keyboardShouldPersistTaps="handled" > - - {accountingOptions.map(renderOption)} - {/* Keep Other from expanding across the empty second column on wide layouts. */} - {!isSmallScreenWidth && ( - - )} - + {accountingOptions.map(renderOption)} {isOtherSelected && ( backgroundColor: theme.highlightBG, }, - onboardingSmallIcon: { - padding: 10, - }, - sidebarStatusAvatarContainer: { height: variables.avatarSizeMedium, width: variables.avatarSizeMedium, @@ -5681,8 +5677,28 @@ const staticStyles = (theme: ThemeColors) => paddingVertical: 20, flexGrow: 1, flexShrink: 1, + minHeight: 144, + }, - flexBasis: '35%', + // The basis and maxWidth handle the gap between columns and keep items aligned when the scrollbar appears + onboardingAccountingItemWide: { + flexBasis: '32%', + maxWidth: '32%', + }, + + onboardingAccountingItemNarrow: { + flexBasis: '48.5%', + maxWidth: '48.5%', + }, + + onboardingAccountingItemSelected: { + backgroundColor: theme.reactionActiveBackground, + }, + + onboardingAccountingItemRadioButton: { + position: 'absolute', + top: 16, + left: 16, }, onboardingInterestedFeaturesItem: { diff --git a/tests/ui/OnboardingAccountingAndInterestedFeatures.tsx b/tests/ui/OnboardingAccountingAndInterestedFeatures.tsx index 49800908bd4d..916bbe8d3f9c 100644 --- a/tests/ui/OnboardingAccountingAndInterestedFeatures.tsx +++ b/tests/ui/OnboardingAccountingAndInterestedFeatures.tsx @@ -17,6 +17,7 @@ import BaseOnboardingAccounting from '@pages/OnboardingAccounting/BaseOnboarding import BaseOnboardingInterestedFeatures from '@pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import type {OnboardingModalNavigatorParamList} from '@src/libs/Navigation/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -153,14 +154,50 @@ describe('Onboarding interested features and accounting pages', () => { expect(navigate).not.toHaveBeenCalledWith(ROUTES.ONBOARDING_ACCOUNTING.getRoute()); }); - it('keeps Other half-width, auto-focuses its input, and completes with a trimmed integration name', async () => { + it('renders every accounting tile from the mocks', async () => { + renderAccountingPage(); + + await waitForBatchedUpdatesWithAct(); + const expectedTiles: TranslationPaths[] = [ + 'workspace.accounting.qbo', + 'workspace.accounting.intuitEnterpriseSuite', + 'workspace.accounting.qbd', + 'workspace.accounting.xero', + 'workspace.accounting.netsuite', + 'workspace.accounting.intacct', + 'workspace.certinia.title', + 'workspace.accounting.rillet', + 'workspace.accounting.sap', + 'workspace.accounting.oracle', + 'workspace.accounting.microsoftDynamics', + 'workspace.accounting.other', + ]; + for (const translationKey of expectedTiles) { + expect(screen.getByText(TestHelper.translateLocal(translationKey))).toBeOnTheScreen(); + } + }); + + it('completes onboarding with an integration that was previously missing from this screen', async () => { + renderAccountingPage(); + + await waitForBatchedUpdatesWithAct(); + fireEvent.press(screen.getByText(TestHelper.translateLocal('workspace.certinia.title'))); + fireEvent.press(screen.getByText(TestHelper.translateLocal('common.continue'))); + + await waitFor(() => { + expect(mockCompleteOnboardingFlow).toHaveBeenCalledWith({ + featuresMap: expect.arrayContaining([{id: CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED, enabled: true, enabledByDefault: true}]), + userReportedIntegration: CONST.POLICY.CONNECTIONS.NAME.CERTINIA, + userReportedIntegrationName: undefined, + }); + }); + }); + + it('auto-focuses the Other input and completes with a trimmed integration name', async () => { const scrollToEndSpy = jest.spyOn(ScrollView.prototype, 'scrollToEnd'); const renderResult = renderAccountingPage(); await waitForBatchedUpdatesWithAct(); - expect(screen.queryByText(TestHelper.translateLocal('onboarding.accounting.none'))).not.toBeOnTheScreen(); - expect(screen.getByTestId('onboarding-accounting-wide-layout-spacer')).toHaveStyle({backgroundColor: 'transparent', flexBasis: '35%', flexGrow: 1}); - fireEvent.press(screen.getByText(TestHelper.translateLocal('workspace.accounting.other'))); const otherAccountingSoftwareLabel = TestHelper.translateLocal('onboarding.accounting.otherAccountingSoftware'); const otherAccountingSoftwareInput = screen.getByLabelText(otherAccountingSoftwareLabel);