diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 67b9855413d6..4e03ce769e16 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -655,6 +655,15 @@ const CONST = { }, BANK_INFO_STEP_ACCOUNT_HOLDER_KEY_PREFIX: 'accountHolder', }, + ENABLE_GLOBAL_REIMBURSEMENTS: { + STEP_NAMES: ['1', '2', '3'], + STEP: { + BUSINESS_INFO: 'BusinessInfoStep', + AGREEMENTS: 'AgreementsStep', + DOCUSIGN: 'DocusignStep', + }, + ALLOWED_FILE_TYPES: ['pdf', 'jpg', 'jpeg', 'png'], + }, INCORPORATION_TYPES: { LLC: 'LLC', CORPORATION: 'Corp', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8443812a2a06..60f25aa90ce1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -871,6 +871,8 @@ const ONYXKEYS = { MERGE_ACCOUNT_DETAILS_FORM_DRAFT: 'mergeAccountDetailsFormDraft', WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm', WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft', + ENABLE_GLOBAL_REIMBURSEMENTS: 'enableGlobalReimbursementsForm', + ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT: 'enableGlobalReimbursementsFormDraft', }, DERIVED: { REPORT_ATTRIBUTES: 'reportAttributes', @@ -982,6 +984,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.MERGE_ACCOUNT_DETAILS_FORM]: FormTypes.MergeAccountDetailsForm; [ONYXKEYS.FORMS.INTERNATIONAL_BANK_ACCOUNT_FORM]: FormTypes.InternationalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm; + [ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS]: FormTypes.EnableGlobalReimbursementsForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 013e525a5b7f..67fbfa10d16c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -285,6 +285,10 @@ const ROUTES = { }, SETTINGS_ADD_US_BANK_ACCOUNT: 'settings/wallet/add-us-bank-account', SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', + SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS: { + route: 'settings/wallet/:bankAccountID/enable-global-reimbursements', + getRoute: (bankAccountID: number | undefined) => `settings/wallet/${bankAccountID}/enable-global-reimbursements` as const, + }, SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS: { route: 'settings/wallet/card/:domain/digital-details/update-address', getRoute: (domain: string) => `settings/wallet/card/${domain}/digital-details/update-address` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9a7c01be8fbb..fd0511de2efb 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -136,6 +136,7 @@ const SCREENS = { REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud', REPORT_VIRTUAL_CARD_FRAUD_CONFIRMATION: 'Settings_Wallet_ReportVirtualCardFraudConfirmation', CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', + ENABLE_GLOBAL_REIMBURSEMENTS: 'Settings_Wallet_Enable_Global_Reimbursements', }, EXIT_SURVEY: { diff --git a/src/components/SubStepForms/AgreementsFullStep/index.tsx b/src/components/SubStepForms/AgreementsFullStep/index.tsx new file mode 100644 index 000000000000..ab8fe9fc445b --- /dev/null +++ b/src/components/SubStepForms/AgreementsFullStep/index.tsx @@ -0,0 +1,126 @@ +import type {ComponentType} from 'react'; +import React from 'react'; +import type {FormOnyxKeys} from '@components/Form/types'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useSubStep from '@hooks/useSubStep'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import {clearErrors} from '@userActions/FormActions'; +import type {OnyxFormValuesMapping} from '@src/ONYXKEYS'; +import Confirmation from './subSteps/Confirmation'; + +type AgreementsFullStepProps = { + /** Default values for inputs */ + defaultValues: Partial, boolean>>; + + /** The ID of the form */ + formID: TFormID; + + /** Input IDs for field in the form */ + inputIDs: { + provideTruthfulInformation: FormOnyxKeys; + agreeToTermsAndConditions: FormOnyxKeys; + consentToPrivacyNotice: FormOnyxKeys; + authorizedToBindClientToAgreement: FormOnyxKeys; + }; + + /** Indicates that action is being processed */ + isLoading: boolean; + + /** Handles back button press */ + onBackButtonPress: () => void; + + /** Handles submit button press */ + onSubmit: () => void; + + /** Currency of related account */ + currency: string; + + /** Array of step names */ + stepNames?: readonly string[]; + + /** Index of currently active step in header */ + startStepIndex: number; +}; + +type AgreementsFullStepSubStepProps = SubStepProps & { + /** Default values for inputs */ + defaultValues: AgreementsFullStepProps['defaultValues']; + + /** The ID of the form */ + formID: TFormID; + + /** Input IDs for field in the form */ + inputIDs: AgreementsFullStepProps['inputIDs']; + + /** Indicates that action is being processed */ + isLoading: boolean; + + /** Currency of related account */ + currency: string; +}; + +function AgreementsFullStep({ + defaultValues, + formID, + inputIDs, + isLoading, + onBackButtonPress, + onSubmit, + currency, + stepNames, + startStepIndex, +}: AgreementsFullStepProps) { + const {translate} = useLocalize(); + + const bodyContent: Array>> = [Confirmation]; + + const { + componentToRender: SubStep, + isEditing, + screenIndex, + nextScreen, + prevScreen, + moveTo, + goToTheLastStep, + } = useSubStep>({bodyContent, startFrom: 0, onFinished: onSubmit}); + + const handleBackButtonPress = () => { + clearErrors(formID); + if (isEditing) { + goToTheLastStep(); + return; + } + + if (screenIndex === 0) { + onBackButtonPress(); + } else { + prevScreen(); + } + }; + + return ( + + + + ); +} + +AgreementsFullStep.displayName = 'AgreementsFullStep'; + +export default AgreementsFullStep; diff --git a/src/pages/ReimbursementAccount/NonUSD/Agreements/subSteps/Confirmation.tsx b/src/components/SubStepForms/AgreementsFullStep/subSteps/Confirmation.tsx similarity index 51% rename from src/pages/ReimbursementAccount/NonUSD/Agreements/subSteps/Confirmation.tsx rename to src/components/SubStepForms/AgreementsFullStep/subSteps/Confirmation.tsx index fff40eeb52cd..d9d37d23230e 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Agreements/subSteps/Confirmation.tsx +++ b/src/components/SubStepForms/AgreementsFullStep/subSteps/Confirmation.tsx @@ -2,21 +2,15 @@ import React, {useCallback, useMemo} from 'react'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import type {FormInputErrors, FormOnyxKeys, FormOnyxValues} from '@components/Form/types'; import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import {getFieldRequiredErrors, isRequiredFulfilled} from '@libs/ValidationUtils'; import requiresDocusignStep from '@pages/ReimbursementAccount/NonUSD/utils/requiresDocusignStep'; -import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; -import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; - -const {AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT, PROVIDE_TRUTHFUL_INFORMATION, AGREE_TO_TERMS_AND_CONDITIONS, CONSENT_TO_PRIVACY_NOTICE} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; -const STEP_FIELDS = [AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT, PROVIDE_TRUTHFUL_INFORMATION, AGREE_TO_TERMS_AND_CONDITIONS, CONSENT_TO_PRIVACY_NOTICE]; +import type {OnyxFormValuesMapping} from '@src/ONYXKEYS'; function IsAuthorizedToUseBankAccountLabel() { const {translate} = useLocalize(); @@ -38,95 +32,110 @@ function ConsentToPrivacyNoticeLabel() { return ; } -const INPUT_KEYS = { - PROVIDE_TRUTHFUL_INFORMATION: INPUT_IDS.ADDITIONAL_DATA.CORPAY.PROVIDE_TRUTHFUL_INFORMATION, - AGREE_TO_TERMS_AND_CONDITIONS: INPUT_IDS.ADDITIONAL_DATA.CORPAY.AGREE_TO_TERMS_AND_CONDITIONS, - CONSENT_TO_PRIVACY_NOTICE: INPUT_IDS.ADDITIONAL_DATA.CORPAY.CONSENT_TO_PRIVACY_NOTICE, - AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT: INPUT_IDS.ADDITIONAL_DATA.CORPAY.AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT, -}; +type ConfirmationProps = SubStepProps & { + /** Default values for inputs */ + defaultValues: Partial, boolean>>; + + /** The ID of the form */ + formID: TFormID; + + /** Input IDs for field in the form */ + inputIDs: { + provideTruthfulInformation: FormOnyxKeys; + agreeToTermsAndConditions: FormOnyxKeys; + consentToPrivacyNotice: FormOnyxKeys; + authorizedToBindClientToAgreement: FormOnyxKeys; + }; -type ConfirmationProps = SubStepProps & { - policyCurrency: string | undefined; + /** Indicates that action is being processed */ + isLoading: boolean; + + /** Currency of related account */ + currency: string; }; -function Confirmation({onNext, policyCurrency}: ConfirmationProps) { +function Confirmation({defaultValues, formID, inputIDs, isLoading, onNext, currency}: ConfirmationProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); - const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, {canBeMissing: false}); - const agreementsStepValues = useMemo(() => getSubStepValues(INPUT_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); - const isDocusignStepRequired = requiresDocusignStep(policyCurrency); + const isDocusignStepRequired = requiresDocusignStep(currency); + + const stepFields = useMemo( + () => [inputIDs.authorizedToBindClientToAgreement, inputIDs.provideTruthfulInformation, inputIDs.agreeToTermsAndConditions, inputIDs.consentToPrivacyNotice], + [inputIDs], + ); const validate = useCallback( - (values: FormOnyxValues): FormInputErrors => { - const errors = getFieldRequiredErrors(values, STEP_FIELDS); + (values: FormOnyxValues): FormInputErrors => { + const errors = getFieldRequiredErrors(values, stepFields); - if (!isRequiredFulfilled(values[AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT])) { - errors[AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT] = translate('agreementsStep.error.authorized'); + if (!isRequiredFulfilled(values[inputIDs.authorizedToBindClientToAgreement] as string)) { + errors[inputIDs.authorizedToBindClientToAgreement] = translate('agreementsStep.error.authorized'); } - if (!isRequiredFulfilled(values[PROVIDE_TRUTHFUL_INFORMATION])) { - errors[PROVIDE_TRUTHFUL_INFORMATION] = translate('agreementsStep.error.certify'); + if (!isRequiredFulfilled(values[inputIDs.provideTruthfulInformation] as string)) { + errors[inputIDs.provideTruthfulInformation] = translate('agreementsStep.error.certify'); } - if (!isRequiredFulfilled(values[AGREE_TO_TERMS_AND_CONDITIONS])) { - errors[AGREE_TO_TERMS_AND_CONDITIONS] = translate('common.error.acceptTerms'); + if (!isRequiredFulfilled(values[inputIDs.agreeToTermsAndConditions] as string)) { + errors[inputIDs.agreeToTermsAndConditions] = translate('common.error.acceptTerms'); } - if (!isRequiredFulfilled(values[CONSENT_TO_PRIVACY_NOTICE])) { - errors[CONSENT_TO_PRIVACY_NOTICE] = translate('agreementsStep.error.consent'); + if (!isRequiredFulfilled(values[inputIDs.consentToPrivacyNotice] as string)) { + errors[inputIDs.consentToPrivacyNotice] = translate('agreementsStep.error.consent'); } return errors; }, - [translate], + [inputIDs.agreeToTermsAndConditions, inputIDs.authorizedToBindClientToAgreement, inputIDs.consentToPrivacyNotice, inputIDs.provideTruthfulInformation, stepFields, translate], ); return ( {translate('agreementsStep.pleaseConfirm')} {!isDocusignStepRequired && {translate('agreementsStep.regulationRequiresUs')}} diff --git a/src/components/SubStepForms/DocusignFullStep/index.tsx b/src/components/SubStepForms/DocusignFullStep/index.tsx new file mode 100644 index 000000000000..36a7a31458f0 --- /dev/null +++ b/src/components/SubStepForms/DocusignFullStep/index.tsx @@ -0,0 +1,121 @@ +import type {ComponentType} from 'react'; +import React from 'react'; +import type {FormOnyxKeys} from '@components/Form/types'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useSubStep from '@hooks/useSubStep'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; +import {clearErrors} from '@userActions/FormActions'; +import type {OnyxFormValuesMapping} from '@src/ONYXKEYS'; +import UploadPowerform from './subSteps/UploadPowerform'; + +type DocusignFullStepProps = { + /** Default value for file upload input */ + defaultValue: FileObject[]; + + /** The ID of the form */ + formID: TFormID; + + /** ID of the input in the form */ + inputID: FormOnyxKeys; + + /** Indicates that action is being processed */ + isLoading: boolean; + + /** Handles back button press */ + onBackButtonPress: () => void; + + /** Handles submit button press */ + onSubmit: () => void; + + /** Currency of related account */ + currency: string; + + /** Array of step names */ + stepNames?: readonly string[]; + + /** Index of currently active step in header */ + startStepIndex: number; +}; + +type DocusignFullStepStepProps = SubStepProps & { + defaultValue: FileObject[]; + + /** The ID of the form */ + formID: TFormID; + + /** ID of the input in the form */ + inputID: FormOnyxKeys; + + /** Indicates that action is being processed */ + isLoading: boolean; + + /** Currency of related account */ + currency: string; +}; + +function DocusignFullStep({ + defaultValue, + formID, + inputID, + isLoading, + onBackButtonPress, + onSubmit, + currency, + startStepIndex, + stepNames, +}: DocusignFullStepProps) { + const {translate} = useLocalize(); + + const bodyContent: Array>> = [UploadPowerform]; + + const { + componentToRender: SubStep, + isEditing, + screenIndex, + nextScreen, + prevScreen, + moveTo, + goToTheLastStep, + } = useSubStep>({bodyContent, startFrom: 0, onFinished: onSubmit}); + + const handleBackButtonPress = () => { + clearErrors(formID); + if (isEditing) { + goToTheLastStep(); + return; + } + + if (screenIndex === 0) { + onBackButtonPress(); + } else { + prevScreen(); + } + }; + + return ( + + + + ); +} + +DocusignFullStep.displayName = 'DocusignFullStep'; + +export default DocusignFullStep; diff --git a/src/pages/ReimbursementAccount/NonUSD/Docusign/subSteps/UploadPowerform.tsx b/src/components/SubStepForms/DocusignFullStep/subSteps/UploadPowerform.tsx similarity index 54% rename from src/pages/ReimbursementAccount/NonUSD/Docusign/subSteps/UploadPowerform.tsx rename to src/components/SubStepForms/DocusignFullStep/subSteps/UploadPowerform.tsx index f2905f5c3213..43b57aae5798 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Docusign/subSteps/UploadPowerform.tsx +++ b/src/components/SubStepForms/DocusignFullStep/subSteps/UploadPowerform.tsx @@ -2,84 +2,88 @@ import React, {useCallback, useState} from 'react'; import Button from '@components/Button'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import type {FormInputErrors, FormOnyxKeys, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import UploadFile from '@components/UploadFile'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import mapCurrencyToCountry from '@libs/mapCurrencyToCountry'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; import {clearErrorFields, setDraftValues, setErrorFields} from '@userActions/FormActions'; import {openLink} from '@userActions/Link'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import type {OnyxFormValuesMapping} from '@src/ONYXKEYS'; -type UploadPowerformProps = SubStepProps; +type UploadPowerformProps = SubStepProps & { + /** Default value for file upload input */ + defaultValue: FileObject[]; -const {ACH_AUTHORIZATION_FORM} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; -const STEP_FIELDS = [ACH_AUTHORIZATION_FORM]; + /** The ID of the form */ + formID: TFormID; -function UploadPowerform({onNext, isEditing}: UploadPowerformProps) { + /** ID of the input in the form */ + inputID: FormOnyxKeys; + + /** Indicates that action is being processed */ + isLoading: boolean; + + /** Currency of related account */ + currency: string; +}; +function UploadPowerform({defaultValue, formID, inputID, isLoading, onNext, currency}: UploadPowerformProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {environmentURL} = useEnvironment(); - const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); - const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, {canBeMissing: false}); - const countryStepCountryValue = reimbursementAccount?.achData?.[INPUT_IDS.ADDITIONAL_DATA.COUNTRY] ?? ''; - - const defaultValue: FileObject[] = Array.isArray(reimbursementAccountDraft?.[ACH_AUTHORIZATION_FORM]) ? (reimbursementAccountDraft?.[ACH_AUTHORIZATION_FORM] ?? []) : []; - const [uploadedFiles, setUploadedFiles] = useState(defaultValue); - const validate = useCallback((values: FormOnyxValues): FormInputErrors => { - return getFieldRequiredErrors(values, STEP_FIELDS); - }, []); - - const handleSubmit = useReimbursementAccountStepFormSubmit({ - fieldIds: STEP_FIELDS, - onNext, - shouldSaveDraft: isEditing, - }); + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + return getFieldRequiredErrors(values, [inputID]); + }, + [inputID], + ); const handleSelectFile = (files: FileObject[]) => { - setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[ACH_AUTHORIZATION_FORM]: [...uploadedFiles, ...files]}); + setDraftValues(formID, {[inputID]: [...uploadedFiles, ...files]}); setUploadedFiles((prev) => [...prev, ...files]); }; const handleRemoveFile = (fileName: string) => { const newUploadedFiles = uploadedFiles.filter((file) => file.name !== fileName); - setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[ACH_AUTHORIZATION_FORM]: newUploadedFiles}); + setDraftValues(formID, {[inputID]: newUploadedFiles}); setUploadedFiles(newUploadedFiles); }; const setUploadError = (error: string) => { if (!error) { - clearErrorFields(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM); + clearErrorFields(formID); return; } - setErrorFields(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[ACH_AUTHORIZATION_FORM]: {onUpload: error}}); + setErrorFields(formID, {[inputID]: {onUpload: error}}); }; + const country = mapCurrencyToCountry(currency ?? ''); + return ( - {countryStepCountryValue === CONST.COUNTRY.CA && {translate('docusignStep.pleaseComplete')}} - {countryStepCountryValue === CONST.COUNTRY.AU && ( + {(country === CONST.COUNTRY.CA || country === CONST.COUNTRY.US) && ( + {translate('docusignStep.pleaseComplete')} + )} + {country === CONST.COUNTRY.AU && ( <> {translate('docusignStep.pleaseCompleteTheBusinessAccount')} {translate('docusignStep.pleaseCompleteTheDirect')} @@ -90,12 +94,14 @@ function UploadPowerform({onNext, isEditing}: UploadPowerformProps) { large style={[styles.w100, styles.mb15]} onPress={() => { - openLink(CONST.DOCUSIGN_POWERFORM_LINK[countryStepCountryValue as 'CA' | 'AU'], environmentURL); + openLink(CONST.DOCUSIGN_POWERFORM_LINK[country as 'CA' | 'AU' | 'US'], environmentURL); }} text={translate('docusignStep.takeMeTo')} /> - {countryStepCountryValue === CONST.COUNTRY.CA && {translate('docusignStep.uploadAdditional')}} - {countryStepCountryValue === CONST.COUNTRY.AU && {translate('docusignStep.pleaseUploadTheDirect')}} + {(country === CONST.COUNTRY.CA || country === CONST.COUNTRY.US) && ( + {translate('docusignStep.uploadAdditional')} + )} + {country === CONST.COUNTRY.AU && {translate('docusignStep.pleaseUploadTheDirect')}} {translate('docusignStep.pleaseUpload')} { setUploadError(error); }} diff --git a/src/components/SubStepForms/PushRowFieldsStep.tsx b/src/components/SubStepForms/PushRowFieldsStep.tsx new file mode 100644 index 000000000000..74098e1a1bab --- /dev/null +++ b/src/components/SubStepForms/PushRowFieldsStep.tsx @@ -0,0 +1,76 @@ +import React, {useCallback} from 'react'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxKeys, FormOnyxValues} from '@components/Form/types'; +import PushRowWithModal from '@components/PushRowWithModal'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getFieldRequiredErrors} from '@libs/ValidationUtils'; +import type {OnyxFormValuesMapping} from '@src/ONYXKEYS'; + +type PushRowField = { + inputID: FormOnyxKeys; + defaultValue: string; + options: Record; + description: string; + modalHeaderTitle: string; + searchInputTitle: string; +}; + +type PushRowFieldsStepProps = SubStepProps & { + /** The ID of the form */ + formID: TFormID; + + /** Title of the form */ + formTitle: string; + + /** A function to call when the form is submitted */ + onSubmit: (values: FormOnyxValues) => void; + + pushRowFields: Array>; +}; + +function PushRowFieldsStep({formID, formTitle, pushRowFields, onSubmit, isEditing}: PushRowFieldsStepProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const pushRowFieldsIDs = pushRowFields.map((field) => field.inputID); + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + return getFieldRequiredErrors(values, pushRowFieldsIDs); + }, + [pushRowFieldsIDs], + ); + + return ( + + {formTitle} + {pushRowFields.map((pushRowField: PushRowField) => ( + + ))} + + ); +} + +PushRowFieldsStep.displayName = 'PushRowFieldsStep'; + +export default PushRowFieldsStep; diff --git a/src/components/SubStepForms/RegistrationNumberStep.tsx b/src/components/SubStepForms/RegistrationNumberStep.tsx new file mode 100644 index 000000000000..eea62da28bfc --- /dev/null +++ b/src/components/SubStepForms/RegistrationNumberStep.tsx @@ -0,0 +1,114 @@ +import React, {useCallback, useRef} from 'react'; +import {View} from 'react-native'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxKeys, FormOnyxValues} from '@components/Form/types'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import TextLink from '@components/TextLink'; +import useDelayedAutoFocus from '@hooks/useDelayedAutoFocus'; +import useLocalize from '@hooks/useLocalize'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getFieldRequiredErrors, isValidRegistrationNumber} from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; +import type {OnyxFormValuesMapping} from '@src/ONYXKEYS'; + +type RegistrationNumberStepProps = SubStepProps & { + /** The ID of the form */ + formID: TFormID; + + /** A function to call when the form is submitted */ + onSubmit: (values: FormOnyxValues) => void; + + /** The ID of the form input */ + inputID: FormOnyxKeys; + + /** The default values for the input */ + defaultValue: string; + + /** Country of affiliated policy */ + country: Country | ''; + + /** Whether to delay autoFocus to avoid conflicts with navigation animations */ + shouldDelayAutoFocus?: boolean; +}; + +function RegistrationNumberStep({ + formID, + onSubmit, + inputID, + defaultValue, + isEditing, + country, + shouldDelayAutoFocus = false, +}: RegistrationNumberStepProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const theme = useTheme(); + + const internalInputRef = useRef(null); + useDelayedAutoFocus(internalInputRef, shouldDelayAutoFocus); + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = getFieldRequiredErrors(values, [inputID]); + + if (values[inputID] && !isValidRegistrationNumber(values[inputID] as string, country)) { + errors[inputID] = translate('businessInfoStep.error.registrationNumber'); + } + + return errors; + }, + [country, inputID, translate], + ); + return ( + + {translate('businessInfoStep.whatsTheBusinessRegistrationNumber')} + + + + + + {translate('businessInfoStep.whatsThisNumber')} + + + + + ); +} + +RegistrationNumberStep.displayName = 'RegistrationNumberStep'; + +export default RegistrationNumberStep; diff --git a/src/hooks/useDelayedAutoFocus.ts b/src/hooks/useDelayedAutoFocus.ts new file mode 100644 index 000000000000..a3988df11cf5 --- /dev/null +++ b/src/hooks/useDelayedAutoFocus.ts @@ -0,0 +1,30 @@ +import {useFocusEffect} from '@react-navigation/native'; +import type {RefObject} from 'react'; +import {useCallback, useRef} from 'react'; +import CONST from '@src/CONST'; + +function useDelayedAutoFocus(ref: RefObject<{focus: () => void} | null>, shouldDelayAutoFocus: boolean) { + const focusTimeoutRef = useRef(null); + + useFocusEffect( + useCallback(() => { + if (!shouldDelayAutoFocus) { + return undefined; + } + + focusTimeoutRef.current = setTimeout(() => { + ref.current?.focus(); + }, CONST.ANIMATED_TRANSITION); + + return () => { + const timeout = focusTimeoutRef.current; + if (timeout) { + clearTimeout(timeout); + focusTimeoutRef.current = null; + } + }; + }, [shouldDelayAutoFocus, ref]), + ); +} + +export default useDelayedAutoFocus; diff --git a/src/hooks/useEnableGlobalReimbursementsStepFormSubmit.ts b/src/hooks/useEnableGlobalReimbursementsStepFormSubmit.ts new file mode 100644 index 000000000000..3b5d89f0630a --- /dev/null +++ b/src/hooks/useEnableGlobalReimbursementsStepFormSubmit.ts @@ -0,0 +1,27 @@ +import type {FormOnyxKeys} from '@components/Form/types'; +import type {OnyxFormKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import useStepFormSubmit from './useStepFormSubmit'; +import type {SubStepProps} from './useSubStep/types'; + +type UseEnableGlobalReimbursementsStepFormSubmit = Pick & { + formId?: OnyxFormKey; + fieldIds: Array>; + shouldSaveDraft: boolean; +}; + +/** + * Hook for handling submit method in EnterSignerInfo substeps. + * When user is in editing mode, we should save values only when user confirms the change + * @param onNext - callback + * @param fieldIds - field IDs for particular step + * @param shouldSaveDraft - if we should save draft values + */ +export default function useEnableGlobalReimbursementsStepFormSubmit({onNext, fieldIds, shouldSaveDraft}: UseEnableGlobalReimbursementsStepFormSubmit) { + return useStepFormSubmit({ + formId: ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS, + onNext, + fieldIds, + shouldSaveDraft, + }); +} diff --git a/src/languages/de.ts b/src/languages/de.ts index b880a0181fb3..74cc3febd4d5 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -655,6 +655,7 @@ const translations = { forwardTo: 'Weiterleiten an', merge: 'Zusammenführen', unstableInternetConnection: 'Instabile Internetverbindung. Bitte überprüfe dein Netzwerk und versuche es erneut.', + enableGlobalReimbursements: 'Globale Rückerstattungen aktivieren', }, supportalNoAccess: { title: 'Nicht so schnell', diff --git a/src/languages/en.ts b/src/languages/en.ts index 784f0a942f25..cdc84f61aa3d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -646,6 +646,7 @@ const translations = { forwardTo: 'Forward to', merge: 'Merge', unstableInternetConnection: 'Unstable internet connection. Please check your network and try again.', + enableGlobalReimbursements: 'Enable Global Reimbursements', }, supportalNoAccess: { title: 'Not so fast', diff --git a/src/languages/es.ts b/src/languages/es.ts index a030a7a67dad..8afbd0af0270 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -636,6 +636,7 @@ const translations = { forwardTo: 'Reenviar a', merge: 'Fusionar', unstableInternetConnection: 'Conexión a internet inestable. Por favor, revisa tu red e inténtalo de nuevo.', + enableGlobalReimbursements: 'Habilitar Reembolsos Globales', }, supportalNoAccess: { title: 'No tan rápido', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 5e5a088e6026..7c87b5da22fd 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -655,6 +655,7 @@ const translations = { forwardTo: 'Transférer à', merge: 'Fusionner', unstableInternetConnection: 'Connexion Internet instable. Veuillez vérifier votre réseau et réessayer.', + enableGlobalReimbursements: 'Activer les remboursements globaux', }, supportalNoAccess: { title: 'Pas si vite', diff --git a/src/languages/it.ts b/src/languages/it.ts index b15c5a040492..16e06ff9a682 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -655,6 +655,7 @@ const translations = { forwardTo: 'Inoltra a', merge: 'Unisci', unstableInternetConnection: 'Connessione Internet instabile. Controlla la tua rete e riprova.', + enableGlobalReimbursements: 'Abilita i rimborsi globali', }, supportalNoAccess: { title: 'Non così in fretta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 1d5117543387..d2b679df2ad8 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -655,6 +655,7 @@ const translations = { forwardTo: '転送先', merge: 'マージ', unstableInternetConnection: 'インターネット接続が不安定です。ネットワークを確認してもう一度お試しください。', + enableGlobalReimbursements: 'グローバル払い戻しを有効にする', }, supportalNoAccess: { title: 'ちょっと待ってください', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e8b9febe5710..491865b348b4 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -654,6 +654,7 @@ const translations = { forwardTo: 'Doorsturen naar', merge: 'Samenvoegen', unstableInternetConnection: 'Onstabiele internetverbinding. Controleer je netwerk en probeer het opnieuw.', + enableGlobalReimbursements: 'Wereldwijde terugbetalingen inschakelen', }, supportalNoAccess: { title: 'Niet zo snel', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index a53863d32010..17fb784af800 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -655,6 +655,7 @@ const translations = { forwardTo: 'Przekaż do', merge: 'Scal', unstableInternetConnection: 'Niestabilne połączenie internetowe. Sprawdź swoją sieć i spróbuj ponownie.', + enableGlobalReimbursements: 'Włącz globalne zwroty', }, supportalNoAccess: { title: 'Nie tak szybko', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 2ea6ce467af7..7780f2c7fe4a 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -655,6 +655,7 @@ const translations = { forwardTo: 'Encaminhar para', merge: 'Mesclar', unstableInternetConnection: 'Conexão de internet instável. Verifique sua rede e tente novamente.', + enableGlobalReimbursements: 'Ativar reembolsos globais', }, supportalNoAccess: { title: 'Não tão rápido', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 0f6cf59103f8..25d6803910c5 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -654,6 +654,7 @@ const translations = { forwardTo: '转发到', merge: '合并', unstableInternetConnection: '互联网连接不稳定。请检查你的网络,然后重试。', + enableGlobalReimbursements: '启用全球报销', }, supportalNoAccess: { title: '慢一点', diff --git a/src/libs/API/parameters/EnableGlobalReimbursementsForUSDBankAccountParams.ts b/src/libs/API/parameters/EnableGlobalReimbursementsForUSDBankAccountParams.ts new file mode 100644 index 000000000000..2022cc094128 --- /dev/null +++ b/src/libs/API/parameters/EnableGlobalReimbursementsForUSDBankAccountParams.ts @@ -0,0 +1,9 @@ +import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; + +type EnableGlobalReimbursementsForUSDBankAccountParams = { + inputs: string; + achAuthorizationForm?: FileObject; + bankAccountID: number; +}; + +export default EnableGlobalReimbursementsForUSDBankAccountParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 647aa4c3151d..31f7b757a15b 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -420,3 +420,4 @@ export type {default as OpenUnreportedExpensesPageParams} from './OpenUnreported export type {default as VerifyTestDriveRecipientParams} from './VerifyTestDriveRecipientParams'; export type {default as ExportSearchWithTemplateParams} from './ExportSearchWithTemplateParams'; export type {default as AssignReportToMeParams} from './AssignReportToMeParams'; +export type {default as EnableGlobalReimbursementsForUSDBankAccountParams} from './EnableGlobalReimbursementsForUSDBankAccountParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index accd91e64516..af6a12e640d0 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -495,6 +495,7 @@ const WRITE_COMMANDS = { PAY_AND_DOWNGRADE: 'PayAndDowngrade', COMPLETE_CONCIERGE_CALL: 'CompleteConciergeCall', FINISH_CORPAY_BANK_ACCOUNT_ONBOARDING: 'FinishCorpayBankAccountOnboarding', + ENABLE_GLOBAL_REIMBURSEMENTS_FOR_USD_BANK_ACCOUNT: 'EnableGlobalReimbursementsForUSDBankAccount', REOPEN_REPORT: 'ReopenReport', TRAVEL_SIGNUP_REQUEST: 'RequestTravelAccess', DELETE_VACATION_DELEGATE: 'DeleteVacationDelegate', @@ -861,6 +862,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.COMPLETE_CONCIERGE_CALL]: Parameters.CompleteConciergeCallParams; [WRITE_COMMANDS.FINISH_CORPAY_BANK_ACCOUNT_ONBOARDING]: Parameters.FinishCorpayBankAccountOnboardingParams; [WRITE_COMMANDS.DELETE_VACATION_DELEGATE]: null; + [WRITE_COMMANDS.ENABLE_GLOBAL_REIMBURSEMENTS_FOR_USD_BANK_ACCOUNT]: Parameters.EnableGlobalReimbursementsForUSDBankAccountParams; [WRITE_COMMANDS.REOPEN_REPORT]: Parameters.ReopenReportParams; [WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 09d14e538af6..4384f1421370 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -303,6 +303,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/TransferBalancePage').default, [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: () => require('../../../../pages/settings/Wallet/ChooseTransferAccountPage').default, [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: () => require('../../../../pages/EnablePayments/EnablePayments').default, + [SCREENS.SETTINGS.WALLET.ENABLE_GLOBAL_REIMBURSEMENTS]: () => require('../../../../pages/settings/Wallet/EnableGlobalReimbursements').default, [SCREENS.SETTINGS.ADD_DEBIT_CARD]: () => require('../../../../pages/settings/Wallet/AddDebitCardPage').default, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: () => require('../../../../pages/settings/Wallet/InternationalDepositAccount').default, [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index 9044c9541800..7b7e766a364b 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -34,6 +34,7 @@ const SETTINGS_TO_RHP: Partial['config'] = { path: ROUTES.SETTINGS_ENABLE_PAYMENTS, exact: true, }, + [SCREENS.SETTINGS.WALLET.ENABLE_GLOBAL_REIMBURSEMENTS]: { + path: ROUTES.SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS.route, + exact: true, + }, [SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: { path: ROUTES.SETTINGS_WALLET_TRANSFER_BALANCE, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index bbea1b3c3b7a..9d97d9e0db98 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -163,6 +163,9 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: undefined; [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: undefined; [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: undefined; + [SCREENS.SETTINGS.WALLET.ENABLE_GLOBAL_REIMBURSEMENTS]: { + bankAccountID: string; + }; [SCREENS.SETTINGS.ADD_DEBIT_CARD]: undefined; [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: undefined; [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT]: undefined; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 0f511b16233f..b98a6ebe952c 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -11,7 +11,7 @@ import type {Report, TaxRates} from '@src/types/onyx'; import {getMonthFromExpirationDateString, getYearFromExpirationDateString} from './CardUtils'; import DateUtils from './DateUtils'; import {translateLocal} from './Localize'; -import {appendCountryCode, getPhoneNumberWithoutSpecialChars} from './LoginUtils'; +import {getPhoneNumberWithoutSpecialChars} from './LoginUtils'; import {parsePhoneNumber} from './PhoneNumber'; import StringUtils from './StringUtils'; @@ -542,8 +542,7 @@ function isValidEmail(email: string): boolean { * @param phoneNumber */ function isValidPhoneInternational(phoneNumber: string): boolean { - const phoneNumberWithCountryCode = appendCountryCode(phoneNumber); - const parsedPhoneNumber = parsePhoneNumber(phoneNumberWithCountryCode); + const parsedPhoneNumber = parsePhoneNumber(phoneNumber); return parsedPhoneNumber.possible && Str.isValidE164Phone(parsedPhoneNumber.number?.e164 ?? ''); } diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index f9c461b05805..2dcf2da26e01 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -8,6 +8,7 @@ import type { BankAccountHandlePlaidErrorParams, ConnectBankAccountParams, DeletePaymentBankAccountParams, + EnableGlobalReimbursementsForUSDBankAccountParams, FinishCorpayBankAccountOnboardingParams, OpenReimbursementAccountPageParams, SaveCorpayOnboardingBeneficialOwnerParams, @@ -790,6 +791,48 @@ function finishCorpayBankAccountOnboarding(parameters: FinishCorpayBankAccountOn return API.write(WRITE_COMMANDS.FINISH_CORPAY_BANK_ACCOUNT_ONBOARDING, parameters, onyxData); } +function enableGlobalReimbursementsForUSDBankAccount(parameters: EnableGlobalReimbursementsForUSDBankAccountParams) { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS, + value: { + isEnablingGlobalReimbursements: true, + errors: null, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS, + value: { + isEnablingGlobalReimbursements: false, + isSuccess: true, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS, + value: { + isEnablingGlobalReimbursements: false, + isSuccess: false, + errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + ], + }; + + return API.write(WRITE_COMMANDS.ENABLE_GLOBAL_REIMBURSEMENTS_FOR_USD_BANK_ACCOUNT, parameters, onyxData); +} + +function clearEnableGlobalReimbursementsForUSDBankAccount() { + Onyx.merge(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS, {isSuccess: null, isEnablingGlobalReimbursements: null}); +} + function clearCorpayBankAccountFields() { Onyx.set(ONYXKEYS.CORPAY_FIELDS, null); } @@ -1145,4 +1188,6 @@ export { clearCorpayBankAccountFields, finishCorpayBankAccountOnboarding, clearReimbursementAccountFinishCorpayBankAccountOnboarding, + enableGlobalReimbursementsForUSDBankAccount, + clearEnableGlobalReimbursementsForUSDBankAccount, }; diff --git a/src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx b/src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx index 55369475a522..4da01d34528e 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx @@ -1,10 +1,6 @@ -import type {ComponentType} from 'react'; import React, {useEffect, useMemo} from 'react'; -import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; -import useLocalize from '@hooks/useLocalize'; +import AgreementsFullStep from '@components/SubStepForms/AgreementsFullStep'; import useOnyx from '@hooks/useOnyx'; -import useSubStep from '@hooks/useSubStep'; -import type {SubStepProps} from '@hooks/useSubStep/types'; import requiresDocusignStep from '@pages/ReimbursementAccount/NonUSD/utils/requiresDocusignStep'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; import {clearReimbursementAccountFinishCorpayBankAccountOnboarding, finishCorpayBankAccountOnboarding} from '@userActions/BankAccounts'; @@ -12,7 +8,6 @@ import {clearErrors} from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import Confirmation from './subSteps/Confirmation'; type AgreementsProps = { /** Handles back button press */ @@ -25,24 +20,17 @@ type AgreementsProps = { stepNames?: readonly string[]; /** Currency of the policy */ - policyCurrency: string | undefined; + policyCurrency: string; }; -type AgreementsSubStepProps = SubStepProps & { - policyCurrency: string | undefined; -}; - -const bodyContent: Array> = [Confirmation]; - const INPUT_KEYS = { - PROVIDE_TRUTHFUL_INFORMATION: INPUT_IDS.ADDITIONAL_DATA.CORPAY.PROVIDE_TRUTHFUL_INFORMATION, - AGREE_TO_TERMS_AND_CONDITIONS: INPUT_IDS.ADDITIONAL_DATA.CORPAY.AGREE_TO_TERMS_AND_CONDITIONS, - CONSENT_TO_PRIVACY_NOTICE: INPUT_IDS.ADDITIONAL_DATA.CORPAY.CONSENT_TO_PRIVACY_NOTICE, - AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT: INPUT_IDS.ADDITIONAL_DATA.CORPAY.AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT, + provideTruthfulInformation: INPUT_IDS.ADDITIONAL_DATA.CORPAY.PROVIDE_TRUTHFUL_INFORMATION, + agreeToTermsAndConditions: INPUT_IDS.ADDITIONAL_DATA.CORPAY.AGREE_TO_TERMS_AND_CONDITIONS, + consentToPrivacyNotice: INPUT_IDS.ADDITIONAL_DATA.CORPAY.CONSENT_TO_PRIVACY_NOTICE, + authorizedToBindClientToAgreement: INPUT_IDS.ADDITIONAL_DATA.CORPAY.AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT, }; function Agreements({onBackButtonPress, onSubmit, stepNames, policyCurrency}: AgreementsProps) { - const {translate} = useLocalize(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, {canBeMissing: false}); const agreementsStepValues = useMemo(() => getSubStepValues(INPUT_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); @@ -86,45 +74,23 @@ function Agreements({onBackButtonPress, onSubmit, stepNames, policyCurrency}: Ag }; }, [reimbursementAccount, onSubmit, policyCurrency, isDocusignStepRequired]); - const { - componentToRender: SubStep, - isEditing, - screenIndex, - nextScreen, - prevScreen, - moveTo, - goToTheLastStep, - } = useSubStep({bodyContent, startFrom: 0, onFinished: submit}); - const handleBackButtonPress = () => { clearErrors(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM); - if (isEditing) { - goToTheLastStep(); - return; - } - - if (screenIndex === 0) { - onBackButtonPress(); - } else { - prevScreen(); - } + onBackButtonPress(); }; return ( - - - + stepNames={stepNames} + /> ); } diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/BusinessInfo.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/BusinessInfo.tsx index 29e51beaef57..93dd039275a6 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/BusinessInfo.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/BusinessInfo.tsx @@ -1,6 +1,8 @@ +import {Str} from 'expensify-common'; import type {ComponentType} from 'react'; import React, {useCallback, useEffect, useMemo} from 'react'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useSubStep from '@hooks/useSubStep'; @@ -72,6 +74,7 @@ const INPUT_KEYS = { function BusinessInfo({onBackButtonPress, onSubmit, stepNames}: BusinessInfoProps) { const {translate} = useLocalize(); + const {isDevelopment} = useEnvironment(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); const policyID = reimbursementAccount?.achData?.policyID; @@ -92,6 +95,8 @@ function BusinessInfo({onBackButtonPress, onSubmit, stepNames}: BusinessInfoProp saveCorpayOnboardingCompanyDetails( { ...businessInfoStepValues, + // Corpay does not accept emails with a "+" character and will not let us connect account at the end of whole flow + businessConfirmationEmail: isDevelopment ? Str.replaceAll(businessInfoStepValues.businessConfirmationEmail, '+', '') : businessInfoStepValues.businessConfirmationEmail, fundSourceCountries: country, fundDestinationCountries: country, currencyNeeded: currency, @@ -99,7 +104,7 @@ function BusinessInfo({onBackButtonPress, onSubmit, stepNames}: BusinessInfoProp }, bankAccountID, ); - }, [country, currency, bankAccountID, businessInfoStepValues]); + }, [businessInfoStepValues, isDevelopment, country, currency, bankAccountID]); useEffect(() => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/AverageReimbursement.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/AverageReimbursement.tsx index fd9c3e8d1b04..005645e62a3d 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/AverageReimbursement.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/AverageReimbursement.tsx @@ -1,15 +1,9 @@ -import React, {useCallback, useMemo} from 'react'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import PushRowWithModal from '@components/PushRowWithModal'; -import Text from '@components/Text'; +import React, {useMemo} from 'react'; +import PushRowFieldsStep from '@components/SubStepForms/PushRowFieldsStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import getListOptionsFromCorpayPicklist from '@pages/ReimbursementAccount/NonUSD/utils/getListOptionsFromCorpayPicklist'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -19,9 +13,8 @@ type AverageReimbursementProps = SubStepProps; const {TRADE_VOLUME} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; const STEP_FIELDS = [TRADE_VOLUME]; -function AverageReimbursement({onNext, isEditing}: AverageReimbursementProps) { +function AverageReimbursement({onNext, onMove, isEditing}: AverageReimbursementProps) { const {translate} = useLocalize(); - const styles = useThemeStyles(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); const [corpayOnboardingFields] = useOnyx(ONYXKEYS.CORPAY_ONBOARDING_FIELDS, {canBeMissing: false}); const policyID = reimbursementAccount?.achData?.policyID; @@ -30,11 +23,19 @@ function AverageReimbursement({onNext, isEditing}: AverageReimbursementProps) { const tradeVolumeRangeListOptions = useMemo(() => getListOptionsFromCorpayPicklist(corpayOnboardingFields?.picklists.TradeVolumeRange), [corpayOnboardingFields]); - const tradeVolumeDefaultValue = reimbursementAccount?.achData?.corpay?.[TRADE_VOLUME] ?? ''; - - const validate = useCallback((values: FormOnyxValues): FormInputErrors => { - return getFieldRequiredErrors(values, STEP_FIELDS); - }, []); + const pushRowFields = useMemo( + () => [ + { + inputID: TRADE_VOLUME, + defaultValue: reimbursementAccount?.achData?.corpay?.[TRADE_VOLUME] ?? '', + options: tradeVolumeRangeListOptions, + description: translate('businessInfoStep.averageReimbursementAmountInCurrency', {currencyCode: currency}), + modalHeaderTitle: translate('businessInfoStep.selectAverageReimbursement'), + searchInputTitle: translate('businessInfoStep.findAverageReimbursement'), + }, + ], + [reimbursementAccount, currency, tradeVolumeRangeListOptions, translate], + ); const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, @@ -42,28 +43,20 @@ function AverageReimbursement({onNext, isEditing}: AverageReimbursementProps) { shouldSaveDraft: isEditing, }); + if (corpayOnboardingFields === undefined) { + return null; + } + return ( - - {translate('businessInfoStep.whatsYourExpectedAverageReimbursements')} - - + pushRowFields={pushRowFields} + /> ); } diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/BusinessType.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/BusinessType.tsx index f3f4f8328960..41d5b7c9b077 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/BusinessType.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/BusinessType.tsx @@ -1,15 +1,9 @@ -import React, {useCallback, useMemo} from 'react'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import PushRowWithModal from '@components/PushRowWithModal'; -import Text from '@components/Text'; +import React, {useMemo} from 'react'; +import PushRowFieldsStep from '@components/SubStepForms/PushRowFieldsStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import getListOptionsFromCorpayPicklist from '@pages/ReimbursementAccount/NonUSD/utils/getListOptionsFromCorpayPicklist'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -19,10 +13,8 @@ type BusinessTypeProps = SubStepProps; const {BUSINESS_CATEGORY, APPLICANT_TYPE_ID} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; const STEP_FIELDS = [BUSINESS_CATEGORY, APPLICANT_TYPE_ID]; -function BusinessType({onNext, isEditing}: BusinessTypeProps) { +function BusinessType({onNext, isEditing, onMove}: BusinessTypeProps) { const {translate} = useLocalize(); - const styles = useThemeStyles(); - const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); const [corpayOnboardingFields] = useOnyx(ONYXKEYS.CORPAY_ONBOARDING_FIELDS, {canBeMissing: false}); @@ -32,9 +24,27 @@ function BusinessType({onNext, isEditing}: BusinessTypeProps) { const incorporationTypeDefaultValue = reimbursementAccount?.achData?.corpay?.[APPLICANT_TYPE_ID] ?? ''; const businessCategoryDefaultValue = reimbursementAccount?.achData?.corpay?.[BUSINESS_CATEGORY] ?? ''; - const validate = useCallback((values: FormOnyxValues): FormInputErrors => { - return getFieldRequiredErrors(values, STEP_FIELDS); - }, []); + const pushRowFields = useMemo( + () => [ + { + inputID: APPLICANT_TYPE_ID, + defaultValue: incorporationTypeDefaultValue, + options: incorporationTypeListOptions, + description: translate('businessInfoStep.incorporationTypeName'), + modalHeaderTitle: translate('businessInfoStep.selectIncorporationType'), + searchInputTitle: translate('businessInfoStep.findIncorporationType'), + }, + { + inputID: BUSINESS_CATEGORY, + defaultValue: businessCategoryDefaultValue, + options: natureOfBusinessListOptions, + description: translate('businessInfoStep.businessCategory'), + modalHeaderTitle: translate('businessInfoStep.selectBusinessCategory'), + searchInputTitle: translate('businessInfoStep.findBusinessCategory'), + }, + ], + [businessCategoryDefaultValue, incorporationTypeDefaultValue, incorporationTypeListOptions, natureOfBusinessListOptions, translate], + ); const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, @@ -42,37 +52,20 @@ function BusinessType({onNext, isEditing}: BusinessTypeProps) { shouldSaveDraft: isEditing, }); + if (corpayOnboardingFields === undefined) { + return null; + } + return ( - - {translate('businessInfoStep.whatTypeOfBusinessIsIt')} - - - + pushRowFields={pushRowFields} + /> ); } diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/PaymentVolume.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/PaymentVolume.tsx index c461a97502e5..cf7fe3654429 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/PaymentVolume.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/PaymentVolume.tsx @@ -1,15 +1,9 @@ -import React, {useCallback, useMemo} from 'react'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import PushRowWithModal from '@components/PushRowWithModal'; -import Text from '@components/Text'; +import React, {useMemo} from 'react'; +import PushRowFieldsStep from '@components/SubStepForms/PushRowFieldsStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import getListOptionsFromCorpayPicklist from '@pages/ReimbursementAccount/NonUSD/utils/getListOptionsFromCorpayPicklist'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -19,9 +13,8 @@ type PaymentVolumeProps = SubStepProps; const {ANNUAL_VOLUME} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; const STEP_FIELDS = [ANNUAL_VOLUME]; -function PaymentVolume({onNext, isEditing}: PaymentVolumeProps) { +function PaymentVolume({onNext, onMove, isEditing}: PaymentVolumeProps) { const {translate} = useLocalize(); - const styles = useThemeStyles(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); const [corpayOnboardingFields] = useOnyx(ONYXKEYS.CORPAY_ONBOARDING_FIELDS, {canBeMissing: false}); const policyID = reimbursementAccount?.achData?.policyID; @@ -32,9 +25,19 @@ function PaymentVolume({onNext, isEditing}: PaymentVolumeProps) { const annualVolumeDefaultValue = reimbursementAccount?.achData?.corpay?.[ANNUAL_VOLUME] ?? ''; - const validate = useCallback((values: FormOnyxValues): FormInputErrors => { - return getFieldRequiredErrors(values, STEP_FIELDS); - }, []); + const pushRowFields = useMemo( + () => [ + { + inputID: ANNUAL_VOLUME, + defaultValue: annualVolumeDefaultValue, + options: annualVolumeRangeListOptions, + description: translate('businessInfoStep.annualPaymentVolumeInCurrency', {currencyCode: currency}), + modalHeaderTitle: translate('businessInfoStep.selectAnnualPaymentVolume'), + searchInputTitle: translate('businessInfoStep.findAnnualPaymentVolume'), + }, + ], + [annualVolumeDefaultValue, annualVolumeRangeListOptions, translate, currency], + ); const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, @@ -42,28 +45,20 @@ function PaymentVolume({onNext, isEditing}: PaymentVolumeProps) { shouldSaveDraft: isEditing, }); + if (corpayOnboardingFields === undefined) { + return null; + } + return ( - - {translate('businessInfoStep.whatsTheBusinessAnnualPayment')} - - + pushRowFields={pushRowFields} + /> ); } diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx index adf291c03fde..8c7ca75f7430 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/RegistrationNumber.tsx @@ -1,21 +1,8 @@ -import React, {useCallback} from 'react'; -import {View} from 'react-native'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import Text from '@components/Text'; -import TextInput from '@components/TextInput'; -import TextLink from '@components/TextLink'; -import useLocalize from '@hooks/useLocalize'; +import React from 'react'; +import RegistrationNumberStep from '@components/SubStepForms/RegistrationNumberStep'; import useOnyx from '@hooks/useOnyx'; import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {getFieldRequiredErrors, isValidRegistrationNumber} from '@libs/ValidationUtils'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -24,29 +11,12 @@ type RegistrationNumberProps = SubStepProps; const {BUSINESS_REGISTRATION_INCORPORATION_NUMBER, COMPANY_COUNTRY_CODE} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; const STEP_FIELDS = [BUSINESS_REGISTRATION_INCORPORATION_NUMBER]; -function RegistrationNumber({onNext, isEditing}: RegistrationNumberProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const theme = useTheme(); - - const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); - const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); +function RegistrationNumber({onNext, onMove, isEditing}: RegistrationNumberProps) { + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); const defaultValue = reimbursementAccount?.achData?.corpay?.[BUSINESS_REGISTRATION_INCORPORATION_NUMBER] ?? ''; const businessStepCountryDraftValue = reimbursementAccount?.achData?.corpay?.[COMPANY_COUNTRY_CODE] ?? reimbursementAccountDraft?.[COMPANY_COUNTRY_CODE] ?? ''; - const validate = useCallback( - (values: FormOnyxValues): FormInputErrors => { - const errors = getFieldRequiredErrors(values, STEP_FIELDS); - - if (values[BUSINESS_REGISTRATION_INCORPORATION_NUMBER] && !isValidRegistrationNumber(values[BUSINESS_REGISTRATION_INCORPORATION_NUMBER], businessStepCountryDraftValue)) { - errors[BUSINESS_REGISTRATION_INCORPORATION_NUMBER] = translate('businessInfoStep.error.registrationNumber'); - } - - return errors; - }, - [businessStepCountryDraftValue, translate], - ); - const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, @@ -54,43 +24,17 @@ function RegistrationNumber({onNext, isEditing}: RegistrationNumberProps) { }); return ( - - {translate('businessInfoStep.whatsTheBusinessRegistrationNumber')} - - - - - - {translate('businessInfoStep.whatsThisNumber')} - - - - + inputID={BUSINESS_REGISTRATION_INCORPORATION_NUMBER} + defaultValue={defaultValue} + country={businessStepCountryDraftValue} + shouldDelayAutoFocus + /> ); } diff --git a/src/pages/ReimbursementAccount/NonUSD/Docusign/Docusign.tsx b/src/pages/ReimbursementAccount/NonUSD/Docusign/index.tsx similarity index 66% rename from src/pages/ReimbursementAccount/NonUSD/Docusign/Docusign.tsx rename to src/pages/ReimbursementAccount/NonUSD/Docusign/index.tsx index 81f8beff94d2..32b6c2e85622 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Docusign/Docusign.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/Docusign/index.tsx @@ -1,17 +1,12 @@ -import type {ComponentType} from 'react'; import React, {useEffect, useMemo} from 'react'; -import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; -import useLocalize from '@hooks/useLocalize'; +import DocusignFullStep from '@components/SubStepForms/DocusignFullStep'; import useOnyx from '@hooks/useOnyx'; -import useSubStep from '@hooks/useSubStep'; -import type {SubStepProps} from '@hooks/useSubStep/types'; import getSubStepValues from '@pages/ReimbursementAccount/utils/getSubStepValues'; import {clearReimbursementAccountFinishCorpayBankAccountOnboarding, finishCorpayBankAccountOnboarding} from '@userActions/BankAccounts'; import {clearErrors} from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import UploadPowerform from './subSteps/UploadPowerform'; type DocusignProps = { /** Handles back button press */ @@ -20,19 +15,12 @@ type DocusignProps = { /** Handles submit button press */ onSubmit: () => void; - /** ID of current policy */ - policyID: string | undefined; - /** Array of step names */ stepNames?: readonly string[]; -}; - -type DocusignStepProps = { - /** ID of current policy */ - policyID: string | undefined; -} & SubStepProps; -const bodyContent: Array> = [UploadPowerform]; + /** Currency of the policy */ + policyCurrency: string; +}; const INPUT_KEYS = { PROVIDE_TRUTHFUL_INFORMATION: INPUT_IDS.ADDITIONAL_DATA.CORPAY.PROVIDE_TRUTHFUL_INFORMATION, @@ -42,9 +30,7 @@ const INPUT_KEYS = { ACH_AUTHORIZATION_FORM: INPUT_IDS.ADDITIONAL_DATA.CORPAY.ACH_AUTHORIZATION_FORM, }; -function Docusign({onBackButtonPress, onSubmit, policyID, stepNames}: DocusignProps) { - const {translate} = useLocalize(); - +function Docusign({onBackButtonPress, onSubmit, stepNames, policyCurrency}: DocusignProps) { const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, {canBeMissing: false}); const finalStepValues = useMemo(() => getSubStepValues(INPUT_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); @@ -79,45 +65,23 @@ function Docusign({onBackButtonPress, onSubmit, policyID, stepNames}: DocusignPr }; }, [reimbursementAccount, onSubmit]); - const { - componentToRender: SubStep, - isEditing, - screenIndex, - nextScreen, - prevScreen, - moveTo, - goToTheLastStep, - } = useSubStep({bodyContent, startFrom: 0, onFinished: submit}); - const handleBackButtonPress = () => { clearErrors(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM); - if (isEditing) { - goToTheLastStep(); - return; - } - - if (screenIndex === 0) { - onBackButtonPress(); - } else { - prevScreen(); - } + onBackButtonPress(); }; return ( - - - + stepNames={stepNames} + /> ); } diff --git a/src/pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlow.tsx b/src/pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlow.tsx index 7a74beefeb74..6dc652c7e39c 100644 --- a/src/pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlow.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/NonUSDVerifiedBankAccountFlow.tsx @@ -7,7 +7,7 @@ import BankInfo from './BankInfo/BankInfo'; import BeneficialOwnerInfo from './BeneficialOwnerInfo/BeneficialOwnerInfo'; import BusinessInfo from './BusinessInfo/BusinessInfo'; import Country from './Country'; -import Docusign from './Docusign/Docusign'; +import Docusign from './Docusign'; import Finish from './Finish'; import SignerInfo from './SignerInfo'; import requiresDocusignStep from './utils/requiresDocusignStep'; @@ -18,7 +18,7 @@ type NonUSDVerifiedBankAccountFlowProps = { setShouldShowContinueSetupButton: (shouldShowConnectedVerifiedBankAccount: boolean) => void; policyID: string | undefined; shouldShowContinueSetupButtonValue: boolean; - policyCurrency: string | undefined; + policyCurrency: string; }; function NonUSDVerifiedBankAccountFlow({ @@ -142,8 +142,8 @@ function NonUSDVerifiedBankAccountFlow({ ); case CONST.NON_USD_BANK_ACCOUNT.STEP.DOCUSIGN: @@ -151,7 +151,7 @@ function NonUSDVerifiedBankAccountFlow({ ); diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx index 0a5eee194141..779765d85bd9 100644 --- a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx @@ -1,7 +1,9 @@ +import {Str} from 'expensify-common'; import type {ComponentType} from 'react'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import YesNoStep from '@components/SubStepForms/YesNoStep'; +import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useSubStep from '@hooks/useSubStep'; @@ -44,6 +46,7 @@ const userIsOwnerBodyContent: Array> = [Jo function SignerInfo({onBackButtonPress, onSubmit, stepNames}: SignerInfoProps) { const {translate} = useLocalize(); + const {isDevelopment} = useEnvironment(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: false}); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); @@ -56,16 +59,19 @@ function SignerInfo({onBackButtonPress, onSubmit, stepNames}: SignerInfoProps) { const bankAccountID = reimbursementAccount?.achData?.bankAccountID ?? CONST.DEFAULT_NUMBER_ID; const [currentSubStep, setCurrentSubStep] = useState(SUBSTEP.IS_DIRECTOR); const [isUserDirector, setIsUserDirector] = useState(false); + const primaryLogin = account?.primaryLogin ?? ''; + // Corpay does not accept emails with a "+" character and will not let us connect account at the end of whole flow + const signerEmail = isDevelopment ? Str.replaceAll(primaryLogin, '+', '') : primaryLogin; const submit = useCallback(() => { - const {signerDetails, signerFiles} = getSignerDetailsAndSignerFilesForSignerInfo(reimbursementAccountDraft, account?.primaryLogin ?? '', isUserOwner); + const {signerDetails, signerFiles} = getSignerDetailsAndSignerFilesForSignerInfo(reimbursementAccountDraft, signerEmail, isUserOwner); saveCorpayOnboardingDirectorInformation({ inputs: JSON.stringify(signerDetails), ...signerFiles, bankAccountID, }); - }, [account?.primaryLogin, bankAccountID, isUserOwner, reimbursementAccountDraft]); + }, [bankAccountID, isUserOwner, reimbursementAccountDraft, signerEmail]); useEffect(() => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 69dc05fe6fbb..dc6e87264474 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -51,6 +51,7 @@ import type {ACHDataReimbursementAccount} from '@src/types/onyx/ReimbursementAcc import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ConnectedVerifiedBankAccount from './ConnectedVerifiedBankAccount'; import NonUSDVerifiedBankAccountFlow from './NonUSD/NonUSDVerifiedBankAccountFlow'; +import requiresDocusignStep from './NonUSD/utils/requiresDocusignStep'; import USDVerifiedBankAccountFlow from './USD/USDVerifiedBankAccountFlow'; import getFieldsForStep from './USD/utils/getFieldsForStep'; import getStepToOpenFromRouteParams from './USD/utils/getStepToOpenFromRouteParams'; @@ -95,6 +96,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen const achData = reimbursementAccount?.achData; const isPreviousPolicy = policyIDParam === achData?.policyID; const hasConfirmedUSDCurrency = (reimbursementAccountDraft?.[INPUT_IDS.ADDITIONAL_DATA.COUNTRY] ?? '') !== '' || (achData?.accountNumber ?? '') !== ''; + const isDocusignStepRequired = requiresDocusignStep(policyCurrency); /** We main rely on `achData.currentStep` to determine the step to display in USD flow. @@ -296,7 +298,12 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen return; } - if (isPastSignerStep && allAgreementsChecked) { + if (isPastSignerStep && allAgreementsChecked && !isDocusignStepRequired) { + setNonUSDBankAccountStep(CONST.NON_USD_BANK_ACCOUNT.STEP.AGREEMENTS); + return; + } + + if (isPastSignerStep && allAgreementsChecked && isDocusignStepRequired) { setNonUSDBankAccountStep(CONST.NON_USD_BANK_ACCOUNT.STEP.DOCUSIGN); return; } diff --git a/src/pages/ReimbursementAccount/utils/getSubStepValues.ts b/src/pages/ReimbursementAccount/utils/getSubStepValues.ts index 2a73546075c9..d0918b5441da 100644 --- a/src/pages/ReimbursementAccount/utils/getSubStepValues.ts +++ b/src/pages/ReimbursementAccount/utils/getSubStepValues.ts @@ -1,5 +1,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ReimbursementAccountForm} from '@src/types/form'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type {ReimbursementAccount} from '@src/types/onyx'; import type {ACHData, Corpay} from '@src/types/onyx/ReimbursementAccount'; @@ -16,7 +17,7 @@ function getSubStepValues( acc[value] = (reimbursementAccountDraft?.[value] ?? reimbursementAccount?.achData?.[value as keyof ACHData] ?? reimbursementAccount?.achData?.corpay?.[value as keyof Corpay] ?? - '') as ReimbursementAccountForm[TProps]; + (value === INPUT_IDS.ADDITIONAL_DATA.CORPAY.ACH_AUTHORIZATION_FORM ? [] : '')) as ReimbursementAccountForm[TProps]; return acc; }, {} as SubStepValues); } diff --git a/src/pages/settings/Wallet/EnableGlobalReimbursements/Agreements/index.tsx b/src/pages/settings/Wallet/EnableGlobalReimbursements/Agreements/index.tsx new file mode 100644 index 000000000000..1161f736073c --- /dev/null +++ b/src/pages/settings/Wallet/EnableGlobalReimbursements/Agreements/index.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import AgreementsFullStep from '@components/SubStepForms/AgreementsFullStep'; +import useOnyx from '@hooks/useOnyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/EnableGlobalReimbursementsForm'; + +type AgreementsProps = { + /** Handles back button press */ + onBackButtonPress: () => void; + + /** Handles submit button press */ + onSubmit: () => void; + + /** Currency of bank account */ + currency: string; +}; + +const inputIDs = { + provideTruthfulInformation: INPUT_IDS.PROVIDE_TRUTHFUL_INFORMATION, + agreeToTermsAndConditions: INPUT_IDS.AGREE_TO_TERMS_AND_CONDITIONS, + consentToPrivacyNotice: INPUT_IDS.CONSENT_TO_PRIVACY_NOTICE, + authorizedToBindClientToAgreement: INPUT_IDS.AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT, +}; + +function Agreements({onBackButtonPress, onSubmit, currency}: AgreementsProps) { + const [enableGlobalReimbursementsDraft] = useOnyx(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT, {canBeMissing: true}); + const defaultValues: Record = Object.fromEntries( + Object.keys(inputIDs).map((key) => { + const typedKey = key as keyof typeof inputIDs; + return [typedKey, enableGlobalReimbursementsDraft?.[typedKey] ?? false]; + }), + ) as Record; + + return ( + + ); +} + +Agreements.displayName = 'Agreements'; + +export default Agreements; diff --git a/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/index.tsx b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/index.tsx new file mode 100644 index 000000000000..c28744223f43 --- /dev/null +++ b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/index.tsx @@ -0,0 +1,84 @@ +import type {ComponentType} from 'react'; +import React from 'react'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useSubStep from '@hooks/useSubStep'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import {clearErrors} from '@userActions/FormActions'; +import CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import AverageReimbursement from './subSteps/AverageReimbursement'; +import BusinessType from './subSteps/BusinessType'; +import Confirmation from './subSteps/Confirmation'; +import PaymentVolume from './subSteps/PaymentVolume'; +import RegistrationNumber from './subSteps/RegistrationNumber'; + +type BusinessInfoProps = { + /** Handles back button press */ + onBackButtonPress: () => void; + + /** Handles submit button press */ + onSubmit: () => void; + + /** Currency of bank account */ + currency: string; + + /** Country of bank account */ + country: Country | ''; +}; + +type BusinessInfoSubStepProps = SubStepProps & {currency: string; country: Country | ''}; + +const bodyContent: Array> = [RegistrationNumber, BusinessType, PaymentVolume, AverageReimbursement, Confirmation]; + +function BusinessInfo({onBackButtonPress, onSubmit, currency, country}: BusinessInfoProps) { + const {translate} = useLocalize(); + + const { + componentToRender: SubStep, + isEditing, + screenIndex, + nextScreen, + prevScreen, + moveTo, + goToTheLastStep, + } = useSubStep({bodyContent, startFrom: 0, onFinished: onSubmit}); + + const handleBackButtonPress = () => { + clearErrors(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS); + if (isEditing) { + goToTheLastStep(); + return; + } + + if (screenIndex === 0) { + onBackButtonPress(); + } else { + prevScreen(); + } + }; + + return ( + + + + ); +} + +BusinessInfo.displayName = 'BusinessInfo'; + +export default BusinessInfo; diff --git a/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/AverageReimbursement.tsx b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/AverageReimbursement.tsx new file mode 100644 index 000000000000..587f12d66881 --- /dev/null +++ b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/AverageReimbursement.tsx @@ -0,0 +1,62 @@ +import React, {useMemo} from 'react'; +import PushRowFieldsStep from '@components/SubStepForms/PushRowFieldsStep'; +import useEnableGlobalReimbursementsStepFormSubmit from '@hooks/useEnableGlobalReimbursementsStepFormSubmit'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import getListOptionsFromCorpayPicklist from '@pages/ReimbursementAccount/NonUSD/utils/getListOptionsFromCorpayPicklist'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/EnableGlobalReimbursementsForm'; + +type AverageReimbursementsProps = SubStepProps & {currency: string}; + +const {TRADE_VOLUME} = INPUT_IDS; +const STEP_FIELDS = [TRADE_VOLUME]; + +function AverageReimbursements({onNext, onMove, isEditing, currency}: AverageReimbursementsProps) { + const {translate} = useLocalize(); + const [enableGlobalReimbursementsDraft] = useOnyx(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT, {canBeMissing: true}); + const [corpayOnboardingFields] = useOnyx(ONYXKEYS.CORPAY_ONBOARDING_FIELDS, {canBeMissing: true}); + + const tradeVolumeRangeListOptions = useMemo(() => getListOptionsFromCorpayPicklist(corpayOnboardingFields?.picklists.TradeVolumeRange), [corpayOnboardingFields]); + + const pushRowFields = useMemo( + () => [ + { + inputID: TRADE_VOLUME, + defaultValue: enableGlobalReimbursementsDraft?.[TRADE_VOLUME] ?? '', + options: tradeVolumeRangeListOptions, + description: translate('businessInfoStep.averageReimbursementAmountInCurrency', {currencyCode: currency}), + modalHeaderTitle: translate('businessInfoStep.selectAverageReimbursement'), + searchInputTitle: translate('businessInfoStep.findAverageReimbursement'), + }, + ], + [enableGlobalReimbursementsDraft, currency, tradeVolumeRangeListOptions, translate], + ); + + const handleSubmit = useEnableGlobalReimbursementsStepFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + if (corpayOnboardingFields === undefined) { + return null; + } + + return ( + + ); +} + +AverageReimbursements.displayName = 'AverageReimbursements'; + +export default AverageReimbursements; diff --git a/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/BusinessType.tsx b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/BusinessType.tsx new file mode 100644 index 000000000000..27a3b1c3a9d1 --- /dev/null +++ b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/BusinessType.tsx @@ -0,0 +1,69 @@ +import React, {useMemo} from 'react'; +import PushRowFieldsStep from '@components/SubStepForms/PushRowFieldsStep'; +import useEnableGlobalReimbursementsStepFormSubmit from '@hooks/useEnableGlobalReimbursementsStepFormSubmit'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import getListOptionsFromCorpayPicklist from '@pages/ReimbursementAccount/NonUSD/utils/getListOptionsFromCorpayPicklist'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/EnableGlobalReimbursementsForm'; + +const {APPLICANT_TYPE_ID, BUSINESS_CATEGORY} = INPUT_IDS; +const STEP_FIELDS = [APPLICANT_TYPE_ID, BUSINESS_CATEGORY]; + +function BusinessType({onNext, onMove, isEditing}: SubStepProps) { + const {translate} = useLocalize(); + const [enableGlobalReimbursementsDraft] = useOnyx(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT, {canBeMissing: true}); + const [corpayOnboardingFields] = useOnyx(ONYXKEYS.CORPAY_ONBOARDING_FIELDS, {canBeMissing: true}); + + const incorporationTypeListOptions = useMemo(() => getListOptionsFromCorpayPicklist(corpayOnboardingFields?.picklists.ApplicantType), [corpayOnboardingFields]); + const natureOfBusinessListOptions = useMemo(() => getListOptionsFromCorpayPicklist(corpayOnboardingFields?.picklists.NatureOfBusiness), [corpayOnboardingFields]); + + const pushRowFields = useMemo( + () => [ + { + inputID: APPLICANT_TYPE_ID, + defaultValue: enableGlobalReimbursementsDraft?.[APPLICANT_TYPE_ID] ?? '', + options: incorporationTypeListOptions, + description: translate('businessInfoStep.incorporationTypeName'), + modalHeaderTitle: translate('businessInfoStep.selectIncorporationType'), + searchInputTitle: translate('businessInfoStep.findIncorporationType'), + }, + { + inputID: BUSINESS_CATEGORY, + defaultValue: enableGlobalReimbursementsDraft?.[BUSINESS_CATEGORY] ?? '', + options: natureOfBusinessListOptions, + description: translate('businessInfoStep.businessCategory'), + modalHeaderTitle: translate('businessInfoStep.selectBusinessCategory'), + searchInputTitle: translate('businessInfoStep.findBusinessCategory'), + }, + ], + [enableGlobalReimbursementsDraft, incorporationTypeListOptions, natureOfBusinessListOptions, translate], + ); + + const handleSubmit = useEnableGlobalReimbursementsStepFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + if (corpayOnboardingFields === undefined) { + return null; + } + + return ( + + ); +} + +BusinessType.displayName = 'BusinessType'; + +export default BusinessType; diff --git a/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/Confirmation.tsx b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/Confirmation.tsx new file mode 100644 index 000000000000..04aee0217e1d --- /dev/null +++ b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/Confirmation.tsx @@ -0,0 +1,101 @@ +import React, {useMemo} from 'react'; +import ConfirmationStep from '@components/SubStepForms/ConfirmationStep'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/EnableGlobalReimbursementsForm'; + +const {BUSINESS_REGISTRATION_INCORPORATION_NUMBER, ANNUAL_VOLUME, APPLICANT_TYPE_ID, TRADE_VOLUME, BUSINESS_CATEGORY} = INPUT_IDS; + +const displayStringValue = (list: Array<{id: string; name: string; stringValue: string}>, matchingName: string) => { + return list.find((item) => item.name === matchingName)?.stringValue ?? ''; +}; + +function Confirmation({onNext, onMove, isEditing}: SubStepProps) { + const {translate} = useLocalize(); + + const [enableGlobalReimbursementsDraft] = useOnyx(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT, {canBeMissing: false}); + const [corpayOnboardingFields] = useOnyx(ONYXKEYS.CORPAY_ONBOARDING_FIELDS, {canBeMissing: false}); + const error = getLatestErrorMessage(enableGlobalReimbursementsDraft); + + const paymentVolume = useMemo( + () => displayStringValue(corpayOnboardingFields?.picklists.AnnualVolumeRange ?? [], enableGlobalReimbursementsDraft?.[ANNUAL_VOLUME] ?? ''), + [corpayOnboardingFields?.picklists.AnnualVolumeRange, enableGlobalReimbursementsDraft], + ); + const businessCategory = useMemo( + () => displayStringValue(corpayOnboardingFields?.picklists.NatureOfBusiness ?? [], enableGlobalReimbursementsDraft?.[BUSINESS_CATEGORY] ?? ''), + [corpayOnboardingFields?.picklists.NatureOfBusiness, enableGlobalReimbursementsDraft], + ); + const businessType = useMemo( + () => displayStringValue(corpayOnboardingFields?.picklists.ApplicantType ?? [], enableGlobalReimbursementsDraft?.[APPLICANT_TYPE_ID] ?? ''), + [corpayOnboardingFields?.picklists.ApplicantType, enableGlobalReimbursementsDraft], + ); + const tradeVolumeRange = useMemo( + () => displayStringValue(corpayOnboardingFields?.picklists.TradeVolumeRange ?? [], enableGlobalReimbursementsDraft?.[TRADE_VOLUME] ?? ''), + [corpayOnboardingFields?.picklists.TradeVolumeRange, enableGlobalReimbursementsDraft], + ); + + const summaryItems = useMemo( + () => [ + { + title: enableGlobalReimbursementsDraft?.[BUSINESS_REGISTRATION_INCORPORATION_NUMBER] ?? '', + description: translate('businessInfoStep.registrationNumber'), + shouldShowRightIcon: true, + onPress: () => { + onMove(0); + }, + }, + { + title: businessType, + description: translate('businessInfoStep.businessType'), + shouldShowRightIcon: true, + onPress: () => { + onMove(1); + }, + }, + { + title: businessCategory, + description: translate('businessInfoStep.businessCategory'), + shouldShowRightIcon: true, + onPress: () => { + onMove(1); + }, + }, + { + title: paymentVolume, + description: translate('businessInfoStep.annualPaymentVolume'), + shouldShowRightIcon: true, + onPress: () => { + onMove(2); + }, + }, + { + title: tradeVolumeRange, + description: translate('businessInfoStep.averageReimbursementAmount'), + shouldShowRightIcon: true, + onPress: () => { + onMove(3); + }, + }, + ], + [businessCategory, businessType, enableGlobalReimbursementsDraft, onMove, paymentVolume, tradeVolumeRange, translate], + ); + + return ( + + ); +} + +Confirmation.displayName = 'Confirmation'; + +export default Confirmation; diff --git a/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/PaymentVolume.tsx b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/PaymentVolume.tsx new file mode 100644 index 000000000000..1052d8e44106 --- /dev/null +++ b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/PaymentVolume.tsx @@ -0,0 +1,62 @@ +import React, {useMemo} from 'react'; +import PushRowFieldsStep from '@components/SubStepForms/PushRowFieldsStep'; +import useEnableGlobalReimbursementsStepFormSubmit from '@hooks/useEnableGlobalReimbursementsStepFormSubmit'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import getListOptionsFromCorpayPicklist from '@pages/ReimbursementAccount/NonUSD/utils/getListOptionsFromCorpayPicklist'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/EnableGlobalReimbursementsForm'; + +type PaymentVolumeProps = SubStepProps & {currency: string}; + +const {ANNUAL_VOLUME} = INPUT_IDS; +const STEP_FIELDS = [ANNUAL_VOLUME]; + +function PaymentVolume({onNext, onMove, isEditing, currency}: PaymentVolumeProps) { + const {translate} = useLocalize(); + const [enableGlobalReimbursementsDraft] = useOnyx(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT, {canBeMissing: true}); + const [corpayOnboardingFields] = useOnyx(ONYXKEYS.CORPAY_ONBOARDING_FIELDS, {canBeMissing: true}); + + const annualVolumeRangeListOptions = useMemo(() => getListOptionsFromCorpayPicklist(corpayOnboardingFields?.picklists.AnnualVolumeRange), [corpayOnboardingFields]); + + const pushRowFields = useMemo( + () => [ + { + inputID: ANNUAL_VOLUME, + defaultValue: enableGlobalReimbursementsDraft?.[ANNUAL_VOLUME] ?? '', + options: annualVolumeRangeListOptions, + description: translate('businessInfoStep.annualPaymentVolumeInCurrency', {currencyCode: currency}), + modalHeaderTitle: translate('businessInfoStep.selectAnnualPaymentVolume'), + searchInputTitle: translate('businessInfoStep.findAnnualPaymentVolume'), + }, + ], + [enableGlobalReimbursementsDraft, currency, annualVolumeRangeListOptions, translate], + ); + + const handleSubmit = useEnableGlobalReimbursementsStepFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + if (corpayOnboardingFields === undefined) { + return null; + } + + return ( + + ); +} + +PaymentVolume.displayName = 'PaymentVolume'; + +export default PaymentVolume; diff --git a/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/RegistrationNumber.tsx b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/RegistrationNumber.tsx new file mode 100644 index 000000000000..b145ea6b8fcc --- /dev/null +++ b/src/pages/settings/Wallet/EnableGlobalReimbursements/BusinessInfo/subSteps/RegistrationNumber.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import RegistrationNumberStep from '@components/SubStepForms/RegistrationNumberStep'; +import useEnableGlobalReimbursementsStepFormSubmit from '@hooks/useEnableGlobalReimbursementsStepFormSubmit'; +import useOnyx from '@hooks/useOnyx'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import type {Country} from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/EnableGlobalReimbursementsForm'; + +type RegistrationNumberProps = SubStepProps & {country: Country | ''}; + +const {BUSINESS_REGISTRATION_INCORPORATION_NUMBER} = INPUT_IDS; +const STEP_FIELDS = [BUSINESS_REGISTRATION_INCORPORATION_NUMBER]; + +function RegistrationNumber({onNext, onMove, isEditing, country}: RegistrationNumberProps) { + const [enableGlobalReimbursementsDraft] = useOnyx(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT, {canBeMissing: true}); + const handleSubmit = useEnableGlobalReimbursementsStepFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + + ); +} + +RegistrationNumber.displayName = 'RegistrationNumber'; + +export default RegistrationNumber; diff --git a/src/pages/settings/Wallet/EnableGlobalReimbursements/Docusign/index.tsx b/src/pages/settings/Wallet/EnableGlobalReimbursements/Docusign/index.tsx new file mode 100644 index 000000000000..e1e94b9de98d --- /dev/null +++ b/src/pages/settings/Wallet/EnableGlobalReimbursements/Docusign/index.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import DocusignFullStep from '@components/SubStepForms/DocusignFullStep'; +import useOnyx from '@hooks/useOnyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/EnableGlobalReimbursementsForm'; + +type DocusignProps = { + /** Handles back button press */ + onBackButtonPress: () => void; + + /** Handles submit button press */ + onSubmit: () => void; + + /** Currency of affiliated policy */ + currency: string; +}; + +function Docusign({onBackButtonPress, onSubmit, currency}: DocusignProps) { + const [enableGlobalReimbursements] = useOnyx(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS, {canBeMissing: true}); + const [enableGlobalReimbursementsDraft] = useOnyx(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT, {canBeMissing: true}); + const defaultValue = enableGlobalReimbursementsDraft?.[INPUT_IDS.ACH_AUTHORIZATION_FORM] ?? []; + + return ( + + ); +} + +Docusign.displayName = 'Docusign'; + +export default Docusign; diff --git a/src/pages/settings/Wallet/EnableGlobalReimbursements/index.tsx b/src/pages/settings/Wallet/EnableGlobalReimbursements/index.tsx new file mode 100644 index 000000000000..d0be360f8c6b --- /dev/null +++ b/src/pages/settings/Wallet/EnableGlobalReimbursements/index.tsx @@ -0,0 +1,139 @@ +import React, {useEffect, useState} from 'react'; +import type {ValueOf} from 'type-fest'; +import useOnyx from '@hooks/useOnyx'; +import Navigation from '@navigation/Navigation'; +import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import {clearEnableGlobalReimbursementsForUSDBankAccount, enableGlobalReimbursementsForUSDBankAccount, getCorpayOnboardingFields} from '@userActions/BankAccounts'; +import {clearErrors} from '@userActions/FormActions'; +import CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/EnableGlobalReimbursementsForm'; +import Agreements from './Agreements'; +import BusinessInfo from './BusinessInfo'; +import Docusign from './Docusign'; + +type EnableGlobalReimbursementsProps = PlatformStackScreenProps; + +function EnableGlobalReimbursements({route}: EnableGlobalReimbursementsProps) { + const bankAccountID = route.params?.bankAccountID; + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: false}); + const [enableGlobalReimbursements] = useOnyx(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS, {canBeMissing: true}); + const [enableGlobalReimbursementsDraft] = useOnyx(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT, {canBeMissing: true}); + const currency = bankAccountList?.[bankAccountID]?.bankCurrency ?? ''; + const country = bankAccountList?.[bankAccountID]?.bankCountry; + + const [enableGlobalReimbursementsStep, setEnableGlobalReimbursementsStep] = useState>( + CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.BUSINESS_INFO, + ); + + useEffect(() => { + getCorpayOnboardingFields(country as Country); + }, [country]); + + const handleNextEnableGlobalReimbursementsStep = () => { + switch (enableGlobalReimbursementsStep) { + case CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.BUSINESS_INFO: + setEnableGlobalReimbursementsStep(CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.AGREEMENTS); + break; + + case CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.AGREEMENTS: + setEnableGlobalReimbursementsStep(CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.DOCUSIGN); + break; + + default: + enableGlobalReimbursementsForUSDBankAccount({ + inputs: JSON.stringify({ + provideTruthfulInformation: enableGlobalReimbursementsDraft?.[INPUT_IDS.PROVIDE_TRUTHFUL_INFORMATION], + agreeToTermsAndConditions: enableGlobalReimbursementsDraft?.[INPUT_IDS.AGREE_TO_TERMS_AND_CONDITIONS], + consentToPrivacyNotice: enableGlobalReimbursementsDraft?.[INPUT_IDS.CONSENT_TO_PRIVACY_NOTICE], + authorizedToBindClientToAgreement: enableGlobalReimbursementsDraft?.[INPUT_IDS.AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT], + natureOfBusiness: enableGlobalReimbursementsDraft?.[INPUT_IDS.BUSINESS_CATEGORY], + applicantTypeId: enableGlobalReimbursementsDraft?.[INPUT_IDS.APPLICANT_TYPE_ID], + tradeVolume: enableGlobalReimbursementsDraft?.[INPUT_IDS.TRADE_VOLUME], + annualVolume: enableGlobalReimbursementsDraft?.[INPUT_IDS.ANNUAL_VOLUME], + businessRegistrationIncorporationNumber: enableGlobalReimbursementsDraft?.[INPUT_IDS.BUSINESS_REGISTRATION_INCORPORATION_NUMBER], + fundSourceCountries: country, + fundDestinationCountries: country, + currencyNeeded: currency, + purposeOfTransactionId: CONST.NON_USD_BANK_ACCOUNT.PURPOSE_OF_TRANSACTION_ID, + }), + achAuthorizationForm: enableGlobalReimbursementsDraft?.[INPUT_IDS.ACH_AUTHORIZATION_FORM].at(0), + bankAccountID: Number(bankAccountID), + }); + } + }; + + const handleEnableGlobalReimbursementGoBack = () => { + clearErrors(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS); + switch (enableGlobalReimbursementsStep) { + case CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.BUSINESS_INFO: + Navigation.goBack(); + break; + case CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.AGREEMENTS: + setEnableGlobalReimbursementsStep(CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.BUSINESS_INFO); + break; + case CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.DOCUSIGN: + setEnableGlobalReimbursementsStep(CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.AGREEMENTS); + break; + default: + return null; + } + }; + + useEffect(() => { + return clearErrors(ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS); + }, []); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (enableGlobalReimbursements?.errors || enableGlobalReimbursements?.isEnablingGlobalReimbursements || !enableGlobalReimbursements?.isSuccess) { + return; + } + + if (enableGlobalReimbursements?.isSuccess) { + clearEnableGlobalReimbursementsForUSDBankAccount(); + Navigation.closeRHPFlow(); + } + + return () => { + clearEnableGlobalReimbursementsForUSDBankAccount(); + }; + }, [enableGlobalReimbursements?.errors, enableGlobalReimbursements?.isEnablingGlobalReimbursements, enableGlobalReimbursements?.isSuccess]); + + switch (enableGlobalReimbursementsStep) { + case CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.BUSINESS_INFO: + return ( + + ); + case CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.AGREEMENTS: + return ( + + ); + case CONST.ENABLE_GLOBAL_REIMBURSEMENTS.STEP.DOCUSIGN: + return ( + + ); + default: + return null; + } +} + +EnableGlobalReimbursements.displayName = 'EnableGlobalReimbursements'; + +export default EnableGlobalReimbursements; diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 0e8e87cc26f0..632c99b37db3 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -29,6 +29,7 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaymentMethodState from '@hooks/usePaymentMethodState'; import type {FormattedSelectedPaymentMethod, FormattedSelectedPaymentMethodIcon} from '@hooks/usePaymentMethodState/types'; +import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -69,6 +70,7 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { const isUserValidated = userAccount?.validated ?? false; const {isActingAsDelegate, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const {isAccountLocked, showLockedAccountModal} = useContext(LockedAccountContext); + const {isBetaEnabled} = usePermissions(); const theme = useTheme(); const styles = useThemeStyles(); @@ -385,6 +387,11 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { !isCurrentPaymentMethodDefault() && !(paymentMethod.formattedSelectedPaymentMethod.type === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && paymentMethod.selectedPaymentMethod.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS); + const shouldShowEnableGlobalReimbursementsButton = + isBetaEnabled(CONST.BETAS.GLOBAL_REIMBURSEMENTS_ON_ND) && + paymentMethod.selectedPaymentMethod.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && + !paymentMethod.selectedPaymentMethod?.additionalData?.corpay?.achAuthorizationForm; + // Determines whether or not the modal popup is mounted from the bottom of the screen instead of the side mount on Web or Desktop screens const isPopoverBottomMount = anchorPosition.anchorPositionTop === 0 || shouldUseNarrowLayout; const alertTextStyle = [styles.inlineSystemMessage, styles.flexShrink1]; @@ -693,6 +700,28 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { }} wrapperStyle={[styles.pv3, styles.ph5, !shouldUseNarrowLayout ? styles.sidebarPopover : {}]} /> + {shouldShowEnableGlobalReimbursementsButton && ( + { + if (isActingAsDelegate) { + closeModal(() => { + showDelegateNoAccessModal(); + }); + return; + } + if (isAccountLocked) { + closeModal(() => showLockedAccountModal()); + return; + } + closeModal(() => + Navigation.navigate(ROUTES.SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS.getRoute(paymentMethod.selectedPaymentMethod.bankAccountID)), + ); + }} + wrapperStyle={[styles.pv3, styles.ph5, !shouldUseNarrowLayout ? styles.sidebarPopover : {}]} + /> + )} )} diff --git a/src/types/form/EnableGlobalReimbursementsForm.ts b/src/types/form/EnableGlobalReimbursementsForm.ts new file mode 100644 index 000000000000..162e066248d2 --- /dev/null +++ b/src/types/form/EnableGlobalReimbursementsForm.ts @@ -0,0 +1,37 @@ +import type {ValueOf} from 'type-fest'; +import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; +import type Form from './Form'; + +const INPUT_IDS = { + BUSINESS_REGISTRATION_INCORPORATION_NUMBER: 'businessRegistrationIncorporationNumber', + BUSINESS_CATEGORY: 'natureOfBusiness', + APPLICANT_TYPE_ID: 'applicantTypeId', + TRADE_VOLUME: 'tradeVolume', + ANNUAL_VOLUME: 'annualVolume', + PROVIDE_TRUTHFUL_INFORMATION: 'provideTruthfulInformation', + AGREE_TO_TERMS_AND_CONDITIONS: 'agreeToTermsAndConditions', + CONSENT_TO_PRIVACY_NOTICE: 'consentToPrivacyNotice', + AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT: 'authorizedToBindClientToAgreement', + ACH_AUTHORIZATION_FORM: 'achAuthorizationForm', +} as const; + +type InputID = ValueOf; + +type EnableGlobalReimbursementsForm = Form< + InputID, + { + [INPUT_IDS.BUSINESS_REGISTRATION_INCORPORATION_NUMBER]: string; + [INPUT_IDS.BUSINESS_CATEGORY]: string; + [INPUT_IDS.APPLICANT_TYPE_ID]: string; + [INPUT_IDS.TRADE_VOLUME]: string; + [INPUT_IDS.ANNUAL_VOLUME]: string; + [INPUT_IDS.PROVIDE_TRUTHFUL_INFORMATION]: boolean; + [INPUT_IDS.AGREE_TO_TERMS_AND_CONDITIONS]: boolean; + [INPUT_IDS.CONSENT_TO_PRIVACY_NOTICE]: boolean; + [INPUT_IDS.AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT]: boolean; + [INPUT_IDS.ACH_AUTHORIZATION_FORM]: FileObject[]; + } +> & {isEnablingGlobalReimbursements?: boolean; isSuccess?: boolean}; + +export type {EnableGlobalReimbursementsForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index feffe56baea8..18052991c328 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -96,3 +96,4 @@ export type {MoneyRequestTimeForm} from './MoneyRequestTimeForm'; export type {MoneyRequestSubrateForm} from './MoneyRequestSubrateForm'; export type {InternationalBankAccountForm} from './InternationalBankAccountForm'; export type {WorkspacePerDiemForm} from './WorkspacePerDiemForm'; +export type {EnableGlobalReimbursementsForm} from './EnableGlobalReimbursementsForm'; diff --git a/src/types/onyx/BankAccount.ts b/src/types/onyx/BankAccount.ts index f71969db4709..43008aff07ed 100644 --- a/src/types/onyx/BankAccount.ts +++ b/src/types/onyx/BankAccount.ts @@ -1,3 +1,4 @@ +import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; import type CONST from '@src/CONST'; import type AccountData from './AccountData'; import type {BankName} from './Bank'; @@ -28,6 +29,15 @@ type BankAccountAdditionalData = { /** Is billing card */ isBillingCard?: boolean; + + /** ID of related policy */ + policyID?: string; + + /** Corpay fields */ + corpay?: { + /** Powerform files */ + achAuthorizationForm?: FileObject[]; + }; }; /** Model of bank account */