From 52c93d8146f7bf0959e8e238733a3b8517c40545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Fri, 25 Jul 2025 16:04:59 +0200 Subject: [PATCH 01/18] feat: Enable Globalreimbursements for USD accounts --- src/CONST/index.ts | 7 + src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../SubStepForms/AgreementsFullStep/index.tsx | 109 +++++++++++++ .../subSteps/Confirmation.tsx | 143 ++++++++++++++++++ .../DocusignFullStep/Docusign.tsx | 104 +++++++++++++ .../subSteps/UploadPowerform.tsx | 122 +++++++++++++++ src/languages/en.ts | 1 + ...alReimbursementsForUSDBankAccountParams.ts | 9 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 5 +- .../ModalStackNavigators/index.tsx | 1 + .../RELATIONS/SETTINGS_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 4 + src/libs/Navigation/types.ts | 3 + src/libs/actions/BankAccounts.ts | 39 +++++ .../Agreements/index.tsx | 35 +++++ .../Docusign/index.tsx | 53 +++++++ .../EnableGlobalReimbursements/index.tsx | 54 +++++++ .../settings/Wallet/WalletPage/WalletPage.tsx | 20 +++ .../form/EnableGlobalReimbursementsForm.ts | 28 ++++ src/types/form/index.ts | 1 + src/types/onyx/BankAccount.ts | 3 + 24 files changed, 750 insertions(+), 1 deletion(-) create mode 100644 src/components/SubStepForms/AgreementsFullStep/index.tsx create mode 100644 src/components/SubStepForms/AgreementsFullStep/subSteps/Confirmation.tsx create mode 100644 src/components/SubStepForms/DocusignFullStep/Docusign.tsx create mode 100644 src/components/SubStepForms/DocusignFullStep/subSteps/UploadPowerform.tsx create mode 100644 src/libs/API/parameters/EnableGlobalReimbursementsForUSDBankAccountParams.ts create mode 100644 src/pages/settings/Wallet/EnableGlobalReimbursements/Agreements/index.tsx create mode 100644 src/pages/settings/Wallet/EnableGlobalReimbursements/Docusign/index.tsx create mode 100644 src/pages/settings/Wallet/EnableGlobalReimbursements/index.tsx create mode 100644 src/types/form/EnableGlobalReimbursementsForm.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 4edcd477c26b..c887a8a858da 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -628,6 +628,13 @@ const CONST = { }, BANK_INFO_STEP_ACCOUNT_HOLDER_KEY_PREFIX: 'accountHolder', }, + ENABLE_GLOBAL_REIMBURSEMENTS: { + STEP_NAMES: ['1', '2'], + STEP: { + AGREEMENTS: 'AgreementsStep', + DOCUSIGN: 'DocusignStep', + }, + }, INCORPORATION_TYPES: { LLC: 'LLC', CORPORATION: 'Corp', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d550a3bf6993..ee0cc4bbaada 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -847,6 +847,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', @@ -956,6 +958,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 6da5dadba9cc..f8549fc28d10 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -272,6 +272,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/:policyID/enable-global-reimbursements', + getRoute: (policyID: string | undefined) => `settings/wallet/${policyID}/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 b14fdfdf1615..98388fcbbb35 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -132,6 +132,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..c8e72017d155 --- /dev/null +++ b/src/components/SubStepForms/AgreementsFullStep/index.tsx @@ -0,0 +1,109 @@ +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 = { + /** The ID of the form */ + formID: TFormID; + + /** Input IDs for field in the form */ + inputIDs: { + provideTruthfulInformation: FormOnyxKeys; + agreeToTermsAndConditions: FormOnyxKeys; + consentToPrivacyNotice: FormOnyxKeys; + authorizedToBindClientToAgreement: FormOnyxKeys; + }; + + /** Handles back button press */ + onBackButtonPress: () => void; + + /** Handles submit button press */ + onSubmit: () => void; + + /** Array of step names */ + stepNames?: readonly string[]; + + /** Index of currently active step in header */ + startStepIndex: number; + + /** Currency of the policy */ + policyCurrency: string | undefined; +}; + +type AgreementsFullStepSubStepProps = SubStepProps & { + policyCurrency: string | undefined; + + /** The ID of the form */ + formID: TFormID; + + /** Input IDs for field in the form */ + inputIDs: AgreementsFullStepProps['inputIDs']; +}; + +function AgreementsFullStep({ + formID, + inputIDs, + onBackButtonPress, + onSubmit, + stepNames, + startStepIndex, + policyCurrency, +}: 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/components/SubStepForms/AgreementsFullStep/subSteps/Confirmation.tsx b/src/components/SubStepForms/AgreementsFullStep/subSteps/Confirmation.tsx new file mode 100644 index 000000000000..2cb0c9ffca9e --- /dev/null +++ b/src/components/SubStepForms/AgreementsFullStep/subSteps/Confirmation.tsx @@ -0,0 +1,143 @@ +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, FormOnyxKeys, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +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 type {OnyxFormValuesMapping} from '@src/ONYXKEYS'; + +function IsAuthorizedToUseBankAccountLabel() { + const {translate} = useLocalize(); + return {translate('agreementsStep.iAmAuthorized')}; +} + +function CertifyTrueAndAccurateLabel() { + const {translate} = useLocalize(); + return {translate('agreementsStep.iCertify')}; +} + +function TermsAndConditionsLabel() { + const {translate} = useLocalize(); + return ( + + {translate('common.iAcceptThe')} + {`${translate('agreementsStep.termsAndConditions')}`}. + + ); +} + +function ConsentToPrivacyNoticeLabel() { + const {translate} = useLocalize(); + return ( + + {translate('agreementsStep.iConsentToThe')} {`${translate('agreementsStep.privacyNotice')}`}. + + ); +} + +type ConfirmationProps = SubStepProps & { + policyCurrency: string | undefined; + + /** The ID of the form */ + formID: TFormID; + + /** Input IDs for field in the form */ + inputIDs: { + provideTruthfulInformation: FormOnyxKeys; + agreeToTermsAndConditions: FormOnyxKeys; + consentToPrivacyNotice: FormOnyxKeys; + authorizedToBindClientToAgreement: FormOnyxKeys; + }; +}; + +function Confirmation({onNext, policyCurrency, formID, inputIDs}: ConfirmationProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const isDocusignStepRequired = requiresDocusignStep(policyCurrency); + + const stepFields = useMemo( + () => [inputIDs.authorizedToBindClientToAgreement, inputIDs.provideTruthfulInformation, inputIDs.agreeToTermsAndConditions, inputIDs.consentToPrivacyNotice], + [inputIDs], + ); + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = getFieldRequiredErrors(values, stepFields); + + if (!isRequiredFulfilled(values[inputIDs.authorizedToBindClientToAgreement] as string)) { + errors[inputIDs.authorizedToBindClientToAgreement] = translate('agreementsStep.error.authorized'); + } + + if (!isRequiredFulfilled(values[inputIDs.provideTruthfulInformation] as string)) { + errors[inputIDs.provideTruthfulInformation] = translate('agreementsStep.error.certify'); + } + + if (!isRequiredFulfilled(values[inputIDs.agreeToTermsAndConditions] as string)) { + errors[inputIDs.agreeToTermsAndConditions] = translate('common.error.acceptTerms'); + } + + if (!isRequiredFulfilled(values[inputIDs.consentToPrivacyNotice] as string)) { + errors[inputIDs.consentToPrivacyNotice] = translate('agreementsStep.error.consent'); + } + + return errors; + }, + [inputIDs.agreeToTermsAndConditions, inputIDs.authorizedToBindClientToAgreement, inputIDs.consentToPrivacyNotice, inputIDs.provideTruthfulInformation, stepFields, translate], + ); + + return ( + + {translate('agreementsStep.pleaseConfirm')} + {!isDocusignStepRequired && {translate('agreementsStep.regulationRequiresUs')}} + + + + + + ); +} + +Confirmation.displayName = 'Confirmation'; + +export default Confirmation; diff --git a/src/components/SubStepForms/DocusignFullStep/Docusign.tsx b/src/components/SubStepForms/DocusignFullStep/Docusign.tsx new file mode 100644 index 000000000000..50b8d4996a87 --- /dev/null +++ b/src/components/SubStepForms/DocusignFullStep/Docusign.tsx @@ -0,0 +1,104 @@ +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 UploadPowerform from './subSteps/UploadPowerform'; + +type DocusignFullStepProps = { + /** The ID of the form */ + formID: TFormID; + + /** ID of the input in the form */ + inputID: FormOnyxKeys; + + /** Handles back button press */ + onBackButtonPress: () => void; + + /** Handles submit button press */ + onSubmit: () => void; + + /** Array of step names */ + stepNames?: readonly string[]; + + /** Index of currently active step in header */ + startStepIndex: number; + + /** Currency of the policy */ + policyCurrency: string | undefined; +}; + +type DocusignFullStepStepProps = SubStepProps & { + policyCurrency: string | undefined; + + /** The ID of the form */ + formID: TFormID; + + /** ID of the input in the form */ + inputID: FormOnyxKeys; +}; + +function DocusignFullStep({ + onBackButtonPress, + onSubmit, + stepNames, + formID, + startStepIndex, + policyCurrency, + inputID, +}: 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/components/SubStepForms/DocusignFullStep/subSteps/UploadPowerform.tsx b/src/components/SubStepForms/DocusignFullStep/subSteps/UploadPowerform.tsx new file mode 100644 index 000000000000..375dad73e46c --- /dev/null +++ b/src/components/SubStepForms/DocusignFullStep/subSteps/UploadPowerform.tsx @@ -0,0 +1,122 @@ +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, 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 {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 type {OnyxFormValuesMapping} from '@src/ONYXKEYS'; + +type UploadPowerformProps = SubStepProps & { + policyCurrency: string | undefined; + + /** The ID of the form */ + formID: TFormID; + + /** ID of the input in the form */ + inputID: FormOnyxKeys; +}; +function UploadPowerform({onNext, isEditing, policyCurrency, formID, inputID}: UploadPowerformProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {environmentURL} = useEnvironment(); + + 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 handleSelectFile = (files: FileObject[]) => { + setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[ACH_AUTHORIZATION_FORM]: [...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}); + setUploadedFiles(newUploadedFiles); + }; + + const setUploadError = (error: string) => { + if (!error) { + clearErrorFields(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM); + return; + } + + setErrorFields(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[ACH_AUTHORIZATION_FORM]: {onUpload: error}}); + }; + + return ( + + {countryStepCountryValue === CONST.COUNTRY.CA && {translate('docusignStep.pleaseComplete')}} + {countryStepCountryValue === CONST.COUNTRY.AU && ( + <> + {translate('docusignStep.pleaseCompleteTheBusinessAccount')} + {translate('docusignStep.pleaseCompleteTheDirect')} + + )} +