;
+type ActionWithPayload = {
+ type: string;
+ payload?: P;
+};
+type StateHolder
= {
+ state: string;
+ payload: P | null;
+};
+type State
= {
+ previous: StateHolder
;
+ current: StateHolder
;
+};
+
+/**
+ * Represents the state machine configuration as a nested record where:
+ * - The first level keys are the state names.
+ * - The second level keys are the action types valid for that state.
+ * - The corresponding values are the next states to transition to when the action is triggered.
+ */
+type StateMachine = Record>;
+
+// eslint-disable-next-line @typescript-eslint/unbound-method
+const client = Log.client;
+
+/**
+ * A hook that creates a state machine that can be used with Reanimated Worklets.
+ * You can transition state from worklet or from the JS thread.
+ *
+ * State machines are helpful for managing complex UI interactions. We want to transition
+ * between states based on user actions. But also we want to ignore some actions
+ * when we are in certain states.
+ *
+ * For example:
+ * 1. Initial state is idle. It can react to KEYBOARD_OPEN action.
+ * 2. We open emoji picker. It sends EMOJI_PICKER_OPEN action.
+ * 2. There is no handling for this action in idle state so we do nothing.
+ * 3. We close emoji picker and it sends EMOJI_PICKER_CLOSE action which again does nothing.
+ * 4. We open keyboard. It sends KEYBOARD_OPEN action. idle can react to this action
+ * by transitioning into keyboardOpen state
+ * 5. Our state is keyboardOpen. It can react to KEYBOARD_CLOSE, EMOJI_PICKER_OPEN actions
+ * 6. We open emoji picker again. It sends EMOJI_PICKER_OPEN action which transitions our state
+ * into emojiPickerOpen state. Now we react only to EMOJI_PICKER_CLOSE action.
+ * 7. Before rendering the emoji picker, the app hides the keyboard.
+ * It sends KEYBOARD_CLOSE action. But we ignore it since our emojiPickerOpen state can only handle
+ * EMOJI_PICKER_CLOSE action. So we write the logic for handling hiding the keyboard,
+ * but maintaining the offset based on the keyboard state shared value
+ * 7. We close the picker and send EMOJI_PICKER_CLOSE action which transitions us back into keyboardOpen state.
+ *
+ * State machine object example:
+ * const stateMachine = {
+ * idle: {
+ * KEYBOARD_OPEN: 'keyboardOpen',
+ * },
+ * keyboardOpen: {
+ * KEYBOARD_CLOSE: 'idle',
+ * EMOJI_PICKER_OPEN: 'emojiPickerOpen',
+ * },
+ * emojiPickerOpen: {
+ * EMOJI_PICKER_CLOSE: 'keyboardOpen',
+ * },
+ * }
+ *
+ * Initial state example:
+ * {
+ * previous: null,
+ * current: {
+ * state: 'idle',
+ * payload: null,
+ * },
+ * }
+ *
+ * @param stateMachine - a state machine object
+ * @param initialState - the initial state of the state machine
+ * @returns an object containing the current state, a transition function, and a reset function
+ */
+function useWorkletStateMachine(stateMachine: StateMachine, initialState: State
) {
+ const currentState = useSharedValue(initialState);
+
+ const log = useCallback((message: string, params?: P | null) => {
+ 'worklet';
+
+ if (!DEBUG_MODE) {
+ return;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/unbound-method, @typescript-eslint/restrict-template-expressions
+ runOnJS(client)(`[StateMachine] ${message}. Params: ${JSON.stringify(params)}`);
+ }, []);
+
+ const transitionWorklet = useCallback(
+ (action: ActionWithPayload
) => {
+ 'worklet';
+
+ if (!action) {
+ throw new Error('state machine action is required');
+ }
+
+ const state = currentState.get();
+
+ log(`Current STATE: ${state.current.state}`);
+ log(`Next ACTION: ${action.type}`, action.payload);
+
+ const nextMachine = stateMachine[state.current.state];
+
+ if (!nextMachine) {
+ log(`No next machine found for state: ${state.current.state}`);
+ return;
+ }
+
+ const nextState = nextMachine[action.type];
+
+ if (!nextState) {
+ log(`No next state found for action: ${action.type}`);
+ return;
+ }
+
+ let nextPayload;
+
+ if (typeof action.payload === 'undefined') {
+ // we save previous payload
+ nextPayload = state.current.payload;
+ } else {
+ // we merge previous payload with the new payload
+ nextPayload = {
+ ...state.current.payload,
+ ...action.payload,
+ };
+ }
+
+ log(`Next STATE: ${nextState}`, nextPayload);
+
+ currentState.set({
+ previous: state.current,
+ current: {
+ state: nextState,
+ payload: nextPayload,
+ },
+ });
+ },
+ [currentState, log, stateMachine],
+ );
+
+ const resetWorklet = useCallback(() => {
+ 'worklet';
+
+ log('RESET STATE MACHINE');
+ // eslint-disable-next-line react-compiler/react-compiler
+ currentState.set(initialState);
+ }, [currentState, initialState, log]);
+
+ const reset = useCallback(() => {
+ runOnUI(resetWorklet)();
+ }, [resetWorklet]);
+
+ const transition = useCallback(
+ (action: ActionWithPayload
) => {
+ executeOnUIRuntimeSync(transitionWorklet)(action);
+ },
+ [transitionWorklet],
+ );
+
+ return {
+ currentState,
+ transitionWorklet,
+ transition,
+ reset,
+ };
+}
+
+export type {ActionWithPayload, State};
+export default useWorkletStateMachine;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 3d35fed3e0a4..5d9f25404d88 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -517,6 +517,7 @@ const translations = {
chooseDocument: 'Choose file',
attachmentTooLarge: 'Attachment is too large',
sizeExceeded: 'Attachment size is larger than 24 MB limit',
+ sizeExceededWithLimit: ({maxUploadSizeInMB}: SizeExceededParams) => `Attachment size is larger than ${maxUploadSizeInMB} MB limit`,
attachmentTooSmall: 'Attachment is too small',
sizeNotMet: 'Attachment size must be greater than 240 bytes',
wrongFileType: 'Invalid file type',
@@ -1753,6 +1754,7 @@ const translations = {
},
onboarding: {
welcome: 'Welcome!',
+ welcomeSignOffTitle: "It's great to meet you!",
explanationModal: {
title: 'Welcome to Expensify',
description: 'One app to handle your business and personal spend at the speed of chat. Try it out and let us know what you think. Much more to come!',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index cb7f53424958..3ce50b3c5bf7 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -512,6 +512,7 @@ const translations = {
chooseDocument: 'Elegir un archivo',
attachmentTooLarge: 'Archivo adjunto demasiado grande',
sizeExceeded: 'El archivo adjunto supera el límite de 24 MB.',
+ sizeExceededWithLimit: ({maxUploadSizeInMB}: SizeExceededParams) => `El archivo adjunto supera el límite de ${maxUploadSizeInMB} MB.`,
attachmentTooSmall: 'Archivo adjunto demasiado pequeño',
sizeNotMet: 'El archivo adjunto debe ser más grande que 240 bytes.',
wrongFileType: 'Tipo de archivo inválido',
@@ -1756,6 +1757,7 @@ const translations = {
},
onboarding: {
welcome: '¡Bienvenido!',
+ welcomeSignOffTitle: '¡Es un placer conocerte!',
explanationModal: {
title: 'Bienvenido a Expensify',
description: 'Una aplicación para gestionar en un chat todos los gastos de tu empresa y personales. Inténtalo y dinos qué te parece. ¡Hay mucho más por venir!',
diff --git a/src/libs/API/parameters/BankAccountCreateCorpayParams.ts b/src/libs/API/parameters/BankAccountCreateCorpayParams.ts
new file mode 100644
index 000000000000..3c617d326009
--- /dev/null
+++ b/src/libs/API/parameters/BankAccountCreateCorpayParams.ts
@@ -0,0 +1,8 @@
+type BankAccountCreateCorpayParams = {
+ type: number;
+ isSavings: boolean;
+ isWithdrawal: boolean;
+ inputs: string;
+};
+
+export default BankAccountCreateCorpayParams;
diff --git a/src/libs/API/parameters/GetCorpayBankAccountFieldsParams.ts b/src/libs/API/parameters/GetCorpayBankAccountFieldsParams.ts
new file mode 100644
index 000000000000..3e02b57f9e12
--- /dev/null
+++ b/src/libs/API/parameters/GetCorpayBankAccountFieldsParams.ts
@@ -0,0 +1,8 @@
+type GetCorpayBankAccountFieldsParams = {
+ countryISO: string;
+ currency: string;
+ isWithdrawal: boolean;
+ isBusinessBankAccount: boolean;
+};
+
+export default GetCorpayBankAccountFieldsParams;
diff --git a/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts b/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts
index 6ef6b3712439..5b7a221a8702 100644
--- a/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts
+++ b/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts
@@ -1,6 +1,6 @@
type VerifyIdentityForBankAccountParams = {
bankAccountID: number;
onfidoData: string;
- policyID: string;
+ policyID?: string;
};
export default VerifyIdentityForBankAccountParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 7e8b8cec520b..ea2d9893cf24 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -8,6 +8,7 @@ export type {default as RestartBankAccountSetupParams} from './RestartBankAccoun
export type {default as AddSchoolPrincipalParams} from './AddSchoolPrincipalParams';
export type {default as AuthenticatePusherParams} from './AuthenticatePusherParams';
export type {default as BankAccountHandlePlaidErrorParams} from './BankAccountHandlePlaidErrorParams';
+export type {default as BankAccountCreateCorpayParams} from './BankAccountCreateCorpayParams';
export type {default as BeginAppleSignInParams} from './BeginAppleSignInParams';
export type {default as BeginGoogleSignInParams} from './BeginGoogleSignInParams';
export type {default as BeginSignInParams} from './BeginSignInParams';
@@ -29,6 +30,7 @@ export type {default as ExpandURLPreviewParams} from './ExpandURLPreviewParams';
export type {default as GetMissingOnyxMessagesParams} from './GetMissingOnyxMessagesParams';
export type {default as GetNewerActionsParams} from './GetNewerActionsParams';
export type {default as GetOlderActionsParams} from './GetOlderActionsParams';
+export type {default as GetCorpayBankAccountFieldsParams} from './GetCorpayBankAccountFieldsParams';
export type {default as GetPolicyCategoriesParams} from './GetPolicyCategories';
export type {default as GetReportPrivateNoteParams} from './GetReportPrivateNoteParams';
export type {default as GetRouteParams} from './GetRouteParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index aa9831ca4053..7b8c6df92a22 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -440,6 +440,7 @@ const WRITE_COMMANDS = {
SELF_TOUR_VIEWED: 'SelfTourViewed',
UPDATE_INVOICE_COMPANY_NAME: 'UpdateInvoiceCompanyName',
UPDATE_INVOICE_COMPANY_WEBSITE: 'UpdateInvoiceCompanyWebsite',
+ BANK_ACCOUNT_CREATE_CORPAY: 'BankAccount_CreateCorpay',
UPDATE_WORKSPACE_CUSTOM_UNIT: 'UpdateWorkspaceCustomUnit',
VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'ValidateUserAndGetAccessiblePolicies',
DISMISS_PRODUCT_TRAINING: 'DismissProductTraining',
@@ -774,6 +775,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams;
[WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH]: Parameters.ApproveMoneyRequestOnSearchParams;
[WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.UnholdMoneyRequestOnSearchParams;
+ [WRITE_COMMANDS.BANK_ACCOUNT_CREATE_CORPAY]: Parameters.BankAccountCreateCorpayParams;
[WRITE_COMMANDS.REQUEST_REFUND]: null;
[WRITE_COMMANDS.CONNECT_POLICY_TO_SAGE_INTACCT]: Parameters.ConnectPolicyToSageIntacctParams;
@@ -902,6 +904,7 @@ type WriteCommandParameters = {
};
const READ_COMMANDS = {
+ GET_CORPAY_BANK_ACCOUNT_FIELDS: 'GetCorpayBankAccountFields',
CONNECT_POLICY_TO_QUICKBOOKS_ONLINE: 'ConnectPolicyToQuickbooksOnline',
CONNECT_POLICY_TO_XERO: 'ConnectPolicyToXero',
SYNC_POLICY_TO_QUICKBOOKS_ONLINE: 'SyncPolicyToQuickbooksOnline',
@@ -983,6 +986,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.OPEN_PLAID_BANK_ACCOUNT_SELECTOR]: Parameters.OpenPlaidBankAccountSelectorParams;
[READ_COMMANDS.GET_OLDER_ACTIONS]: Parameters.GetOlderActionsParams;
[READ_COMMANDS.GET_NEWER_ACTIONS]: Parameters.GetNewerActionsParams;
+ [READ_COMMANDS.GET_CORPAY_BANK_ACCOUNT_FIELDS]: Parameters.GetCorpayBankAccountFieldsParams;
[READ_COMMANDS.EXPAND_URL_PREVIEW]: Parameters.ExpandURLPreviewParams;
[READ_COMMANDS.GET_REPORT_PRIVATE_NOTE]: Parameters.GetReportPrivateNoteParams;
[READ_COMMANDS.OPEN_ROOM_MEMBERS_PAGE]: Parameters.OpenRoomMembersPageParams;
diff --git a/src/libs/MoneyRequestUtils.ts b/src/libs/MoneyRequestUtils.ts
index d76c9325cc0e..7009379e15de 100644
--- a/src/libs/MoneyRequestUtils.ts
+++ b/src/libs/MoneyRequestUtils.ts
@@ -50,6 +50,9 @@ function validateAmount(amount: string, decimals: number, amountMaxLength: numbe
? `^${shouldAllowNegative ? '-?' : ''}\\d{1,${amountMaxLength}}$` // Don't allow decimal point if decimals === 0
: `^${shouldAllowNegative ? '-?' : ''}\\d{1,${amountMaxLength}}(\\.\\d{0,${decimals}})?$`; // Allow the decimal point and the desired number of digits after the point
const decimalNumberRegex = new RegExp(regexString, 'i');
+ if (shouldAllowNegative) {
+ return amount === '' || amount === '-' || decimalNumberRegex.test(amount);
+ }
return amount === '' || decimalNumberRegex.test(amount);
}
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index a482abeb76d9..569dca63bab7 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -1052,8 +1052,8 @@ function isSettled(reportOrID: OnyxInputOrEntry | SearchReport | string
/**
* Whether the current user is the submitter of the report
*/
-function isCurrentUserSubmitter(reportID: string): boolean {
- if (!allReports) {
+function isCurrentUserSubmitter(reportID: string | undefined): boolean {
+ if (!allReports || !reportID) {
return false;
}
const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
@@ -1455,7 +1455,7 @@ function getMostRecentlyVisitedReport(reports: Array>, reportM
const shouldKeep = !isChatThread(report) || !isHiddenForCurrentUser(report);
return shouldKeep && !!report?.reportID && !!(reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`]?.lastVisitTime ?? report?.lastReadTime);
});
- return lodashMaxBy(filteredReports, (a) => new Date(reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${a?.reportID}`]?.lastVisitTime ?? a?.lastReadTime ?? '').valueOf());
+ return lodashMaxBy(filteredReports, (a) => [reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${a?.reportID}`]?.lastVisitTime ?? '', a?.lastReadTime ?? '']);
}
function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = false, policyID?: string, excludeReportID?: string): OnyxEntry {
@@ -6790,7 +6790,10 @@ function getInvoiceChatByParticipants(receiverID: string | number, receiverType:
/**
* Attempts to find a policy expense report in onyx that is owned by ownerAccountID in a given policy
*/
-function getPolicyExpenseChat(ownerAccountID: number, policyID: string): OnyxEntry {
+function getPolicyExpenseChat(ownerAccountID: number, policyID: string | undefined): OnyxEntry {
+ if (!policyID) {
+ return;
+ }
return Object.values(allReports ?? {}).find((report: OnyxEntry) => {
// If the report has been deleted, then skip it
if (!report) {
diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js
index 47b534d32cad..941ac7f59797 100644
--- a/src/libs/SearchParser/searchParser.js
+++ b/src/libs/SearchParser/searchParser.js
@@ -298,7 +298,7 @@ function peg$parse(input, options) {
const keywordFilter = buildFilter(
"eq",
"keyword",
- keywords.map((filter) => filter.right).flat()
+ keywords.map((filter) => filter.right.replace(/^(['"])(.*)\1$/, '$2')).flat()
);
if (keywordFilter.right.length > 0) {
nonKeywords.push(keywordFilter);
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index 68b3ce60963a..849008e10b76 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -66,7 +66,7 @@ function getTransactionItemCommonFormattedProperties(
const formattedTotal = TransactionUtils.getAmount(transactionItem, isExpenseReport);
const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created;
const merchant = TransactionUtils.getMerchant(transactionItem);
- const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT ? '' : merchant;
+ const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ? '' : merchant;
return {
formattedFrom,
@@ -106,7 +106,7 @@ function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean {
if (isTransactionEntry(key)) {
const item = data[key];
const merchant = item.modifiedMerchant ? item.modifiedMerchant : item.merchant ?? '';
- return merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchant !== CONST.TRANSACTION.DEFAULT_MERCHANT;
+ return merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
}
return false;
});
@@ -374,7 +374,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
...reportItem,
action: getAction(data, key),
keyForList: reportItem.reportID,
- from: data.personalDetailsList?.[reportItem.accountID ?? -1],
+ from: data.personalDetailsList?.[reportItem.accountID ?? CONST.DEFAULT_NUMBER_ID],
to: reportItem.managerID ? data.personalDetailsList?.[reportItem.managerID] : emptyPersonalDetails,
transactions,
reportName: isIOUReport ? getIOUReportName(data, reportItem) : reportItem.reportName,
diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts
index 19245ba715f1..09a1f0b4f8fd 100644
--- a/src/libs/actions/BankAccounts.ts
+++ b/src/libs/actions/BankAccounts.ts
@@ -20,7 +20,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
import type {PersonalBankAccountForm} from '@src/types/form';
-import type {ACHContractStepProps, BeneficialOwnersStepProps, CompanyStepProps, RequestorStepProps} from '@src/types/form/ReimbursementAccountForm';
+import type {ACHContractStepProps, BeneficialOwnersStepProps, CompanyStepProps, ReimbursementAccountForm, RequestorStepProps} from '@src/types/form/ReimbursementAccountForm';
import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount';
import type {BankAccountStep, ReimbursementAccountStep, ReimbursementAccountSubStep} from '@src/types/onyx/ReimbursementAccount';
import type {OnyxData} from '@src/types/onyx/Request';
@@ -334,7 +334,7 @@ function validateBankAccount(bankAccountID: number, validateCode: string, policy
key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
value: {
isLoading: false,
- errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('bankAccount.error.validationAmounts'),
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
},
},
],
@@ -344,8 +344,6 @@ function validateBankAccount(bankAccountID: number, validateCode: string, policy
}
function getCorpayBankAccountFields(country: string, currency: string) {
- // TODO - Use parameters when API is ready
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const parameters = {
countryISO: country,
currency,
@@ -353,157 +351,97 @@ function getCorpayBankAccountFields(country: string, currency: string) {
isBusinessBankAccount: true,
};
- // return API.read(READ_COMMANDS.GET_CORPAY_BANK_ACCOUNT_FIELDS, parameters);
- return {
- bankCountry: 'AU',
- bankCurrency: 'AUD',
- classification: 'Business',
- destinationCountry: 'AU',
- formFields: [
+ const onyxData: OnyxData = {
+ optimisticData: [
{
- errorMessage: 'Swift must be less than 12 characters',
- id: 'swiftBicCode',
- isRequired: false,
- isRequiredInValueSet: true,
- label: 'Swift Code',
- regEx: '^.{0,12}$',
- validationRules: [
- {
- errorMessage: 'Swift must be less than 12 characters',
- regEx: '^.{0,12}$',
- },
- {
- errorMessage: 'The following characters are not allowed: <,>, "',
- regEx: '^[^<>\\x22]*$',
- },
- ],
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CORPAY_FIELDS,
+ value: {
+ isLoading: true,
+ isSuccess: false,
+ },
},
+ ],
+ successData: [
{
- errorMessage: 'Beneficiary Bank Name must be less than 250 characters',
- id: 'bankName',
- isRequired: true,
- isRequiredInValueSet: true,
- label: 'Bank Name',
- regEx: '^.{0,250}$',
- validationRules: [
- {
- errorMessage: 'Beneficiary Bank Name must be less than 250 characters',
- regEx: '^.{0,250}$',
- },
- {
- errorMessage: 'The following characters are not allowed: <,>, "',
- regEx: '^[^<>\\x22]*$',
- },
- ],
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CORPAY_FIELDS,
+ value: {
+ isLoading: false,
+ isSuccess: true,
+ },
},
+ ],
+ failureData: [
{
- errorMessage: 'City must be less than 100 characters',
- id: 'bankCity',
- isRequired: true,
- isRequiredInValueSet: true,
- label: 'Bank City',
- regEx: '^.{0,100}$',
- validationRules: [
- {
- errorMessage: 'City must be less than 100 characters',
- regEx: '^.{0,100}$',
- },
- {
- errorMessage: 'The following characters are not allowed: <,>, "',
- regEx: '^[^<>\\x22]*$',
- },
- ],
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CORPAY_FIELDS,
+ value: {
+ isLoading: false,
+ isSuccess: false,
+ },
},
+ ],
+ };
+
+ return API.read(READ_COMMANDS.GET_CORPAY_BANK_ACCOUNT_FIELDS, parameters, onyxData);
+}
+
+function createCorpayBankAccount(fields: ReimbursementAccountForm) {
+ const parameters = {
+ type: 1,
+ isSavings: false,
+ isWithdrawal: true,
+ inputs: JSON.stringify(fields),
+ };
+
+ const onyxData: OnyxData = {
+ optimisticData: [
{
- errorMessage: 'Bank Address Line 1 must be less than 1000 characters',
- id: 'bankAddressLine1',
- isRequired: true,
- isRequiredInValueSet: true,
- label: 'Bank Address',
- regEx: '^.{0,1000}$',
- validationRules: [
- {
- errorMessage: 'Bank Address Line 1 must be less than 1000 characters',
- regEx: '^.{0,1000}$',
- },
- {
- errorMessage: 'The following characters are not allowed: <,>, "',
- regEx: '^[^<>\\x22]*$',
- },
- ],
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: true,
+ isCreateCorpayBankAccount: true,
+ },
},
+ ],
+ successData: [
{
- detailedRule: [
- {
- isRequired: true,
- value: [
- {
- errorMessage: 'Beneficiary Account Number is invalid. Value should be 1 to 50 characters long.',
- regEx: '^.{1,50}$',
- ruleDescription: '1 to 50 characters',
- },
- ],
- },
- ],
- errorMessage: 'Beneficiary Account Number is invalid. Value should be 1 to 50 characters long.',
- id: 'accountNumber',
- isRequired: true,
- isRequiredInValueSet: true,
- label: 'Account Number (iACH)',
- regEx: '^.{1,50}$',
- validationRules: [
- {
- errorMessage: 'Beneficiary Account Number is invalid. Value should be 1 to 50 characters long.',
- regEx: '^.{1,50}$',
- ruleDescription: '1 to 50 characters',
- },
- {
- errorMessage: 'The following characters are not allowed: <,>, "',
- regEx: '^[^<>\\x22]*$',
- },
- ],
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: false,
+ isCreateCorpayBankAccount: false,
+ isSuccess: true,
+ },
},
+ ],
+ failureData: [
{
- detailedRule: [
- {
- isRequired: true,
- value: [
- {
- errorMessage: 'BSB Number is invalid. Value should be exactly 6 digits long.',
- regEx: '^[0-9]{6}$',
- ruleDescription: 'Exactly 6 digits',
- },
- ],
- },
- ],
- errorMessage: 'BSB Number is invalid. Value should be exactly 6 digits long.',
- id: 'routingCode',
- isRequired: true,
- isRequiredInValueSet: true,
- label: 'BSB Number',
- regEx: '^[0-9]{6}$',
- validationRules: [
- {
- errorMessage: 'BSB Number is invalid. Value should be exactly 6 digits long.',
- regEx: '^[0-9]{6}$',
- ruleDescription: 'Exactly 6 digits',
- },
- {
- errorMessage: 'The following characters are not allowed: <,>, "',
- regEx: '^[^<>\\x22]*$',
- },
- ],
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: false,
+ isCreateCorpayBankAccount: false,
+ isSuccess: false,
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ },
},
],
- paymentMethods: ['E'],
- preferredMethod: 'E',
};
+
+ return API.write(WRITE_COMMANDS.BANK_ACCOUNT_CREATE_CORPAY, parameters, onyxData);
}
function clearReimbursementAccount() {
Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT, null);
}
+function clearReimbursementAccountBankCreation() {
+ Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isCreateCorpayBankAccount: null, isSuccess: null, isLoading: null});
+}
+
/**
* Function to display and fetch data for Reimbursement Account step
* @param stepToOpen - current step to open
@@ -629,7 +567,7 @@ function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoD
const parameters: VerifyIdentityForBankAccountParams = {
bankAccountID,
onfidoData: JSON.stringify(onfidoData),
- policyID: policyID ?? '-1',
+ policyID,
};
API.write(WRITE_COMMANDS.VERIFY_IDENTITY_FOR_BANK_ACCOUNT, parameters, getVBBADataForOnyx());
@@ -712,6 +650,7 @@ export {
openPlaidView,
connectBankAccountManually,
connectBankAccountWithPlaid,
+ createCorpayBankAccount,
deletePaymentBankAccount,
handlePlaidError,
setPersonalBankAccountContinueKYCOnSuccess,
@@ -730,6 +669,7 @@ export {
clearPersonalBankAccountSetupType,
validatePlaidSelection,
getCorpayBankAccountFields,
+ clearReimbursementAccountBankCreation,
};
export type {BusinessAddress, PersonalAddress};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index f9e5a0a33679..c3b1b718e9a7 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -7768,6 +7768,7 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O
key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
value: {
...expenseReport,
+ lastVisibleActionCreated: optimisticReportAction?.created,
lastMessageText: ReportActionsUtils.getReportActionText(optimisticReportAction),
lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticReportAction),
stateNum,
@@ -7871,6 +7872,8 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O
},
{optimisticData, successData, failureData},
);
+ Navigation.dismissModal();
+ Report.notifyNewAction(expenseReport.reportID, userAccountID);
}
/**
@@ -7930,6 +7933,7 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R
playSound(SOUNDS.SUCCESS);
API.write(apiCommand, params, {optimisticData, successData, failureData});
+ Report.notifyNewAction(iouReport?.reportID ?? '', userAccountID);
}
function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxEntry, payAsBusiness = false) {
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 0bd5d6294899..cf2143c60629 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -1202,9 +1202,9 @@ function clearAvatarErrors(policyID: string) {
* Optimistically update the general settings. Set the general settings as pending until the response succeeds.
* If the response fails set a general error message. Clear the error message when updating.
*/
-function updateGeneralSettings(policyID: string, name: string, currencyValue?: string) {
+function updateGeneralSettings(name: string, currencyValue?: string, policyID?: string) {
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
- if (!policy) {
+ if (!policy || !policyID) {
return;
}
diff --git a/src/libs/actions/QueuedOnyxUpdates.ts b/src/libs/actions/QueuedOnyxUpdates.ts
index bc19ff12aea1..204e27e1266e 100644
--- a/src/libs/actions/QueuedOnyxUpdates.ts
+++ b/src/libs/actions/QueuedOnyxUpdates.ts
@@ -1,19 +1,44 @@
-import type {OnyxUpdate} from 'react-native-onyx';
+import type {OnyxKey, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
+import CONFIG from '@src/CONFIG';
+import ONYXKEYS from '@src/ONYXKEYS';
// In this file we manage a queue of Onyx updates while the SequentialQueue is processing. There are functions to get the updates and clear the queue after saving the updates in Onyx.
let queuedOnyxUpdates: OnyxUpdate[] = [];
+let currentAccountID: number | undefined;
+
+Onyx.connect({
+ key: ONYXKEYS.SESSION,
+ callback: (session) => {
+ currentAccountID = session?.accountID;
+ },
+});
/**
* @param updates Onyx updates to queue for later
*/
function queueOnyxUpdates(updates: OnyxUpdate[]): Promise {
queuedOnyxUpdates = queuedOnyxUpdates.concat(updates);
+
return Promise.resolve();
}
function flushQueue(): Promise {
+ if (!currentAccountID && !CONFIG.IS_TEST_ENV) {
+ const preservedKeys: OnyxKey[] = [
+ ONYXKEYS.NVP_TRY_FOCUS_MODE,
+ ONYXKEYS.PREFERRED_THEME,
+ ONYXKEYS.NVP_PREFERRED_LOCALE,
+ ONYXKEYS.SESSION,
+ ONYXKEYS.IS_LOADING_APP,
+ ONYXKEYS.CREDENTIALS,
+ ONYXKEYS.IS_SIDEBAR_LOADED,
+ ];
+
+ queuedOnyxUpdates = queuedOnyxUpdates.filter((update) => preservedKeys.includes(update.key as OnyxKey));
+ }
+
return Onyx.update(queuedOnyxUpdates).then(() => {
queuedOnyxUpdates = [];
});
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 462d291acf84..49c294946d09 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -834,7 +834,6 @@ function openReport(
hasLoadingOlderReportActionsError: false,
isLoadingNewerReportActions: false,
hasLoadingNewerReportActionsError: false,
- lastVisitTime: DateUtils.getDBTime(),
},
},
];
@@ -3673,6 +3672,20 @@ function prepareOnboardingOptimisticData(
};
});
+ // Sign-off welcome message
+ const welcomeSignOffComment = ReportUtils.buildOptimisticAddCommentReportAction(
+ Localize.translateLocal('onboarding.welcomeSignOffTitle'),
+ undefined,
+ actorAccountID,
+ tasksData.length + 3,
+ );
+ const welcomeSignOffCommentAction: OptimisticAddCommentReportAction = welcomeSignOffComment.reportAction;
+ const welcomeSignOffMessage = {
+ reportID: targetChatReportID,
+ reportActionID: welcomeSignOffCommentAction.reportActionID,
+ reportComment: welcomeSignOffComment.commentText,
+ };
+
const tasksForParameters = tasksData.map(({task, currentTask, taskCreatedAction, taskReportAction, taskDescription, completedTaskReportAction}) => ({
type: 'task',
task: task.type,
@@ -3828,8 +3841,7 @@ function prepareOnboardingOptimisticData(
}, []);
const optimisticData: OnyxUpdate[] = [...tasksForOptimisticData];
- const lastVisibleActionCreated =
- tasksData.at(-1)?.completedTaskReportAction?.created ?? tasksData.at(-1)?.taskReportAction.reportAction.created ?? videoCommentAction?.created ?? textCommentAction.created;
+ const lastVisibleActionCreated = welcomeSignOffCommentAction.created;
optimisticData.push(
{
@@ -4020,7 +4032,33 @@ function prepareOnboardingOptimisticData(
});
}
- guidedSetupData.push(...tasksForParameters);
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
+ value: {
+ [welcomeSignOffCommentAction.reportActionID]: welcomeSignOffCommentAction as ReportAction,
+ },
+ });
+
+ successData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
+ value: {
+ [welcomeSignOffCommentAction.reportActionID]: {pendingAction: null},
+ },
+ });
+
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
+ value: {
+ [welcomeSignOffCommentAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericAddCommentFailureMessage'),
+ } as ReportAction,
+ },
+ });
+
+ guidedSetupData.push(...tasksForParameters, {type: 'message', ...welcomeSignOffMessage});
return {optimisticData, successData, failureData, guidedSetupData, actorAccountID};
}
diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/BankInfo.tsx b/src/pages/ReimbursementAccount/NonUSD/BankInfo/BankInfo.tsx
index 7c5d853428c5..190f40181ab4 100644
--- a/src/pages/ReimbursementAccount/NonUSD/BankInfo/BankInfo.tsx
+++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/BankInfo.tsx
@@ -1,17 +1,21 @@
import type {ComponentType} from 'react';
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useEffect} from 'react';
import {useOnyx} from 'react-native-onyx';
import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
import useLocalize from '@hooks/useLocalize';
import useSubStep from '@hooks/useSubStep';
+import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import * as BankAccounts from '@userActions/BankAccounts';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
+import AccountHolderDetails from './substeps/AccountHolderDetails';
import BankAccountDetails from './substeps/BankAccountDetails';
import Confirmation from './substeps/Confirmation';
import UploadStatement from './substeps/UploadStatement';
-import type {BankInfoSubStepProps, CorpayFormField} from './types';
+import type {BankInfoSubStepProps} from './types';
+
+const {COUNTRY} = INPUT_IDS.ADDITIONAL_DATA;
type BankInfoProps = {
/** Handles back button press */
@@ -21,25 +25,28 @@ type BankInfoProps = {
onSubmit: () => void;
};
-const {COUNTRY} = INPUT_IDS.ADDITIONAL_DATA;
-
-const bodyContent: Array> = [BankAccountDetails, UploadStatement, Confirmation];
-
function BankInfo({onBackButtonPress, onSubmit}: BankInfoProps) {
const {translate} = useLocalize();
const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
- const [corpayFields, setCorpayFields] = useState([]);
- const country = reimbursementAccountDraft?.[COUNTRY] ?? '';
- const policyID = reimbursementAccount?.achData?.policyID ?? '-1';
+ const [corpayFields] = useOnyx(ONYXKEYS.CORPAY_FIELDS);
+ const policyID = reimbursementAccount?.achData?.policyID;
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
const currency = policy?.outputCurrency ?? '';
+ const country = reimbursementAccountDraft?.[COUNTRY] ?? reimbursementAccountDraft?.[COUNTRY] ?? '';
const submit = () => {
onSubmit();
};
+ useEffect(() => {
+ BankAccounts.getCorpayBankAccountFields(country, currency);
+ }, [country, currency]);
+
+ const bodyContent: Array> =
+ currency !== CONST.CURRENCY.AUD ? [BankAccountDetails, AccountHolderDetails, Confirmation] : [BankAccountDetails, AccountHolderDetails, UploadStatement, Confirmation];
+
const {
componentToRender: SubStep,
isEditing,
@@ -48,15 +55,8 @@ function BankInfo({onBackButtonPress, onSubmit}: BankInfoProps) {
prevScreen,
moveTo,
goToTheLastStep,
- resetScreenIndex,
} = useSubStep({bodyContent, startFrom: 0, onFinished: submit});
- // Temporary solution to get the fields for the corpay bank account fields
- useEffect(() => {
- const response = BankAccounts.getCorpayBankAccountFields(country, currency);
- setCorpayFields((response?.formFields as CorpayFormField[]) ?? []);
- }, [country, currency]);
-
const handleBackButtonPress = () => {
if (isEditing) {
goToTheLastStep();
@@ -65,26 +65,14 @@ function BankInfo({onBackButtonPress, onSubmit}: BankInfoProps) {
if (screenIndex === 0) {
onBackButtonPress();
- } else if (currency === CONST.CURRENCY.AUD) {
- prevScreen();
} else {
- resetScreenIndex();
+ prevScreen();
}
};
- const handleNextScreen = useCallback(() => {
- if (screenIndex === 2) {
- nextScreen();
- return;
- }
-
- if (currency !== CONST.CURRENCY.AUD) {
- goToTheLastStep();
- return;
- }
-
- nextScreen();
- }, [currency, goToTheLastStep, nextScreen, screenIndex]);
+ if (corpayFields?.isLoading !== undefined && !corpayFields?.isLoading && corpayFields?.isSuccess !== undefined && !corpayFields?.isSuccess) {
+ return ;
+ }
return (
diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/AccountHolderDetails.tsx b/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/AccountHolderDetails.tsx
new file mode 100644
index 000000000000..268356dee063
--- /dev/null
+++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/AccountHolderDetails.tsx
@@ -0,0 +1,134 @@
+import React, {useCallback, useMemo} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+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 TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {BankInfoSubStepProps} from '@pages/ReimbursementAccount/NonUSD/BankInfo/types';
+import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {ReimbursementAccountForm} from '@src/types/form/ReimbursementAccountForm';
+import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
+
+const {ACCOUNT_HOLDER_COUNTRY} = INPUT_IDS.ADDITIONAL_DATA.CORPAY;
+
+function AccountHolderDetails({onNext, isEditing, corpayFields}: BankInfoSubStepProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const accountHolderDetailsFields = useMemo(() => {
+ return corpayFields?.formFields?.filter((field) => field.id.includes(CONST.NON_USD_BANK_ACCOUNT.BANK_INFO_STEP_ACCOUNT_HOLDER_KEY_PREFIX));
+ }, [corpayFields]);
+ const fieldIds = accountHolderDetailsFields?.map((field) => field.id);
+
+ const subStepKeys = accountHolderDetailsFields?.reduce((acc, field) => {
+ acc[field.id as keyof ReimbursementAccountForm] = field.id as keyof ReimbursementAccountForm;
+ return acc;
+ }, {} as Record);
+
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
+ const defaultValues = useMemo(() => getSubstepValues(subStepKeys ?? {}, reimbursementAccountDraft, reimbursementAccount), [subStepKeys, reimbursementAccount, reimbursementAccountDraft]);
+
+ const handleSubmit = useReimbursementAccountStepFormSubmit({
+ fieldIds: fieldIds as Array>,
+ onNext,
+ shouldSaveDraft: isEditing,
+ });
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+
+ accountHolderDetailsFields?.forEach((field) => {
+ const fieldID = field.id as keyof FormOnyxValues;
+
+ if (field.isRequired && !values[fieldID]) {
+ errors[fieldID] = translate('common.error.fieldRequired');
+ }
+
+ field.validationRules.forEach((rule) => {
+ if (!rule.regEx) {
+ return;
+ }
+
+ if (new RegExp(rule.regEx).test(values[fieldID] ? String(values[fieldID]) : '')) {
+ return;
+ }
+
+ errors[fieldID] = rule.errorMessage;
+ });
+ });
+
+ return errors;
+ },
+ [accountHolderDetailsFields, translate],
+ );
+
+ const inputs = useMemo(() => {
+ return accountHolderDetailsFields?.map((field) => {
+ if (field.id === ACCOUNT_HOLDER_COUNTRY) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ });
+ }, [accountHolderDetailsFields, styles.mb6, styles.mhn5, defaultValues, isEditing, translate]);
+
+ return (
+
+
+ {translate('bankInfoStep.whatAreYour')}
+ {inputs}
+
+
+ );
+}
+
+AccountHolderDetails.displayName = 'AccountHolderDetails';
+
+export default AccountHolderDetails;
diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/BankAccountDetails.tsx b/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/BankAccountDetails.tsx
index b3482a516c1f..b7492790cfb2 100644
--- a/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/BankAccountDetails.tsx
+++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/BankAccountDetails.tsx
@@ -1,5 +1,5 @@
import React, {useCallback, useMemo} from 'react';
-import {View} from 'react-native';
+import {ActivityIndicator, 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';
@@ -7,6 +7,7 @@ import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import type {BankInfoSubStepProps} from '@pages/ReimbursementAccount/NonUSD/BankInfo/types';
import CONST from '@src/CONST';
@@ -15,14 +16,19 @@ import ONYXKEYS from '@src/ONYXKEYS';
function BankAccountDetails({onNext, isEditing, corpayFields}: BankInfoSubStepProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const theme = useTheme();
- const fieldIds = corpayFields.map((field) => field.id);
+ const bankAccountDetailsFields = useMemo(() => {
+ return corpayFields?.formFields?.filter((field) => !field.id.includes(CONST.NON_USD_BANK_ACCOUNT.BANK_INFO_STEP_ACCOUNT_HOLDER_KEY_PREFIX));
+ }, [corpayFields]);
+
+ const fieldIds = bankAccountDetailsFields?.map((field) => field.id);
const validate = useCallback(
(values: FormOnyxValues): FormInputErrors => {
const errors: FormInputErrors = {};
- corpayFields.forEach((field) => {
+ corpayFields?.formFields?.forEach((field) => {
const fieldID = field.id as keyof FormOnyxValues;
if (field.isRequired && !values[fieldID]) {
@@ -30,7 +36,7 @@ function BankAccountDetails({onNext, isEditing, corpayFields}: BankInfoSubStepPr
}
field.validationRules.forEach((rule) => {
- if (rule.regEx) {
+ if (!rule.regEx) {
return;
}
@@ -54,10 +60,10 @@ function BankAccountDetails({onNext, isEditing, corpayFields}: BankInfoSubStepPr
});
const inputs = useMemo(() => {
- return corpayFields.map((field) => {
+ return bankAccountDetailsFields?.map((field) => {
return (
);
});
- }, [corpayFields, styles.flex2, styles.mb6, isEditing]);
+ }, [bankAccountDetailsFields, styles.mb6, isEditing]);
return (
{translate('bankInfoStep.whatAreYour')}
{inputs}
+ {!inputs && (
+
+ )}
);
diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/Confirmation.tsx
index 8abe5e41aaaf..f10ee293a924 100644
--- a/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/Confirmation.tsx
+++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/Confirmation.tsx
@@ -1,19 +1,20 @@
-import React, {useMemo} from 'react';
+import React, {useEffect, useMemo} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
-import Button from '@components/Button';
+import FormProvider from '@components/Form/FormProvider';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
-import SafeAreaConsumer from '@components/SafeAreaConsumer';
-import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import type {BankInfoSubStepProps} from '@pages/ReimbursementAccount/NonUSD/BankInfo/types';
import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues';
+import * as BankAccounts from '@userActions/BankAccounts';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReimbursementAccountForm} from '@src/types/form/ReimbursementAccountForm';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
+const {ACCOUNT_HOLDER_COUNTRY} = INPUT_IDS.ADDITIONAL_DATA.CORPAY;
function Confirmation({onNext, onMove, corpayFields}: BankInfoSubStepProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
@@ -22,8 +23,8 @@ function Confirmation({onNext, onMove, corpayFields}: BankInfoSubStepProps) {
const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
const inputKeys = useMemo(() => {
const keys: Record = {};
- corpayFields.forEach((field) => {
- keys[field.id] = field.id;
+ corpayFields?.formFields?.forEach((field) => {
+ keys[field.id] = field.id as keyof ReimbursementAccountForm;
});
return keys;
}, [corpayFields]);
@@ -32,14 +33,24 @@ function Confirmation({onNext, onMove, corpayFields}: BankInfoSubStepProps) {
const items = useMemo(
() => (
<>
- {corpayFields.map((field) => {
+ {corpayFields?.formFields?.map((field) => {
+ let title = values[field.id as keyof typeof values] ? String(values[field.id as keyof typeof values]) : '';
+
+ if (field.id === ACCOUNT_HOLDER_COUNTRY) {
+ title = CONST.ALL_COUNTRIES[title as keyof typeof CONST.ALL_COUNTRIES];
+ }
+
return (
{
- onMove(0);
+ if (!field.id.includes(CONST.NON_USD_BANK_ACCOUNT.BANK_INFO_STEP_ACCOUNT_HOLDER_KEY_PREFIX)) {
+ onMove(0);
+ } else {
+ onMove(1);
+ }
}}
key={field.id}
/>
@@ -50,7 +61,7 @@ function Confirmation({onNext, onMove, corpayFields}: BankInfoSubStepProps) {
description={translate('bankInfoStep.bankStatement')}
title={reimbursementAccountDraft[INPUT_IDS.ADDITIONAL_DATA.CORPAY.BANK_STATEMENT].map((file) => file.name).join(', ')}
shouldShowRightIcon
- onPress={() => onMove(1)}
+ onPress={() => onMove(2)}
/>
)}
>
@@ -58,28 +69,40 @@ function Confirmation({onNext, onMove, corpayFields}: BankInfoSubStepProps) {
[corpayFields, onMove, reimbursementAccountDraft, translate, values],
);
+ const handleSubmit = () => {
+ const {formFields, isLoading, isSuccess, ...corpayData} = corpayFields ?? {};
+
+ BankAccounts.createCorpayBankAccount({...reimbursementAccountDraft, ...corpayData} as ReimbursementAccountForm);
+ };
+
+ useEffect(() => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ if (reimbursementAccount?.errors || reimbursementAccount?.isLoading || !reimbursementAccount?.isSuccess) {
+ return;
+ }
+
+ if (reimbursementAccount?.isSuccess) {
+ onNext();
+ BankAccounts.clearReimbursementAccountBankCreation();
+ }
+
+ return () => BankAccounts.clearReimbursementAccountBankCreation();
+ }, [onNext, reimbursementAccount?.errors, reimbursementAccount?.isCreateCorpayBankAccount, reimbursementAccount?.isLoading, reimbursementAccount?.isSuccess]);
+
return (
-
- {({safeAreaPaddingBottomStyle}) => (
-
- {translate('bankInfoStep.letsDoubleCheck')}
- {translate('bankInfoStep.thisBankAccount')}
- {items}
-
-
-
-
- )}
-
+
+
+ {translate('bankInfoStep.letsDoubleCheck')}
+ {translate('bankInfoStep.thisBankAccount')}
+ {items}
+
+
);
}
diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/types.ts b/src/pages/ReimbursementAccount/NonUSD/BankInfo/types.ts
index 17943b29e3d3..8c84f5680e19 100644
--- a/src/pages/ReimbursementAccount/NonUSD/BankInfo/types.ts
+++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/types.ts
@@ -1,17 +1,6 @@
import type {SubStepProps} from '@hooks/useSubStep/types';
-import type {ReimbursementAccountForm} from '@src/types/form';
+import type CorpayFormFields from '@src/types/onyx/CorpayFields';
-type CorpayFormField = {
- id: keyof ReimbursementAccountForm;
- isRequired: boolean;
- errorMessage: string;
- label: string;
- regEx?: string;
- validationRules: Array<{errorMessage: string; regEx: string}>;
- defaultValue?: string;
- detailedRule?: Array<{isRequired: boolean; value: Array<{errorMessage: string; regEx: string; ruleDescription: string}>}>;
-};
+type BankInfoSubStepProps = SubStepProps & {corpayFields?: CorpayFormFields; preferredMethod?: string};
-type BankInfoSubStepProps = SubStepProps & {corpayFields: CorpayFormField[]};
-
-export type {BankInfoSubStepProps, CorpayFormField};
+export type {BankInfoSubStepProps, CorpayFormFields};
diff --git a/src/pages/ReimbursementAccount/NonUSD/Country/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/Country/substeps/Confirmation.tsx
index 199c15989978..f2a7f20755a8 100644
--- a/src/pages/ReimbursementAccount/NonUSD/Country/substeps/Confirmation.tsx
+++ b/src/pages/ReimbursementAccount/NonUSD/Country/substeps/Confirmation.tsx
@@ -29,7 +29,7 @@ function Confirmation({onNext}: SubStepProps) {
const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
- const policyID = reimbursementAccount?.achData?.policyID ?? '-1';
+ const policyID = reimbursementAccount?.achData?.policyID;
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
const currency = policy?.outputCurrency ?? '';
@@ -42,29 +42,34 @@ function Confirmation({onNext}: SubStepProps) {
const disableSubmit = !(currency in CONST.CURRENCY);
const handleSettingsPress = () => {
+ if (!policyID) {
+ return;
+ }
+
Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID));
};
- const handleSelectingCountry = (country: unknown) => {
- setSelectedCountry(typeof country === 'string' ? country : '');
+ const handleSelectingCountry = (country: string) => {
+ FormActions.setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[COUNTRY]: country});
+ setSelectedCountry(country);
};
const validate = useCallback((values: FormOnyxValues): FormInputErrors => {
return ValidationUtils.getFieldRequiredErrors(values, [COUNTRY]);
}, []);
+ useEffect(() => {
+ FormActions.clearErrors(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM);
+ });
+
useEffect(() => {
if (currency === CONST.CURRENCY.EUR) {
- if (countryDefaultValue !== '') {
- FormActions.setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[COUNTRY]: countryDefaultValue});
- setSelectedCountry(countryDefaultValue);
- }
return;
}
FormActions.setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {[COUNTRY]: currencyMappedToCountry});
setSelectedCountry(currencyMappedToCountry);
- }, [countryDefaultValue, currency, currencyMappedToCountry]);
+ }, [currency, currencyMappedToCountry]);
return (
@@ -85,9 +90,8 @@ function Confirmation({onNext}: SubStepProps) {
style={[styles.label]}
onPress={handleSettingsPress}
>
- {translate('common.settings').toLowerCase()}
+ {translate('common.settings').toLowerCase()}.
- .