Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
52c93d8
feat: Enable Globalreimbursements for USD accounts
MrMuzyk Jul 25, 2025
ef07981
Merge branch 'main' of https://github.com/Expensify/App into feat/669…
MrMuzyk Jul 28, 2025
ddd661b
feat: sorted out DocusignFullStep and some basic cleanup
MrMuzyk Jul 28, 2025
53f3c8e
feat: somewhat done
MrMuzyk Jul 29, 2025
f45210a
Merge branch 'main' of https://github.com/Expensify/App into feat/669…
MrMuzyk Jul 31, 2025
1538745
fix: minor type fxies
MrMuzyk Jul 31, 2025
2c11403
Merge branch 'main' of https://github.com/Expensify/App into feat/669…
MrMuzyk Aug 11, 2025
7f48ad3
feat: Add one more step to enabling GRs for USD
MrMuzyk Aug 12, 2025
1b11d59
fix: replace policyID with bankAccountID
MrMuzyk Aug 13, 2025
59374fa
fix: cleanup
MrMuzyk Aug 13, 2025
67c839b
Merge branch 'main' of https://github.com/Expensify/App into feat/669…
MrMuzyk Aug 13, 2025
4cab875
fix: linter
MrMuzyk Aug 13, 2025
d77f3a2
fix: spanish translation
MrMuzyk Aug 13, 2025
2d5344a
Merge branch 'main' of https://github.com/Expensify/App into feat/669…
MrMuzyk Aug 18, 2025
4db015b
fix: CR fixes
MrMuzyk Aug 18, 2025
07404e3
fix: linter
MrMuzyk Aug 18, 2025
1492224
fix: clear errors on component unmounting
MrMuzyk Aug 18, 2025
4687bbe
fix: small correction
MrMuzyk Aug 18, 2025
0fc4857
fix: add spinner to the agreements step
MrMuzyk Aug 19, 2025
a233e0a
Merge branch 'main' of https://github.com/Expensify/App into feat/669…
MrMuzyk Aug 19, 2025
83c96f9
fix: typecheck and crash
MrMuzyk Aug 20, 2025
13b336f
Merge branch 'main' of https://github.com/Expensify/App into feat/669…
MrMuzyk Aug 20, 2025
2ee7a99
align validation with corpays
MrMuzyk Aug 20, 2025
3924e59
fix: improved continue step
MrMuzyk Aug 20, 2025
bb6b983
Merge branch 'main' of https://github.com/Expensify/App into feat/669…
MrMuzyk Aug 25, 2025
9d3ac5a
feat: Hide behind beta
MrMuzyk Aug 25, 2025
710e35c
Merge branch 'main' of https://github.com/Expensify/App into feat/669…
MrMuzyk Aug 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 = {
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
126 changes: 126 additions & 0 deletions src/components/SubStepForms/AgreementsFullStep/index.tsx
Original file line number Diff line number Diff line change
@@ -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<TFormID extends keyof OnyxFormValuesMapping> = {
/** Default values for inputs */
defaultValues: Partial<Record<FormOnyxKeys<TFormID>, boolean>>;

/** The ID of the form */
formID: TFormID;

/** Input IDs for field in the form */
inputIDs: {
provideTruthfulInformation: FormOnyxKeys<TFormID>;
agreeToTermsAndConditions: FormOnyxKeys<TFormID>;
consentToPrivacyNotice: FormOnyxKeys<TFormID>;
authorizedToBindClientToAgreement: FormOnyxKeys<TFormID>;
};

/** 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<TFormID extends keyof OnyxFormValuesMapping> = SubStepProps & {
/** Default values for inputs */
defaultValues: AgreementsFullStepProps<TFormID>['defaultValues'];

/** The ID of the form */
formID: TFormID;

/** Input IDs for field in the form */
inputIDs: AgreementsFullStepProps<TFormID>['inputIDs'];

/** Indicates that action is being processed */
isLoading: boolean;

/** Currency of related account */
currency: string;
};

function AgreementsFullStep<TFormID extends keyof OnyxFormValuesMapping>({
defaultValues,
formID,
inputIDs,
isLoading,
onBackButtonPress,
onSubmit,
currency,
stepNames,
startStepIndex,
}: AgreementsFullStepProps<TFormID>) {
const {translate} = useLocalize();

const bodyContent: Array<ComponentType<AgreementsFullStepSubStepProps<TFormID>>> = [Confirmation];

const {
componentToRender: SubStep,
isEditing,
screenIndex,
nextScreen,
prevScreen,
moveTo,
goToTheLastStep,
} = useSubStep<AgreementsFullStepSubStepProps<TFormID>>({bodyContent, startFrom: 0, onFinished: onSubmit});

const handleBackButtonPress = () => {
clearErrors(formID);
if (isEditing) {
goToTheLastStep();
return;
}

if (screenIndex === 0) {
onBackButtonPress();
} else {
prevScreen();
}
};

return (
<InteractiveStepWrapper
wrapperID={AgreementsFullStep.displayName}
handleBackButtonPress={handleBackButtonPress}
headerTitle={translate('agreementsStep.agreements')}
stepNames={stepNames}
startStepIndex={startStepIndex}
>
<SubStep
defaultValues={defaultValues}
formID={formID}
inputIDs={inputIDs}
isEditing={isEditing}
isLoading={isLoading}
onMove={moveTo}
onNext={nextScreen}
currency={currency}
/>
</InteractiveStepWrapper>
);
}

AgreementsFullStep.displayName = 'AgreementsFullStep';

export default AgreementsFullStep;
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -38,95 +32,110 @@ function ConsentToPrivacyNoticeLabel() {
return <RenderHTML html={translate('agreementsStep.iConsentToThePrivacyNotice')} />;
}

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<TFormID extends keyof OnyxFormValuesMapping> = SubStepProps & {
/** Default values for inputs */
defaultValues: Partial<Record<FormOnyxKeys<TFormID>, boolean>>;

/** The ID of the form */
formID: TFormID;

/** Input IDs for field in the form */
inputIDs: {
provideTruthfulInformation: FormOnyxKeys<TFormID>;
agreeToTermsAndConditions: FormOnyxKeys<TFormID>;
consentToPrivacyNotice: FormOnyxKeys<TFormID>;
authorizedToBindClientToAgreement: FormOnyxKeys<TFormID>;
};

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<TFormID extends keyof OnyxFormValuesMapping>({defaultValues, formID, inputIDs, isLoading, onNext, currency}: ConfirmationProps<TFormID>) {
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<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM> => {
const errors = getFieldRequiredErrors(values, STEP_FIELDS);
(values: FormOnyxValues<TFormID>): FormInputErrors<TFormID> => {
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 (
<FormProvider
formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
formID={formID}
onSubmit={onNext}
validate={validate}
submitButtonText={isDocusignStepRequired ? translate('common.confirm') : translate('agreementsStep.accept')}
style={[styles.mh5, styles.flexGrow1]}
enabledWhenOffline={false}
isLoading={isLoading}
>
<Text style={[styles.textHeadlineLineHeightXXL]}>{translate('agreementsStep.pleaseConfirm')}</Text>
{!isDocusignStepRequired && <Text style={[styles.pv3, styles.textSupporting]}>{translate('agreementsStep.regulationRequiresUs')}</Text>}
<InputWrapper
InputComponent={CheckboxWithLabel}
accessibilityLabel={translate('agreementsStep.iAmAuthorized')}
inputID={AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT}
inputID={inputIDs.authorizedToBindClientToAgreement as string}
style={styles.mt6}
LabelComponent={IsAuthorizedToUseBankAccountLabel}
defaultValue={agreementsStepValues[AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT]}
defaultValue={defaultValues[inputIDs.authorizedToBindClientToAgreement]}
shouldSaveDraft
/>
<InputWrapper
InputComponent={CheckboxWithLabel}
accessibilityLabel={translate('agreementsStep.iCertify')}
inputID={PROVIDE_TRUTHFUL_INFORMATION}
inputID={inputIDs.provideTruthfulInformation as string}
style={styles.mt6}
LabelComponent={CertifyTrueAndAccurateLabel}
defaultValue={agreementsStepValues[PROVIDE_TRUTHFUL_INFORMATION]}
defaultValue={defaultValues[inputIDs.provideTruthfulInformation]}
shouldSaveDraft
/>
<InputWrapper
InputComponent={CheckboxWithLabel}
accessibilityLabel={translate('agreementsStep.iAcceptTheTermsAndConditionsAccessibility')}
inputID={AGREE_TO_TERMS_AND_CONDITIONS}
inputID={inputIDs.agreeToTermsAndConditions as string}
style={styles.mt6}
LabelComponent={TermsAndConditionsLabel}
defaultValue={agreementsStepValues[AGREE_TO_TERMS_AND_CONDITIONS]}
defaultValue={defaultValues[inputIDs.agreeToTermsAndConditions]}
shouldSaveDraft
/>
<InputWrapper
InputComponent={CheckboxWithLabel}
accessibilityLabel={translate('agreementsStep.iConsentToThePrivacyNoticeAccessibility')}
inputID={CONSENT_TO_PRIVACY_NOTICE}
inputID={inputIDs.consentToPrivacyNotice as string}
style={styles.mt6}
LabelComponent={ConsentToPrivacyNoticeLabel}
defaultValue={agreementsStepValues[CONSENT_TO_PRIVACY_NOTICE]}
defaultValue={defaultValues[inputIDs.consentToPrivacyNotice]}
shouldSaveDraft
/>
</FormProvider>
Expand Down
Loading
Loading