diff --git a/assets/images/arrow-collapse.svg b/assets/images/arrow-collapse.svg
new file mode 100644
index 000000000000..3a28e2bb4768
--- /dev/null
+++ b/assets/images/arrow-collapse.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/running-turtle.svg b/assets/images/running-turtle.svg
new file mode 100644
index 000000000000..0ef49b49e1db
--- /dev/null
+++ b/assets/images/running-turtle.svg
@@ -0,0 +1,96 @@
+
diff --git a/src/CONST.ts b/src/CONST.ts
index ba3f24be43d7..ae42767e21f8 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -21,6 +21,7 @@ const EMPTY_ARRAY = Object.freeze([]);
const EMPTY_OBJECT = Object.freeze({});
const DEFAULT_NUMBER_ID = 0;
+const DEFAULT_STRING_NEGATIVE_ID = '-1';
const CLOUDFRONT_DOMAIN = 'cloudfront.net';
const CLOUDFRONT_URL = `https://d2k5nsl2zxldvw.${CLOUDFRONT_DOMAIN}`;
@@ -975,6 +976,7 @@ const CONST = {
EMPTY_ARRAY,
EMPTY_OBJECT,
DEFAULT_NUMBER_ID,
+ DEFAULT_STRING_NEGATIVE_ID,
USE_EXPENSIFY_URL,
EXPENSIFY_URL,
GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com',
@@ -1038,6 +1040,7 @@ const CONST = {
DELAYED_SUBMISSION_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/reports/Automatically-submit-employee-reports',
ENCRYPTION_AND_SECURITY_HELP_URL: 'https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security',
PLAN_TYPES_AND_PRICING_HELP_URL: 'https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing',
+ MERGE_ACCOUNT_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/settings/Merge-accounts',
TEST_RECEIPT_URL: `${CLOUDFRONT_URL}/images/fake-receipt__tacotodds.png`,
// Use Environment.getEnvironmentURL to get the complete URL with port number
DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:',
@@ -4861,6 +4864,17 @@ const CONST = {
DISABLED: 'DISABLED',
DISABLE: 'DISABLE',
},
+ MERGE_ACCOUNT_RESULTS: {
+ SUCCESS: 'success',
+ ERR_2FA: 'err_2fa',
+ ERR_NO_EXIST: 'err_no_exist',
+ ERR_SMART_SCANNER: 'err_smart_scanner',
+ ERR_INVOICING: 'err_invoicing',
+ ERR_SAML_PRIMARY_LOGIN: 'err_saml_primary_login',
+ ERR_SAML_DOMAIN_CONTROL: 'err_saml_domain_control',
+ ERR_SAML_NOT_SUPPORTED: 'err_saml_not_supported',
+ ERR_ACCOUNT_LOCKED: 'err_account_locked',
+ },
DELEGATE_ROLE: {
ALL: 'all',
SUBMITTER: 'submitter',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 72cbcee20e34..b65f0ada760b 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -759,6 +759,8 @@ const ONYXKEYS = {
RULES_CUSTOM_FORM_DRAFT: 'rulesCustomFormDraft',
DEBUG_DETAILS_FORM: 'debugDetailsForm',
DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft',
+ MERGE_ACCOUNT_DETAILS_FORM: 'mergeAccountDetailsForm',
+ MERGE_ACCOUNT_DETAILS_FORM_DRAFT: 'mergeAccountDetailsFormDraft',
WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm',
WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft',
},
@@ -861,6 +863,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.RULES_CUSTOM_FORM]: FormTypes.RulesCustomForm;
[ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm;
[ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm;
+ [ONYXKEYS.FORMS.MERGE_ACCOUNT_DETAILS_FORM]: FormTypes.MergeAccountDetailsForm;
[ONYXKEYS.FORMS.INTERNATIONAL_BANK_ACCOUNT_FORM]: FormTypes.InternationalBankAccountForm;
[ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm;
};
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 41fd4b830f12..c91ce9b68615 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -161,6 +161,15 @@ const ROUTES = {
SETTINGS_WORKSPACES: {route: 'settings/workspaces', getRoute: (backTo?: string) => getUrlWithBackToParam('settings/workspaces', backTo)},
SETTINGS_SECURITY: 'settings/security',
SETTINGS_CLOSE: 'settings/security/closeAccount',
+ SETTINGS_MERGE_ACCOUNTS: 'settings/security/merge-accounts',
+ SETTINGS_MERGE_ACCOUNTS_MAGIC_CODE: {
+ route: 'settings/security/merge-accounts/:login/magic-code',
+ getRoute: (login: string) => `settings/security/merge-accounts/${encodeURIComponent(login)}/magic-code` as const,
+ },
+ SETTINGS_MERGE_ACCOUNTS_RESULT: {
+ route: 'settings/security/merge-accounts/:login/result/:result',
+ getRoute: (login: string, result: string) => `settings/security/merge-accounts/${encodeURIComponent(login)}/result/${result}` as const,
+ },
SETTINGS_ADD_DELEGATE: 'settings/security/delegate',
SETTINGS_DELEGATE_ROLE: {
route: 'settings/security/delegate/:login/role/:role',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 7e09c0277fe4..3dd002f7bf54 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -144,6 +144,11 @@ const SCREENS = {
DELEGATE_CONFIRM: 'Settings_Delegate_Confirm',
UPDATE_DELEGATE_ROLE: 'Settings_Delegate_Update_Role',
},
+ MERGE_ACCOUNTS: {
+ ACCOUNT_DETAILS: 'Settings_MergeAccounts_AccountDetails',
+ ACCOUNT_VALIDATE: 'Settings_MergeAccounts_AccountValidate',
+ MERGE_RESULT: 'Settings_MergeAccounts_MergeResult',
+ },
},
TWO_FACTOR_AUTH: {
ROOT: 'Settings_TwoFactorAuth_Root',
diff --git a/src/components/ConfirmationPage.tsx b/src/components/ConfirmationPage.tsx
index e4fc029ea31e..dc2ce751583e 100644
--- a/src/components/ConfirmationPage.tsx
+++ b/src/components/ConfirmationPage.tsx
@@ -22,15 +22,24 @@ type ConfirmationPageProps = {
/** Description of the confirmation page */
description: React.ReactNode;
- /** The text for the button label */
+ /** The text for the primary button label */
buttonText?: string;
- /** A function that is called when the button is clicked on */
+ /** A function that is called when the primary button is clicked on */
onButtonPress?: () => void;
- /** Whether we should show a confirmation button */
+ /** Whether we should show a primary confirmation button */
shouldShowButton?: boolean;
+ /** The text for the secondary button label */
+ secondaryButtonText?: string;
+
+ /** A function that is called when the secondary button is clicked on */
+ onSecondaryButtonPress?: () => void;
+
+ /** Whether we should show a secondary confirmation button */
+ shouldShowSecondaryButton?: boolean;
+
/** Additional style for the heading */
headingStyle?: TextStyle;
@@ -40,6 +49,9 @@ type ConfirmationPageProps = {
/** Additional style for the description */
descriptionStyle?: TextStyle;
+ /** Additional style for the footer */
+ footerStyle?: ViewStyle;
+
/** Additional style for the container */
containerStyle?: ViewStyle;
};
@@ -51,9 +63,13 @@ function ConfirmationPage({
buttonText = '',
onButtonPress = () => {},
shouldShowButton = false,
+ secondaryButtonText = '',
+ onSecondaryButtonPress = () => {},
+ shouldShowSecondaryButton = false,
headingStyle,
illustrationStyle,
descriptionStyle,
+ footerStyle,
containerStyle,
}: ConfirmationPageProps) {
const styles = useThemeStyles();
@@ -68,6 +84,10 @@ function ConfirmationPage({
autoPlay
loop
style={[styles.confirmationAnimation, illustrationStyle]}
+ webStyle={{
+ width: (illustrationStyle?.width as number) ?? styles.confirmationAnimation.width,
+ height: (illustrationStyle?.height as number) ?? styles.confirmationAnimation.height,
+ }}
/>
) : (
@@ -80,17 +100,28 @@ function ConfirmationPage({
{heading}
{description}
- {shouldShowButton && (
-
-
+ {(shouldShowSecondaryButton || shouldShowButton) && (
+
+ {shouldShowSecondaryButton && (
+
+ )}
+ {shouldShowButton && (
+
+ )}
)}
@@ -100,3 +131,5 @@ function ConfirmationPage({
ConfirmationPage.displayName = 'ConfirmationPage';
export default ConfirmationPage;
+
+export type {ConfirmationPageProps};
diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx
index bf3746b61776..a1abe6553609 100644
--- a/src/components/Form/FormProvider.tsx
+++ b/src/components/Form/FormProvider.tsx
@@ -273,6 +273,7 @@ function FormProvider(
resetForm,
resetErrors,
resetFormFieldError,
+ submit,
}));
const registerInput = useCallback(
diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts
index 7d56ce8988da..97e30eb9753b 100644
--- a/src/components/Form/types.ts
+++ b/src/components/Form/types.ts
@@ -175,6 +175,7 @@ type FormRef = {
resetForm: (optionalValue: FormOnyxValues) => void;
resetErrors: () => void;
resetFormFieldError: (fieldID: keyof Form) => void;
+ submit: () => void;
};
type InputRefs = Record>;
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 712b03bf4590..c66830c8b88e 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -2,6 +2,7 @@ import AddReaction from '@assets/images/add-reaction.svg';
import All from '@assets/images/all.svg';
import Android from '@assets/images/android.svg';
import Apple from '@assets/images/apple.svg';
+import ArrowCollapse from '@assets/images/arrow-collapse.svg';
import ArrowDownLong from '@assets/images/arrow-down-long.svg';
import ArrowRightLong from '@assets/images/arrow-right-long.svg';
import ArrowRight from '@assets/images/arrow-right.svg';
@@ -223,6 +224,7 @@ export {
AnnounceRoomAvatar,
Apple,
AppleLogo,
+ ArrowCollapse,
ArrowRight,
ArrowRightLong,
ArrowsUpDown,
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 737d588127a9..6b089e78142e 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -64,6 +64,7 @@ import TeleScope from '@assets/images/product-illustrations/telescope.svg';
import ThreeLeggedLaptopWoman from '@assets/images/product-illustrations/three_legged_laptop_woman.svg';
import ToddBehindCloud from '@assets/images/product-illustrations/todd-behind-cloud.svg';
import ToddWithPhones from '@assets/images/product-illustrations/todd-with-phones.svg';
+import RunningTurtle from '@assets/images/running-turtle.svg';
import ReportReceipt from '@assets/images/simple-illustration__report-receipt.svg';
import BigVault from '@assets/images/simple-illustrations/emptystate__big-vault.svg';
import Puzzle from '@assets/images/simple-illustrations/emptystate__puzzlepieces.svg';
@@ -276,6 +277,7 @@ export {
Filters,
MagnifyingGlassMoney,
Rules,
+ RunningTurtle,
CompanyCardsEmptyState,
AmexCompanyCards,
MasterCardCompanyCards,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 5b007b482ba5..cce9e3290139 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1364,6 +1364,81 @@ const translations = {
defaultContact: 'Default contact method:',
enterYourDefaultContactMethod: 'Please enter your default contact method to close your account.',
},
+ mergeAccountsPage: {
+ mergeAccount: 'Merge accounts',
+ accountDetails: {
+ accountToMergeInto: 'Enter the account you want to merge into ',
+ notReversibleConsent: 'I understand this is not reversible',
+ },
+ accountValidate: {
+ confirmMerge: 'Are you sure you want to merge accounts?',
+ lossOfUnsubmittedData: `Merging your accounts is irreversible and will result in the loss of any unsubmitted expenses for `,
+ enterMagicCode: `To continue, please enter the magic code sent to `,
+ },
+ mergeSuccess: {
+ accountsMerged: 'Accounts merged!',
+ successfullyMergedAllData: {
+ beforeFirstEmail: `You've successfully merged all data from `,
+ beforeSecondEmail: ` into `,
+ afterSecondEmail: `. Moving forward, you can use either login for this account.`,
+ },
+ },
+ mergePendingSAML: {
+ weAreWorkingOnIt: 'We’re working on it',
+ limitedSupport: 'We don’t yet support merging accounts on New Expensify. Please take this action on Expensify Classic instead.',
+ reachOutForHelp: {
+ beforeLink: 'Feel free to ',
+ linkText: 'reach out to Concierge',
+ afterLink: ' if you have any questions!',
+ },
+ goToExpensifyClassic: 'Go to Expensify Classic',
+ },
+ mergeFailureSAMLDomainControl: {
+ beforeFirstEmail: 'You can’t merge ',
+ beforeDomain: ' because it’s controlled by ',
+ afterDomain: '. Please ',
+ linkText: 'reach out to Concierge',
+ afterLink: ' for assistance.',
+ },
+ mergeFailureSAMLAccount: {
+ beforeEmail: 'You can’t merge ',
+ afterEmail: ' into other accounts because your domain admin has set it as your primary login. Please merge other accounts into it instead.',
+ },
+ mergeFailure2FA: {
+ oldAccount2FAEnabled: {
+ beforeFirstEmail: 'You can’t merge accounts because ',
+ beforeSecondEmail: ' has two-factor authentication (2FA) enabled. Please disable 2FA for ',
+ afterSecondEmail: ' and try again.',
+ },
+ learnMore: 'Learn more about merging accounts.',
+ },
+ mergeFailureAccountLocked: {
+ beforeEmail: 'You can’t merge ',
+ afterEmail: ' because it’s locked. Please ',
+ linkText: 'reach out to Concierge ',
+ afterLink: `for assistance.`,
+ },
+ mergeFailureUncreatedAccount: {
+ noExpensifyAccount: {
+ beforeEmail: 'You can’t merge accounts because ',
+ afterEmail: ' doesn’t have an Expensify account.',
+ },
+ addContactMethod: {
+ beforeLink: 'Please ',
+ linkText: 'add it as a contact method',
+ afterLink: ' instead.',
+ },
+ },
+ mergeFailureSmartScannerAccount: {
+ beforeEmail: 'You can’t merge ',
+ afterEmail: ' into other accounts. Please merge other accounts into it instead.',
+ },
+ mergeFailureInvoicedAccount: {
+ beforeEmail: 'You can’t merge ',
+ afterEmail: ' into other accounts because it’s the billing owner of an invoiced account. Please merge other accounts into it instead.',
+ },
+ mergeFailureGenericHeading: 'Can’t merge accounts',
+ },
passwordPage: {
changePassword: 'Change password',
changingYourPasswordPrompt: 'Changing your password will update your password for both your Expensify.com and New Expensify accounts.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 6052ce92814a..9e4669bc75d5 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1362,6 +1362,81 @@ const translations = {
defaultContact: 'Método de contacto predeterminado:',
enterYourDefaultContactMethod: 'Por favor, introduce tu método de contacto predeterminado para cerrar tu cuenta.',
},
+ mergeAccountsPage: {
+ mergeAccount: 'Fusionar cuentas',
+ accountDetails: {
+ accountToMergeInto: `Introduce la cuenta en la que deseas fusionar `,
+ notReversibleConsent: 'Entiendo que esto no es reversible',
+ },
+ accountValidate: {
+ confirmMerge: '¿Estás seguro de que deseas fusionar cuentas?',
+ lossOfUnsubmittedData: `Fusionar tus cuentas es irreversible y resultará en la pérdida de cualquier gasto no enviado de `,
+ enterMagicCode: `Para continuar, por favor introduce el código mágico enviado a `,
+ },
+ mergeSuccess: {
+ accountsMerged: '¡Cuentas fusionadas!',
+ successfullyMergedAllData: {
+ beforeFirstEmail: 'Has fusionado exitosamente todos los datos de ',
+ beforeSecondEmail: ' en ',
+ afterSecondEmail: '. De ahora en adelante, puedes usar cualquiera de los inicios de sesión para esta cuenta.',
+ },
+ },
+ mergePendingSAML: {
+ weAreWorkingOnIt: 'Estamos trabajando en ello',
+ limitedSupport: 'Todavía no es posible fusionar cuentas en New Expensify. Por favor, realiza esta acción en Expensify Classic en su lugar',
+ reachOutForHelp: {
+ beforeLink: '¡No dudes en ',
+ linkText: 'comunicarte con Concierge',
+ afterLink: ' si tienes alguna pregunta!',
+ },
+ goToExpensifyClassic: 'Dirígete a Expensify Classic',
+ },
+ mergeFailureSAMLDomainControl: {
+ beforeFirstEmail: 'No puedes fusionar ',
+ beforeDomain: ' porque está controlado por ',
+ afterDomain: '. Póngase ',
+ linkText: 'en contacto con Conserjería',
+ afterLink: ' si necesita ayuda.',
+ },
+ mergeFailureSAMLAccount: {
+ beforeEmail: 'No puedes fusionar ',
+ afterEmail: ' en otras cuentas porque tu administrador de dominio la ha establecido como tu inicio de sesión principal. Por favor, fusiona otras cuentas en esta en su lugar.',
+ },
+ mergeFailure2FA: {
+ oldAccount2FAEnabled: {
+ beforeFirstEmail: 'No puedes fusionar cuentas porque ',
+ beforeSecondEmail: ' tiene habilitada la autenticación de dos factores (2FA). Por favor, deshabilita 2FA para ',
+ afterSecondEmail: ' e inténtalo nuevamente.',
+ },
+ learnMore: 'Aprende más sobre cómo fusionar cuentas.',
+ },
+ mergeFailureAccountLocked: {
+ beforeEmail: 'No puedes fusionar ',
+ afterEmail: ' porque está bloqueado. Póngase ',
+ linkText: 'en contacto con Conserjería',
+ afterLink: ` si necesita ayuda.`,
+ },
+ mergeFailureUncreatedAccount: {
+ noExpensifyAccount: {
+ beforeEmail: 'No puedes fusionar cuentas porque ',
+ afterEmail: ' no tiene una cuenta de Expensify.',
+ },
+ addContactMethod: {
+ beforeLink: 'Por favor, ',
+ linkText: 'añádela como método de contacto',
+ afterLink: ' en su lugar.',
+ },
+ },
+ mergeFailureSmartScannerAccount: {
+ beforeEmail: 'No puedes fusionar ',
+ afterEmail: ' en otras cuentas. Por favor, fusiona otras cuentas en esta en su lugar.',
+ },
+ mergeFailureInvoicedAccount: {
+ beforeEmail: 'No puedes fusionar ',
+ afterEmail: ' en otras cuentas porque es el propietario de facturación de una cuenta facturada. Por favor, fusiona otras cuentas en esta en su lugar.',
+ },
+ mergeFailureGenericHeading: 'No se pueden fusionar cuentas',
+ },
passwordPage: {
changePassword: 'Cambiar contraseña',
changingYourPasswordPrompt: 'El cambio de contraseña va a afectar tanto a la cuenta de Expensify.com como la de New Expensify.',
diff --git a/src/libs/API/parameters/GetValidateCodeForAccountMerge.ts b/src/libs/API/parameters/GetValidateCodeForAccountMerge.ts
new file mode 100644
index 000000000000..dc29ea6805aa
--- /dev/null
+++ b/src/libs/API/parameters/GetValidateCodeForAccountMerge.ts
@@ -0,0 +1,3 @@
+type GetValidateCodeForAccountMergeParams = {email: string; authToken: string};
+
+export default GetValidateCodeForAccountMergeParams;
diff --git a/src/libs/API/parameters/MergeWithValidateCode.ts b/src/libs/API/parameters/MergeWithValidateCode.ts
new file mode 100644
index 000000000000..5eeb7ea3825a
--- /dev/null
+++ b/src/libs/API/parameters/MergeWithValidateCode.ts
@@ -0,0 +1,3 @@
+type MergeWithValidateCode = {email: string; validateCode: string};
+
+export default MergeWithValidateCode;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 38ee3ee71053..297ca3faa6ec 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -370,6 +370,8 @@ export type {default as ImportPerDiemRatesParams} from './ImportPerDiemRatesPara
export type {default as ExportPerDiemCSVParams} from './ExportPerDiemCSVParams';
export type {default as UpdateWorkspaceCustomUnitParams} from './UpdateWorkspaceCustomUnitParams';
export type {default as DismissProductTrainingParams} from './DismissProductTraining';
+export type {default as GetValidateCodeForAccountMergeParams} from './GetValidateCodeForAccountMerge';
+export type {default as MergeWithValidateCodeParams} from './MergeWithValidateCode';
export type {default as OpenWorkspacePlanPageParams} from './OpenWorkspacePlanPage';
export type {default as ResetSMSDeliveryFailureStatusParams} from './ResetSMSDeliveryFailureStatusParams';
export type {default as CreatePerDiemRequestParams} from './CreatePerDiemRequestParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 79f26f7603a4..6b0d9cd05795 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -457,6 +457,8 @@ const WRITE_COMMANDS = {
UPDATE_WORKSPACE_CUSTOM_UNIT: 'UpdateWorkspaceCustomUnit',
VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'ValidateUserAndGetAccessiblePolicies',
DISMISS_PRODUCT_TRAINING: 'DismissProductTraining',
+ GET_VALIDATE_CODE_FOR_ACCOUNT_MERGE: 'GetValidateCodeForAccountMerge',
+ MERGE_WITH_VALIDATE_CODE: 'MergeWithValidateCode',
RESET_SMS_DELIVERY_FAILURE_STATUS: 'ResetSMSDeliveryFailureStatus',
SAVE_CORPAY_ONBOARDING_COMPANY_DETAILS: 'SaveCorpayOnboardingCompanyDetails',
SAVE_CORPAY_ONBOARDING_BENEFICIAL_OWNER: 'SaveCorpayOnboardingBeneficialOwner',
@@ -931,6 +933,10 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.JOIN_ACCESSIBLE_POLICY]: Parameters.JoinAccessiblePolicyParams;
// Dismis Product Training
[WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING]: Parameters.DismissProductTrainingParams;
+
+ // Merge accounts API
+ [WRITE_COMMANDS.GET_VALIDATE_CODE_FOR_ACCOUNT_MERGE]: Parameters.GetValidateCodeForAccountMergeParams;
+ [WRITE_COMMANDS.MERGE_WITH_VALIDATE_CODE]: Parameters.MergeWithValidateCodeParams;
};
const READ_COMMANDS = {
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 91939c86f07f..bf49f812d46f 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -592,6 +592,9 @@ const SettingsModalStackNavigator = createModalStackNavigator
require('../../../../pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage').default,
[SCREENS.SETTINGS.DELEGATE.DELEGATE_CONFIRM]: () => require('../../../../pages/settings/Security/AddDelegate/ConfirmDelegatePage').default,
+ [SCREENS.SETTINGS.MERGE_ACCOUNTS.ACCOUNT_DETAILS]: () => require('../../../../pages/settings/Security/MergeAccounts/AccountDetailsPage').default,
+ [SCREENS.SETTINGS.MERGE_ACCOUNTS.ACCOUNT_VALIDATE]: () => require('../../../../pages/settings/Security/MergeAccounts/AccountValidatePage').default,
+ [SCREENS.SETTINGS.MERGE_ACCOUNTS.MERGE_RESULT]: () => require('../../../../pages/settings/Security/MergeAccounts/MergeResultPage').default,
[SCREENS.WORKSPACE.RULES_CUSTOM_NAME]: () => require('../../../../pages/workspace/rules/RulesCustomNamePage').default,
[SCREENS.WORKSPACE.RULES_AUTO_APPROVE_REPORTS_UNDER]: () => require('../../../../pages/workspace/rules/RulesAutoApproveReportsUnderPage').default,
[SCREENS.WORKSPACE.RULES_RANDOM_REPORT_AUDIT]: () => require('../../../../pages/workspace/rules/RulesRandomReportAuditPage').default,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 848c4ed9cdcd..d05dd4cc4d96 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -140,6 +140,22 @@ const config: LinkingOptions['config'] = {
path: ROUTES.SETTINGS_CLOSE,
exact: true,
},
+ [SCREENS.SETTINGS.MERGE_ACCOUNTS.ACCOUNT_DETAILS]: {
+ path: ROUTES.SETTINGS_MERGE_ACCOUNTS,
+ exact: true,
+ },
+ [SCREENS.SETTINGS.MERGE_ACCOUNTS.ACCOUNT_VALIDATE]: {
+ path: ROUTES.SETTINGS_MERGE_ACCOUNTS_MAGIC_CODE.route,
+ parse: {
+ login: (login: string) => decodeURIComponent(login),
+ },
+ },
+ [SCREENS.SETTINGS.MERGE_ACCOUNTS.MERGE_RESULT]: {
+ path: ROUTES.SETTINGS_MERGE_ACCOUNTS_RESULT.route,
+ parse: {
+ login: (login: string) => decodeURIComponent(login),
+ },
+ },
[SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT]: {
path: ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.route,
exact: true,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 45d9119f0a7e..128786ab5c3a 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -99,7 +99,20 @@ type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.PREFERENCES.LANGUAGE]: undefined;
[SCREENS.SETTINGS.PREFERENCES.THEME]: undefined;
[SCREENS.SETTINGS.CLOSE]: undefined;
- [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS]: undefined;
+ [SCREENS.SETTINGS.MERGE_ACCOUNTS.ACCOUNT_DETAILS]: {
+ backTo?: Routes;
+ forwardTo?: Routes;
+ };
+ [SCREENS.SETTINGS.MERGE_ACCOUNTS.ACCOUNT_VALIDATE]: {
+ login: string;
+ backTo?: Routes;
+ forwardTo?: Routes;
+ };
+ [SCREENS.SETTINGS.MERGE_ACCOUNTS.MERGE_RESULT]: {
+ backTo?: Routes;
+ result: ValueOf;
+ login: string;
+ };
[SCREENS.SETTINGS.CONSOLE]: {
backTo: Routes;
};
diff --git a/src/libs/actions/MergeAccounts.ts b/src/libs/actions/MergeAccounts.ts
new file mode 100644
index 000000000000..2fe2c7f7ea08
--- /dev/null
+++ b/src/libs/actions/MergeAccounts.ts
@@ -0,0 +1,135 @@
+import Onyx from 'react-native-onyx';
+import type {OnyxUpdate} from 'react-native-onyx';
+import * as API from '@libs/API';
+import type {GetValidateCodeForAccountMergeParams, MergeWithValidateCodeParams} from '@libs/API/parameters';
+import {WRITE_COMMANDS} from '@libs/API/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type Session from '@src/types/onyx/Session';
+
+let session: Session = {};
+Onyx.connect({
+ key: ONYXKEYS.SESSION,
+ callback: (value) => (session = value ?? {}),
+});
+
+function requestValidationCodeForAccountMerge(email: string) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.ACCOUNT,
+ value: {
+ getValidateCodeForAccountMerge: {
+ isLoading: true,
+ validateCodeSent: false,
+ errors: null,
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.ACCOUNT,
+ value: {
+ getValidateCodeForAccountMerge: {
+ isLoading: false,
+ validateCodeSent: true,
+ errors: null,
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.ACCOUNT,
+ value: {
+ getValidateCodeForAccountMerge: {
+ isLoading: false,
+ validateCodeSent: false,
+ },
+ },
+ },
+ ];
+
+ const parameters: GetValidateCodeForAccountMergeParams = {
+ authToken: session.authToken ?? '',
+ email,
+ };
+
+ API.write(WRITE_COMMANDS.GET_VALIDATE_CODE_FOR_ACCOUNT_MERGE, parameters, {optimisticData, successData, failureData});
+}
+
+function clearRequestValidationCodeForAccountMerge() {
+ Onyx.merge(ONYXKEYS.ACCOUNT, {
+ getValidateCodeForAccountMerge: {
+ errors: null,
+ validateCodeSent: false,
+ isLoading: false,
+ },
+ });
+}
+
+function mergeWithValidateCode(email: string, validateCode: string) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.ACCOUNT,
+ value: {
+ mergeWithValidateCode: {
+ isLoading: true,
+ accountMerged: false,
+ errors: null,
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.ACCOUNT,
+ value: {
+ mergeWithValidateCode: {
+ isLoading: false,
+ accountMerged: true,
+ errors: null,
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.ACCOUNT,
+ value: {
+ mergeWithValidateCode: {
+ isLoading: false,
+ accountMerged: false,
+ },
+ },
+ },
+ ];
+
+ const parameters: MergeWithValidateCodeParams = {
+ email,
+ validateCode,
+ };
+
+ API.write(WRITE_COMMANDS.MERGE_WITH_VALIDATE_CODE, parameters, {optimisticData, successData, failureData});
+}
+
+function clearMergeWithValidateCode() {
+ Onyx.merge(ONYXKEYS.ACCOUNT, {
+ mergeWithValidateCode: {
+ errors: null,
+ isLoading: false,
+ accountMerged: false,
+ },
+ });
+}
+
+export {requestValidationCodeForAccountMerge, clearRequestValidationCodeForAccountMerge, mergeWithValidateCode, clearMergeWithValidateCode};
diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts
index 6c3d17b8236b..cf72677be512 100644
--- a/src/libs/actions/PaymentMethods.ts
+++ b/src/libs/actions/PaymentMethods.ts
@@ -398,7 +398,7 @@ function resetWalletTransferData() {
});
}
-function saveWalletTransferAccountTypeAndID(selectedAccountType: string, selectedAccountID: string) {
+function saveWalletTransferAccountTypeAndID(selectedAccountType: string | undefined, selectedAccountID: string) {
Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {selectedAccountType, selectedAccountID});
}
diff --git a/src/pages/EnablePayments/ActivateStep.tsx b/src/pages/EnablePayments/ActivateStep.tsx
index 41a9bce2a0cc..00fd77c3afed 100644
--- a/src/pages/EnablePayments/ActivateStep.tsx
+++ b/src/pages/EnablePayments/ActivateStep.tsx
@@ -1,27 +1,23 @@
import React from 'react';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import ConfirmationPage from '@components/ConfirmationPage';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import LottieAnimations from '@components/LottieAnimations';
import useLocalize from '@hooks/useLocalize';
-import * as PaymentMethods from '@userActions/PaymentMethods';
+import {continueSetup} from '@userActions/PaymentMethods';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {UserWallet, WalletTerms} from '@src/types/onyx';
+import type {UserWallet} from '@src/types/onyx';
-type ActivateStepOnyxProps = {
- /** Information about the user accepting the terms for payments */
- walletTerms: OnyxEntry;
-};
-
-type ActivateStepProps = ActivateStepOnyxProps & {
+type ActivateStepProps = {
/** The user's wallet */
userWallet: OnyxEntry;
};
-function ActivateStep({userWallet, walletTerms}: ActivateStepProps) {
+function ActivateStep({userWallet}: ActivateStepProps) {
const {translate} = useLocalize();
+ const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS);
const isActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName);
const animation = isActivatedWallet ? LottieAnimations.Fireworks : LottieAnimations.ReviewingBankInfo;
@@ -44,7 +40,7 @@ function ActivateStep({userWallet, walletTerms}: ActivateStepProps) {
description={translate(`activateStep.${isActivatedWallet ? 'activated' : 'checkBackLater'}Message`)}
shouldShowButton={isActivatedWallet}
buttonText={continueButtonText}
- onButtonPress={() => PaymentMethods.continueSetup()}
+ onButtonPress={() => continueSetup()}
/>
>
);
@@ -52,8 +48,4 @@ function ActivateStep({userWallet, walletTerms}: ActivateStepProps) {
ActivateStep.displayName = 'ActivateStep';
-export default withOnyx({
- walletTerms: {
- key: ONYXKEYS.WALLET_TERMS,
- },
-})(ActivateStep);
+export default ActivateStep;
diff --git a/src/pages/home/report/ReportDetailsExportPage.tsx b/src/pages/home/report/ReportDetailsExportPage.tsx
index 4332ba764ff9..abbc1446c9db 100644
--- a/src/pages/home/report/ReportDetailsExportPage.tsx
+++ b/src/pages/home/report/ReportDetailsExportPage.tsx
@@ -10,11 +10,11 @@ import UserListItem from '@components/SelectionList/UserListItem';
import type {SelectorType} from '@components/SelectionScreen';
import SelectionScreen from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
-import * as ReportActions from '@libs/actions/Report';
+import {exportToIntegration, markAsManuallyExported} from '@libs/actions/Report';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types';
-import * as ReportUtils from '@libs/ReportUtils';
+import {canBeExported, getIntegrationIcon, isExported} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -37,16 +37,16 @@ function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) {
const {translate} = useLocalize();
const [modalStatus, setModalStatus] = useState(null);
- const iconToDisplay = ReportUtils.getIntegrationIcon(connectionName);
- const canBeExported = ReportUtils.canBeExported(report);
- const isExported = ReportUtils.isExported(reportActions);
+ const iconToDisplay = getIntegrationIcon(connectionName);
+ const exportable = canBeExported(report);
+ const exported = isExported(reportActions);
const confirmExport = useCallback(
(type = modalStatus) => {
if (type === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) {
- ReportActions.exportToIntegration(reportID, connectionName);
+ exportToIntegration(reportID, connectionName);
} else if (type === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) {
- ReportActions.markAsManuallyExported(reportID, connectionName);
+ markAsManuallyExported(reportID, connectionName);
}
setModalStatus(null);
Navigation.dismissModal();
@@ -64,7 +64,7 @@ function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) {
type: CONST.ICON_TYPE_AVATAR,
},
],
- isDisabled: !canBeExported,
+ isDisabled: !exportable,
},
{
value: CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED,
@@ -75,11 +75,11 @@ function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) {
type: CONST.ICON_TYPE_AVATAR,
},
],
- isDisabled: !canBeExported,
+ isDisabled: !exportable,
},
];
- if (!canBeExported) {
+ if (!exportable) {
return (
- policyID={policyID ?? ''}
+ policyID={policyID ?? CONST.DEFAULT_NUMBER_ID.toString()}
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
displayName={ReportDetailsExportPage.displayName}
@@ -113,7 +113,7 @@ function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) {
title="common.export"
connectionName={connectionName}
onSelectRow={({value}) => {
- if (isExported) {
+ if (exported) {
setModalStatus(value);
} else {
confirmExport(value);
diff --git a/src/pages/settings/Security/MergeAccounts/AccountDetailsPage.tsx b/src/pages/settings/Security/MergeAccounts/AccountDetailsPage.tsx
new file mode 100644
index 000000000000..49da1a9ad2f6
--- /dev/null
+++ b/src/pages/settings/Security/MergeAccounts/AccountDetailsPage.tsx
@@ -0,0 +1,168 @@
+import {Str} from 'expensify-common';
+import React, {useEffect, useRef} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import CheckboxWithLabel from '@components/CheckboxWithLabel';
+import FixedFooter from '@components/FixedFooter';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormOnyxValues, FormRef} from '@components/Form/types';
+import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {addErrorMessage, getLatestErrorMessage} from '@libs/ErrorUtils';
+import {appendCountryCode, getPhoneNumberWithoutSpecialChars} from '@libs/LoginUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import {parsePhoneNumber} from '@libs/PhoneNumber';
+import {isNumericWithSpecialChars} from '@libs/ValidationUtils';
+import {requestValidationCodeForAccountMerge} from '@userActions/MergeAccounts';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/MergeAccountDetailsForm';
+import type {Errors} from '@src/types/onyx/OnyxCommon';
+
+const getErrorKey = (err: string): ValueOf | null => {
+ if (err.includes('404')) {
+ return CONST.MERGE_ACCOUNT_RESULTS.ERR_NO_EXIST;
+ }
+
+ if (err.includes('401')) {
+ return CONST.MERGE_ACCOUNT_RESULTS.ERR_SAML_PRIMARY_LOGIN;
+ }
+
+ if (err.includes('402')) {
+ return CONST.MERGE_ACCOUNT_RESULTS.ERR_SAML_NOT_SUPPORTED;
+ }
+
+ return null;
+};
+
+function AccountDetailsPage() {
+ const formRef = useRef(null);
+ const [userEmailOrPhone] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
+ const [getValidateCodeForAccountMerge] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.getValidateCodeForAccountMerge});
+ const [form] = useOnyx(ONYXKEYS.FORMS.MERGE_ACCOUNT_DETAILS_FORM_DRAFT);
+ const email = form?.[INPUT_IDS.PHONE_OR_EMAIL] ?? '';
+
+ const validateCodeSent = getValidateCodeForAccountMerge?.validateCodeSent;
+ const latestError = getLatestErrorMessage(getValidateCodeForAccountMerge);
+ const errorKey = getErrorKey(latestError);
+ const genericError = !errorKey ? latestError : undefined;
+
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ useEffect(() => {
+ if (!validateCodeSent || !email) {
+ return;
+ }
+
+ return Navigation.navigate(ROUTES.SETTINGS_MERGE_ACCOUNTS_MAGIC_CODE.getRoute(email));
+ }, [validateCodeSent, email]);
+
+ useEffect(() => {
+ if (!errorKey || !email) {
+ return;
+ }
+ return Navigation.navigate(ROUTES.SETTINGS_MERGE_ACCOUNTS_RESULT.getRoute(email, errorKey));
+ }, [errorKey, email]);
+
+ const validate = (values: FormOnyxValues): Errors => {
+ const errors = {};
+
+ const login = values[INPUT_IDS.PHONE_OR_EMAIL];
+
+ if (!login) {
+ addErrorMessage(errors, INPUT_IDS.PHONE_OR_EMAIL, translate('common.pleaseEnterEmailOrPhoneNumber'));
+ } else {
+ const phoneLogin = appendCountryCode(getPhoneNumberWithoutSpecialChars(login));
+ const parsedPhoneNumber = parsePhoneNumber(phoneLogin);
+
+ if (!Str.isValidEmail(login) && !parsedPhoneNumber.possible) {
+ if (isNumericWithSpecialChars(login)) {
+ addErrorMessage(errors, INPUT_IDS.PHONE_OR_EMAIL, translate('common.error.phoneNumber'));
+ } else {
+ addErrorMessage(errors, INPUT_IDS.PHONE_OR_EMAIL, translate('loginForm.error.invalidFormatEmailLogin'));
+ }
+ }
+ }
+
+ if (!values[INPUT_IDS.CONSENT]) {
+ addErrorMessage(errors, INPUT_IDS.CONSENT, translate('common.error.fieldRequired'));
+ }
+ return errors;
+ };
+
+ return (
+
+ Navigation.goBack()}
+ shouldDisplayHelpButton={false}
+ />
+ {
+ requestValidationCodeForAccountMerge(values[INPUT_IDS.PHONE_OR_EMAIL]);
+ }}
+ style={[styles.flexGrow1, styles.mh5]}
+ shouldTrimValues
+ validate={validate}
+ submitButtonText={translate('common.next')}
+ isSubmitButtonVisible={false}
+ ref={formRef}
+ >
+
+
+ {translate('mergeAccountsPage.accountDetails.accountToMergeInto')}
+ {userEmailOrPhone}
+
+
+
+
+
+ {
+ formRef.current?.submit();
+ }}
+ message={genericError}
+ buttonText={translate('common.next')}
+ enabledWhenOffline={false}
+ containerStyles={styles.mt3}
+ isLoading={getValidateCodeForAccountMerge?.isLoading}
+ />
+
+
+
+ );
+}
+
+AccountDetailsPage.displayName = 'AccountDetailsPage';
+
+export default AccountDetailsPage;
diff --git a/src/pages/settings/Security/MergeAccounts/AccountValidatePage.tsx b/src/pages/settings/Security/MergeAccounts/AccountValidatePage.tsx
new file mode 100644
index 000000000000..4a3ed3ef7ad0
--- /dev/null
+++ b/src/pages/settings/Security/MergeAccounts/AccountValidatePage.tsx
@@ -0,0 +1,123 @@
+import {useRoute} from '@react-navigation/native';
+import React, {useEffect, useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import ValidateCodeForm from '@components/ValidateCodeActionModal/ValidateCodeForm';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getLatestErrorMessage} from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import {
+ clearMergeWithValidateCode,
+ clearRequestValidationCodeForAccountMerge,
+ mergeWithValidateCode as mergeWithValidateCodeAction,
+ requestValidationCodeForAccountMerge,
+} from '@userActions/MergeAccounts';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+
+const getErrorKey = (err: string): ValueOf | null => {
+ if (err.includes('401 Cannot merge accounts - 2FA enabled')) {
+ return CONST.MERGE_ACCOUNT_RESULTS.ERR_2FA;
+ }
+
+ if (err.includes('401 Not authorized - domain under control')) {
+ return CONST.MERGE_ACCOUNT_RESULTS.ERR_SAML_DOMAIN_CONTROL;
+ }
+
+ if (err.includes('405 Cannot merge account under invoicing')) {
+ return CONST.MERGE_ACCOUNT_RESULTS.ERR_INVOICING;
+ }
+
+ if (err.includes('405 Cannot merge SmartScanner account')) {
+ return CONST.MERGE_ACCOUNT_RESULTS.ERR_SMART_SCANNER;
+ }
+
+ if (err.includes('413')) {
+ return CONST.MERGE_ACCOUNT_RESULTS.ERR_ACCOUNT_LOCKED;
+ }
+
+ return null;
+};
+
+function AccountValidatePage() {
+ const [hasMagicCodeBeenSent, setMagicCodeBeenSent] = useState(false);
+ const [mergeWithValidateCode] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.mergeWithValidateCode});
+ const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE);
+ const {params} = useRoute>();
+
+ const email = params.login ?? '';
+
+ const accountMerged = mergeWithValidateCode?.accountMerged;
+
+ const latestError = getLatestErrorMessage(mergeWithValidateCode);
+ const errorKey = getErrorKey(latestError);
+
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ useEffect(() => {
+ if (!accountMerged || !email) {
+ return;
+ }
+ return Navigation.navigate(ROUTES.SETTINGS_MERGE_ACCOUNTS_RESULT.getRoute(email, 'success'));
+ }, [accountMerged, email]);
+
+ useEffect(() => {
+ if (!errorKey || !email) {
+ return;
+ }
+ return Navigation.navigate(ROUTES.SETTINGS_MERGE_ACCOUNTS_RESULT.getRoute(email, errorKey));
+ }, [errorKey, email]);
+
+ return (
+
+ {
+ clearRequestValidationCodeForAccountMerge();
+ Navigation.goBack(ROUTES.SETTINGS_MERGE_ACCOUNTS);
+ }}
+ shouldDisplayHelpButton={false}
+ />
+
+ {translate('mergeAccountsPage.accountValidate.confirmMerge')}
+
+ {translate('mergeAccountsPage.accountValidate.lossOfUnsubmittedData')}
+ {email}.
+
+
+ {translate('mergeAccountsPage.accountValidate.enterMagicCode')}
+ {email}.
+
+ {
+ setMagicCodeBeenSent(true);
+ mergeWithValidateCodeAction(email, code);
+ }}
+ sendValidateCode={() => requestValidationCodeForAccountMerge(email)}
+ clearError={() => clearMergeWithValidateCode()}
+ validateError={mergeWithValidateCode?.errors}
+ hasMagicCodeBeenSent={hasMagicCodeBeenSent}
+ hideSubmitButton
+ />
+
+
+ );
+}
+
+AccountValidatePage.displayName = 'AccountValidatePage';
+
+export default AccountValidatePage;
diff --git a/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx b/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx
new file mode 100644
index 000000000000..48eab1c736bb
--- /dev/null
+++ b/src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx
@@ -0,0 +1,275 @@
+import {useRoute} from '@react-navigation/native';
+import React, {useEffect, useMemo} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import ConfirmationPage from '@components/ConfirmationPage';
+import type {ConfirmationPageProps} from '@components/ConfirmationPage';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Illustrations from '@components/Icon/Illustrations';
+import LottieAnimations from '@components/LottieAnimations';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import TextLink from '@components/TextLink';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import {openOldDotLink} from '@userActions/Link';
+import {clearMergeWithValidateCode, clearRequestValidationCodeForAccountMerge} from '@userActions/MergeAccounts';
+import {navigateToConciergeChat} from '@userActions/Report';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+
+function MergeResultPage() {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const [mergeWithValidateCode] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.mergeWithValidateCode});
+ const [userEmailOrPhone] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
+ const {params} = useRoute>();
+ const {result, login} = params;
+
+ const mergeStepHadError = Object.values(mergeWithValidateCode?.errors ?? {}).length > 0;
+
+ const defaultResult = {
+ heading: translate('mergeAccountsPage.mergeFailureGenericHeading'),
+ buttonText: translate('common.buttonConfirm'),
+ illustration: Illustrations.LockClosedOrange,
+ };
+
+ const results: Record, ConfirmationPageProps> = useMemo(() => {
+ return {
+ [CONST.MERGE_ACCOUNT_RESULTS.SUCCESS]: {
+ heading: translate('mergeAccountsPage.mergeSuccess.accountsMerged'),
+ description: (
+ <>
+ {translate('mergeAccountsPage.mergeSuccess.successfullyMergedAllData.beforeFirstEmail')}
+ {login}
+ {translate('mergeAccountsPage.mergeSuccess.successfullyMergedAllData.beforeSecondEmail')}
+ {userEmailOrPhone}
+ {translate('mergeAccountsPage.mergeSuccess.successfullyMergedAllData.afterSecondEmail')}
+ >
+ ),
+ buttonText: translate('common.buttonConfirm'),
+ onButtonPress: () => Navigation.goBack(ROUTES.SETTINGS_SECURITY),
+ illustration: LottieAnimations.Fireworks,
+ illustrationStyle: {width: 150, height: 150},
+ },
+ [CONST.MERGE_ACCOUNT_RESULTS.ERR_NO_EXIST]: {
+ heading: translate('mergeAccountsPage.mergeFailureGenericHeading'),
+ description: (
+ <>
+ {translate('mergeAccountsPage.mergeFailureUncreatedAccount.noExpensifyAccount.beforeEmail')}
+ {login}
+ {translate('mergeAccountsPage.mergeFailureUncreatedAccount.noExpensifyAccount.afterEmail')}{' '}
+ {translate('mergeAccountsPage.mergeFailureUncreatedAccount.addContactMethod.beforeLink')}
+ {
+ Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS.getRoute());
+ }}
+ >
+ {translate('mergeAccountsPage.mergeFailureUncreatedAccount.addContactMethod.linkText')}
+
+ {translate('mergeAccountsPage.mergeFailureUncreatedAccount.addContactMethod.afterLink')}
+ >
+ ),
+ onButtonPress: () => Navigation.goBack(ROUTES.SETTINGS_SECURITY),
+ buttonText: translate('common.buttonConfirm'),
+ illustration: Illustrations.LockClosedOrange,
+ },
+ [CONST.MERGE_ACCOUNT_RESULTS.ERR_2FA]: {
+ heading: translate('mergeAccountsPage.mergeFailureGenericHeading'),
+ description: (
+ <>
+ {translate('mergeAccountsPage.mergeFailure2FA.oldAccount2FAEnabled.beforeFirstEmail')}
+ {login}
+ {translate('mergeAccountsPage.mergeFailure2FA.oldAccount2FAEnabled.beforeSecondEmail')}
+ {login}
+ {translate('mergeAccountsPage.mergeFailure2FA.oldAccount2FAEnabled.afterSecondEmail')}{' '}
+
+ {translate('mergeAccountsPage.mergeFailure2FA.learnMore')}
+
+ >
+ ),
+ onButtonPress: () => Navigation.goBack(ROUTES.SETTINGS_SECURITY),
+ buttonText: translate('common.buttonConfirm'),
+ illustration: Illustrations.LockClosedOrange,
+ },
+ [CONST.MERGE_ACCOUNT_RESULTS.ERR_SMART_SCANNER]: {
+ heading: translate('mergeAccountsPage.mergeFailureGenericHeading'),
+ description: (
+ <>
+ {translate('mergeAccountsPage.mergeFailureSmartScannerAccount.beforeEmail')}
+ {login}
+ {translate('mergeAccountsPage.mergeFailureSmartScannerAccount.afterEmail')}
+ >
+ ),
+ buttonText: translate('common.buttonConfirm'),
+ illustration: Illustrations.LockClosedOrange,
+ onButtonPress: () => Navigation.goBack(ROUTES.SETTINGS_SECURITY),
+ },
+ [CONST.MERGE_ACCOUNT_RESULTS.ERR_SAML_DOMAIN_CONTROL]: {
+ heading: translate('mergeAccountsPage.mergeFailureGenericHeading'),
+ description: (
+ <>
+ {translate('mergeAccountsPage.mergeFailureSAMLDomainControl.beforeFirstEmail')}
+ {login}
+ {translate('mergeAccountsPage.mergeFailureSAMLDomainControl.beforeDomain')}
+ {login.split('@').at(1)}
+ {translate('mergeAccountsPage.mergeFailureSAMLDomainControl.afterDomain')}
+ {
+ navigateToConciergeChat();
+ }}
+ >
+ {translate('mergeAccountsPage.mergeFailureSAMLDomainControl.linkText')}
+
+ {translate('mergeAccountsPage.mergeFailureSAMLDomainControl.afterLink')}
+ >
+ ),
+ buttonText: translate('common.buttonConfirm'),
+ onButtonPress: () => Navigation.goBack(ROUTES.SETTINGS_SECURITY),
+ illustration: Illustrations.LockClosedOrange,
+ },
+ [CONST.MERGE_ACCOUNT_RESULTS.ERR_SAML_NOT_SUPPORTED]: {
+ heading: translate('mergeAccountsPage.mergePendingSAML.weAreWorkingOnIt'),
+ description: (
+ <>
+ {translate('mergeAccountsPage.mergePendingSAML.limitedSupport')}
+
+ {translate('mergeAccountsPage.mergePendingSAML.reachOutForHelp.beforeLink')}
+ {
+ navigateToConciergeChat();
+ }}
+ >
+ {translate('mergeAccountsPage.mergePendingSAML.reachOutForHelp.linkText')}
+
+ {translate('mergeAccountsPage.mergePendingSAML.reachOutForHelp.afterLink')}
+
+ >
+ ),
+ secondaryButtonText: translate('mergeAccountsPage.mergePendingSAML.goToExpensifyClassic'),
+ onSecondaryButtonPress: () => openOldDotLink(CONST.OLDDOT_URLS.INBOX, false),
+ shouldShowSecondaryButton: true,
+ buttonText: translate('common.buttonConfirm'),
+ onButtonPress: () => Navigation.goBack(ROUTES.SETTINGS_SECURITY),
+ illustration: Illustrations.RunningTurtle,
+ illustrationStyle: {width: 132, height: 150},
+ },
+ [CONST.MERGE_ACCOUNT_RESULTS.ERR_SAML_PRIMARY_LOGIN]: {
+ heading: translate('mergeAccountsPage.mergeFailureGenericHeading'),
+ description: (
+ <>
+ {translate('mergeAccountsPage.mergeFailureSAMLAccount.beforeEmail')}
+ {login}
+ {translate('mergeAccountsPage.mergeFailureSAMLAccount.afterEmail')}
+ >
+ ),
+ buttonText: translate('common.buttonConfirm'),
+ onButtonPress: () => Navigation.goBack(ROUTES.SETTINGS_SECURITY),
+ illustration: Illustrations.LockClosedOrange,
+ },
+ [CONST.MERGE_ACCOUNT_RESULTS.ERR_ACCOUNT_LOCKED]: {
+ heading: translate('mergeAccountsPage.mergeFailureGenericHeading'),
+ description: (
+ <>
+ {translate('mergeAccountsPage.mergeFailureAccountLocked.beforeEmail')}
+ {login}
+ {translate('mergeAccountsPage.mergeFailureAccountLocked.afterEmail')}
+ {
+ navigateToConciergeChat();
+ }}
+ >
+ {translate('mergeAccountsPage.mergeFailureAccountLocked.linkText')}
+
+ {translate('mergeAccountsPage.mergeFailureAccountLocked.afterLink')}
+ >
+ ),
+ buttonText: translate('common.buttonConfirm'),
+ onButtonPress: () => Navigation.goBack(ROUTES.SETTINGS_SECURITY),
+ illustration: Illustrations.LockClosedOrange,
+ },
+ [CONST.MERGE_ACCOUNT_RESULTS.ERR_INVOICING]: {
+ heading: translate('mergeAccountsPage.mergeFailureGenericHeading'),
+ description: (
+ <>
+ {translate('mergeAccountsPage.mergeFailureInvoicedAccount.beforeEmail')}
+ {login}
+ {translate('mergeAccountsPage.mergeFailureInvoicedAccount.afterEmail')}
+ >
+ ),
+ buttonText: translate('common.buttonConfirm'),
+ onButtonPress: () => Navigation.goBack(ROUTES.SETTINGS_SECURITY),
+ illustration: Illustrations.LockClosedOrange,
+ },
+ };
+ }, [login, translate, userEmailOrPhone, styles]);
+
+ const {
+ heading,
+ headingStyle,
+ onButtonPress,
+ descriptionStyle,
+ illustration,
+ illustrationStyle,
+ description,
+ buttonText,
+ secondaryButtonText,
+ onSecondaryButtonPress,
+ shouldShowSecondaryButton,
+ } = results[result] || defaultResult;
+
+ useEffect(() => {
+ if (result !== CONST.MERGE_ACCOUNT_RESULTS.SUCCESS) {
+ if (mergeStepHadError) {
+ clearMergeWithValidateCode();
+ } else {
+ clearRequestValidationCodeForAccountMerge();
+ }
+ return;
+ }
+
+ clearRequestValidationCodeForAccountMerge();
+ clearMergeWithValidateCode();
+ }, [result, mergeStepHadError]);
+
+ return (
+
+ {
+ const route = mergeStepHadError ? ROUTES.SETTINGS_MERGE_ACCOUNTS_MAGIC_CODE.getRoute(login) : ROUTES.SETTINGS_MERGE_ACCOUNTS;
+ Navigation.goBack(route);
+ }}
+ shouldDisplayHelpButton={false}
+ />
+
+
+ );
+}
+
+MergeResultPage.displayName = 'MergeResultPage';
+
+export default MergeResultPage;
diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx
index fd7696e21c58..a5a0b781e106 100644
--- a/src/pages/settings/Security/SecuritySettingsPage.tsx
+++ b/src/pages/settings/Security/SecuritySettingsPage.tsx
@@ -117,6 +117,11 @@ function SecuritySettingsPage() {
icon: Expensicons.Shield,
action: isActingAsDelegate ? showDelegateNoAccessMenu : waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute())),
},
+ {
+ translationKey: 'mergeAccountsPage.mergeAccount',
+ icon: Expensicons.ArrowCollapse,
+ action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_MERGE_ACCOUNTS)),
+ },
{
translationKey: 'closeAccountPage.closeAccount',
icon: Expensicons.ClosedSign,
diff --git a/src/pages/settings/Wallet/TransferBalancePage.tsx b/src/pages/settings/Wallet/TransferBalancePage.tsx
index 3d1a21d9cec1..f02b34680dba 100644
--- a/src/pages/settings/Wallet/TransferBalancePage.tsx
+++ b/src/pages/settings/Wallet/TransferBalancePage.tsx
@@ -1,7 +1,6 @@
import React, {useEffect} from 'react';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import ConfirmationPage from '@components/ConfirmationPage';
import CurrentWalletBalance from '@components/CurrentWalletBalance';
@@ -16,39 +15,33 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as CurrencyUtils from '@libs/CurrencyUtils';
-import * as ErrorUtils from '@libs/ErrorUtils';
+import {convertToDisplayString} from '@libs/CurrencyUtils';
+import {getLatestErrorMessage} from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as PaymentUtils from '@libs/PaymentUtils';
+import {calculateWalletTransferBalanceFee, formatPaymentMethods, hasExpensifyPaymentMethod} from '@libs/PaymentUtils';
import variables from '@styles/variables';
-import * as PaymentMethods from '@userActions/PaymentMethods';
+import {
+ dismissSuccessfulTransferBalancePage,
+ resetWalletTransferData,
+ saveWalletTransferAccountTypeAndID,
+ saveWalletTransferMethodType,
+ transferWalletBalance,
+} from '@userActions/PaymentMethods';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {BankAccountList, FundList, UserWallet, WalletTransfer} from '@src/types/onyx';
import type PaymentMethod from '@src/types/onyx/PaymentMethod';
import type {FilterMethodPaymentType} from '@src/types/onyx/WalletTransfer';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
-type TransferBalancePageOnyxProps = {
- /** User's wallet information */
- userWallet: OnyxEntry;
-
- /** List of bank accounts */
- bankAccountList: OnyxEntry;
-
- /** List of user's card objects */
- fundList: OnyxEntry;
-
- /** Wallet balance transfer props */
- walletTransfer: OnyxEntry;
-};
-
-type TransferBalancePageProps = TransferBalancePageOnyxProps;
-
const TRANSFER_TIER_NAMES: string[] = [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM];
-function TransferBalancePage({bankAccountList, fundList, userWallet, walletTransfer}: TransferBalancePageProps) {
+function TransferBalancePage() {
+ const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
+ const [walletTransfer] = useOnyx(ONYXKEYS.WALLET_TRANSFER);
+ const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
+ const [fundList] = useOnyx(ONYXKEYS.FUND_LIST);
+
const styles = useThemeStyles();
const {numberFormat, translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -61,7 +54,7 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
title: translate('transferAmountPage.instant'),
description: translate('transferAmountPage.instantSummary', {
rate: numberFormat(CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.RATE),
- minAmount: CurrencyUtils.convertToDisplayString(CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.MINIMUM_FEE),
+ minAmount: convertToDisplayString(CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.MINIMUM_FEE),
}),
icon: Expensicons.Bolt,
type: CONST.PAYMENT_METHODS.DEBIT_CARD,
@@ -79,7 +72,7 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
* Get the selected/default payment method account for wallet transfer
*/
function getSelectedPaymentMethodAccount(): PaymentMethod | undefined {
- const paymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles);
+ const paymentMethods = formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles);
const defaultAccount = paymentMethods.find((method) => method.isDefault);
const selectedAccount = paymentMethods.find(
@@ -89,15 +82,15 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
}
function navigateToChooseTransferAccount(filterPaymentMethodType: FilterMethodPaymentType) {
- PaymentMethods.saveWalletTransferMethodType(filterPaymentMethodType);
+ saveWalletTransferMethodType(filterPaymentMethodType);
// If we only have a single option for the given paymentMethodType do not force the user to make a selection
- const combinedPaymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles);
+ const combinedPaymentMethods = formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles);
const filteredMethods = combinedPaymentMethods.filter((paymentMethod) => paymentMethod.accountType === filterPaymentMethodType);
if (filteredMethods.length === 1) {
const account = filteredMethods.at(0);
- PaymentMethods.saveWalletTransferAccountTypeAndID(filterPaymentMethodType ?? '', account?.methodID?.toString() ?? '-1');
+ saveWalletTransferAccountTypeAndID(filterPaymentMethodType, account?.methodID?.toString() ?? CONST.DEFAULT_STRING_NEGATIVE_ID);
return;
}
@@ -106,14 +99,14 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
useEffect(() => {
// Reset to the default account when the page is opened
- PaymentMethods.resetWalletTransferData();
+ resetWalletTransferData();
const selectedAccount = getSelectedPaymentMethodAccount();
if (!selectedAccount) {
return;
}
- PaymentMethods.saveWalletTransferAccountTypeAndID(selectedAccount?.accountType ?? '', selectedAccount?.methodID?.toString() ?? '-1');
+ saveWalletTransferAccountTypeAndID(selectedAccount?.accountType, selectedAccount?.methodID?.toString() ?? CONST.DEFAULT_STRING_NEGATIVE_ID);
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we only want this effect to run on initial render
}, []);
@@ -122,7 +115,7 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
);
@@ -143,13 +136,13 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
const selectedPaymentType =
selectedAccount && selectedAccount.accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT ? CONST.WALLET.TRANSFER_METHOD_TYPE.ACH : CONST.WALLET.TRANSFER_METHOD_TYPE.INSTANT;
- const calculatedFee = PaymentUtils.calculateWalletTransferBalanceFee(userWallet?.currentBalance ?? 0, selectedPaymentType);
+ const calculatedFee = calculateWalletTransferBalanceFee(userWallet?.currentBalance ?? 0, selectedPaymentType);
const transferAmount = userWallet?.currentBalance ?? 0 - calculatedFee;
const isTransferable = transferAmount > 0;
const isButtonDisabled = !isTransferable || !selectedAccount;
- const errorMessage = ErrorUtils.getLatestErrorMessage(walletTransfer);
+ const errorMessage = getLatestErrorMessage(walletTransfer);
- const shouldShowTransferView = PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList ?? {}) && TRANSFER_TIER_NAMES.includes(userWallet?.tierName ?? '');
+ const shouldShowTransferView = hasExpensifyPaymentMethod(paymentCardList, bankAccountList ?? {}) && TRANSFER_TIER_NAMES.includes(userWallet?.tierName ?? '');
return (
@@ -206,16 +199,16 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
)}
{translate('transferAmountPage.fee')}
- {CurrencyUtils.convertToDisplayString(calculatedFee)}
+ {convertToDisplayString(calculatedFee)}
selectedAccount && PaymentMethods.transferWalletBalance(selectedAccount)}
+ onSubmit={() => selectedAccount && transferWalletBalance(selectedAccount)}
isDisabled={isButtonDisabled || isOffline}
message={errorMessage}
isAlertVisible={!isEmptyObject(errorMessage)}
@@ -229,17 +222,4 @@ function TransferBalancePage({bankAccountList, fundList, userWallet, walletTrans
TransferBalancePage.displayName = 'TransferBalancePage';
-export default withOnyx({
- userWallet: {
- key: ONYXKEYS.USER_WALLET,
- },
- walletTransfer: {
- key: ONYXKEYS.WALLET_TRANSFER,
- },
- bankAccountList: {
- key: ONYXKEYS.BANK_ACCOUNT_LIST,
- },
- fundList: {
- key: ONYXKEYS.FUND_LIST,
- },
-})(TransferBalancePage);
+export default TransferBalancePage;
diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx
index fc2f919370e2..7af17782d634 100644
--- a/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx
+++ b/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx
@@ -9,7 +9,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig
import Navigation from '@navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
-import * as MemberActions from '@userActions/Policy/Member';
+import {clearWorkspaceOwnerChangeFlow} from '@userActions/Policy/Member';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -24,7 +24,7 @@ function WorkspaceOwnerChangeSuccessPage({route}: WorkspaceOwnerChangeSuccessPag
const policyID = route.params.policyID;
const closePage = useCallback(() => {
- MemberActions.clearWorkspaceOwnerChangeFlow(policyID);
+ clearWorkspaceOwnerChangeFlow(policyID);
Navigation.goBack();
Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, accountID));
}, [accountID, policyID]);
diff --git a/src/types/form/MergeAccountDetailsForm.ts b/src/types/form/MergeAccountDetailsForm.ts
new file mode 100644
index 000000000000..541e97f012bb
--- /dev/null
+++ b/src/types/form/MergeAccountDetailsForm.ts
@@ -0,0 +1,20 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ PHONE_OR_EMAIL: 'mergeAccountPhoneOrEmail',
+ CONSENT: 'mergeAccountConsent',
+} as const;
+
+type InputID = ValueOf;
+
+type MergeAccountDetailsForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.PHONE_OR_EMAIL]: string;
+ [INPUT_IDS.CONSENT]: boolean;
+ }
+>;
+
+export type {MergeAccountDetailsForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index 402c60110c8f..44863f5fdadd 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -88,6 +88,7 @@ export type {WorkspaceCompanyCardFeedName} from './WorkspaceCompanyCardFeedName'
export type {SearchSavedSearchRenameForm} from './SearchSavedSearchRenameForm';
export type {WorkspaceCompanyCardEditName} from './WorkspaceCompanyCardEditName';
export type {PersonalDetailsForm} from './PersonalDetailsForm';
+export type {MergeAccountDetailsForm} from './MergeAccountDetailsForm';
export type {WorkspaceConfirmationForm} from './WorkspaceConfirmationForm';
export type {MoneyRequestTimeForm} from './MoneyRequestTimeForm';
export type {MoneyRequestSubrateForm} from './MoneyRequestSubrateForm';
diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts
index 97fa83dc8670..18786d965bf8 100644
--- a/src/types/onyx/Account.ts
+++ b/src/types/onyx/Account.ts
@@ -174,6 +174,30 @@ type Account = {
/** The calendar link of the guide details */
calendarLink: string;
};
+
+ /** Model of the getValidateCodeForAccountMerge API call */
+ getValidateCodeForAccountMerge?: {
+ /** Whether the validation code was sent */
+ isLoading?: boolean;
+
+ /** Whether the user validation code was sent */
+ validateCodeSent?: boolean;
+
+ /** Errors while requesting the validation code */
+ errors: OnyxCommon.Errors;
+ };
+
+ /** Model of the mergeWithValidateCode API call */
+ mergeWithValidateCode?: {
+ /** Whether the API call is loading */
+ isLoading?: boolean;
+
+ /** Whether the account was merged successfully */
+ accountMerged?: boolean;
+
+ /** Errors while merging the account */
+ errors: OnyxCommon.Errors;
+ };
};
export default Account;