Skip to content

Commit 53786aa

Browse files
authored
Merge pull request #56911 from allroundexperts/feat-47073
Add merge account feature to ND
2 parents be30e8d + ddce6cf commit 53786aa

33 files changed

+1247
-40
lines changed

assets/images/arrow-collapse.svg

+3
Loading

assets/images/running-turtle.svg

+96
Loading

src/CONST.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const EMPTY_ARRAY = Object.freeze([]);
1818
const EMPTY_OBJECT = Object.freeze({});
1919

2020
const DEFAULT_NUMBER_ID = 0;
21-
2221
const CLOUDFRONT_DOMAIN = 'cloudfront.net';
2322
const CLOUDFRONT_URL = `https://d2k5nsl2zxldvw.${CLOUDFRONT_DOMAIN}`;
2423
const ACTIVE_EXPENSIFY_URL = addTrailingForwardSlash(Config?.NEW_EXPENSIFY_URL ?? 'https://new.expensify.com');
@@ -1060,6 +1059,7 @@ const CONST = {
10601059
DELAYED_SUBMISSION_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/reports/Automatically-submit-employee-reports',
10611060
ENCRYPTION_AND_SECURITY_HELP_URL: 'https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security',
10621061
PLAN_TYPES_AND_PRICING_HELP_URL: 'https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing',
1062+
MERGE_ACCOUNT_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/settings/Merge-accounts',
10631063
TEST_RECEIPT_URL: `${CLOUDFRONT_URL}/images/fake-receipt__tacotodds.png`,
10641064
// Use Environment.getEnvironmentURL to get the complete URL with port number
10651065
DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:',
@@ -4977,6 +4977,19 @@ const CONST = {
49774977
DISABLED: 'DISABLED',
49784978
DISABLE: 'DISABLE',
49794979
},
4980+
MERGE_ACCOUNT_RESULTS: {
4981+
SUCCESS: 'success',
4982+
ERR_2FA: 'err_2fa',
4983+
ERR_NO_EXIST: 'err_no_exist',
4984+
ERR_SMART_SCANNER: 'err_smart_scanner',
4985+
ERR_INVOICING: 'err_invoicing',
4986+
ERR_SAML_PRIMARY_LOGIN: 'err_saml_primary_login',
4987+
ERR_SAML_DOMAIN_CONTROL: 'err_saml_domain_control',
4988+
ERR_SAML_NOT_SUPPORTED: 'err_saml_not_supported',
4989+
ERR_ACCOUNT_LOCKED: 'err_account_locked',
4990+
TOO_MANY_ATTEMPTS: 'too_many_attempts',
4991+
ACCOUNT_UNVALIDATED: 'account_unvalidated',
4992+
},
49804993
DELEGATE_ROLE: {
49814994
ALL: 'all',
49824995
SUBMITTER: 'submitter',

src/ONYXKEYS.ts

+3
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,8 @@ const ONYXKEYS = {
774774
RULES_CUSTOM_FORM_DRAFT: 'rulesCustomFormDraft',
775775
DEBUG_DETAILS_FORM: 'debugDetailsForm',
776776
DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft',
777+
MERGE_ACCOUNT_DETAILS_FORM: 'mergeAccountDetailsForm',
778+
MERGE_ACCOUNT_DETAILS_FORM_DRAFT: 'mergeAccountDetailsFormDraft',
777779
WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm',
778780
WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft',
779781
},
@@ -875,6 +877,7 @@ type OnyxFormValuesMapping = {
875877
[ONYXKEYS.FORMS.RULES_CUSTOM_FORM]: FormTypes.RulesCustomForm;
876878
[ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm;
877879
[ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm;
880+
[ONYXKEYS.FORMS.MERGE_ACCOUNT_DETAILS_FORM]: FormTypes.MergeAccountDetailsForm;
878881
[ONYXKEYS.FORMS.INTERNATIONAL_BANK_ACCOUNT_FORM]: FormTypes.InternationalBankAccountForm;
879882
[ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm;
880883
};

src/ROUTES.ts

+12
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,18 @@ const ROUTES = {
186186
SETTINGS_WORKSPACES: {route: 'settings/workspaces', getRoute: (backTo?: string) => getUrlWithBackToParam('settings/workspaces', backTo)},
187187
SETTINGS_SECURITY: 'settings/security',
188188
SETTINGS_CLOSE: 'settings/security/closeAccount',
189+
SETTINGS_MERGE_ACCOUNTS: {
190+
route: 'settings/security/merge-accounts',
191+
getRoute: (email?: string) => `settings/security/merge-accounts${email ? `?email=${encodeURIComponent(email)}` : ''}` as const,
192+
},
193+
SETTINGS_MERGE_ACCOUNTS_MAGIC_CODE: {
194+
route: 'settings/security/merge-accounts/:login/magic-code',
195+
getRoute: (login: string) => `settings/security/merge-accounts/${encodeURIComponent(login)}/magic-code` as const,
196+
},
197+
SETTINGS_MERGE_ACCOUNTS_RESULT: {
198+
route: 'settings/security/merge-accounts/:login/result/:result',
199+
getRoute: (login: string, result: string, backTo?: string) => getUrlWithBackToParam(`settings/security/merge-accounts/${encodeURIComponent(login)}/result/${result}`, backTo),
200+
},
189201
SETTINGS_ADD_DELEGATE: 'settings/security/delegate',
190202
SETTINGS_DELEGATE_ROLE: {
191203
route: 'settings/security/delegate/:login/role/:role',

src/SCREENS.ts

+5
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ const SCREENS = {
146146
DELEGATE_CONFIRM: 'Settings_Delegate_Confirm',
147147
UPDATE_DELEGATE_ROLE: 'Settings_Delegate_Update_Role',
148148
},
149+
MERGE_ACCOUNTS: {
150+
ACCOUNT_DETAILS: 'Settings_MergeAccounts_AccountDetails',
151+
ACCOUNT_VALIDATE: 'Settings_MergeAccounts_AccountValidate',
152+
MERGE_RESULT: 'Settings_MergeAccounts_MergeResult',
153+
},
149154
},
150155
TWO_FACTOR_AUTH: {
151156
ROOT: 'Settings_TwoFactorAuth_Root',

src/components/ConfirmationPage.tsx

+56-14
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,27 @@ type ConfirmationPageProps = {
2222
/** Description of the confirmation page */
2323
description: React.ReactNode;
2424

25-
/** The text for the button label */
25+
/** The text for the call to action */
26+
cta?: React.ReactNode;
27+
28+
/** The text for the primary button label */
2629
buttonText?: string;
2730

28-
/** A function that is called when the button is clicked on */
31+
/** A function that is called when the primary button is clicked on */
2932
onButtonPress?: () => void;
3033

31-
/** Whether we should show a confirmation button */
34+
/** Whether we should show a primary confirmation button */
3235
shouldShowButton?: boolean;
3336

37+
/** The text for the secondary button label */
38+
secondaryButtonText?: string;
39+
40+
/** A function that is called when the secondary button is clicked on */
41+
onSecondaryButtonPress?: () => void;
42+
43+
/** Whether we should show a secondary confirmation button */
44+
shouldShowSecondaryButton?: boolean;
45+
3446
/** Additional style for the heading */
3547
headingStyle?: TextStyle;
3648

@@ -40,6 +52,12 @@ type ConfirmationPageProps = {
4052
/** Additional style for the description */
4153
descriptionStyle?: TextStyle;
4254

55+
/** Additional style for the cta */
56+
ctaStyle?: TextStyle;
57+
58+
/** Additional style for the footer */
59+
footerStyle?: ViewStyle;
60+
4361
/** Additional style for the container */
4462
containerStyle?: ViewStyle;
4563
};
@@ -48,12 +66,18 @@ function ConfirmationPage({
4866
illustration = LottieAnimations.Fireworks,
4967
heading,
5068
description,
69+
cta,
5170
buttonText = '',
5271
onButtonPress = () => {},
5372
shouldShowButton = false,
73+
secondaryButtonText = '',
74+
onSecondaryButtonPress = () => {},
75+
shouldShowSecondaryButton = false,
5476
headingStyle,
5577
illustrationStyle,
5678
descriptionStyle,
79+
ctaStyle,
80+
footerStyle,
5781
containerStyle,
5882
}: ConfirmationPageProps) {
5983
const styles = useThemeStyles();
@@ -68,6 +92,10 @@ function ConfirmationPage({
6892
autoPlay
6993
loop
7094
style={[styles.confirmationAnimation, illustrationStyle]}
95+
webStyle={{
96+
width: (illustrationStyle?.width as number) ?? styles.confirmationAnimation.width,
97+
height: (illustrationStyle?.height as number) ?? styles.confirmationAnimation.height,
98+
}}
7199
/>
72100
) : (
73101
<View style={[styles.confirmationAnimation, illustrationStyle]}>
@@ -79,18 +107,30 @@ function ConfirmationPage({
79107
)}
80108
<Text style={[styles.textHeadline, styles.textAlignCenter, styles.mv2, headingStyle]}>{heading}</Text>
81109
<Text style={[styles.textAlignCenter, descriptionStyle]}>{description}</Text>
110+
{cta ? <Text style={[styles.textAlignCenter, ctaStyle]}>{cta}</Text> : null}
82111
</View>
83-
{shouldShowButton && (
84-
<FixedFooter>
85-
<Button
86-
success
87-
large
88-
text={buttonText}
89-
testID="confirmation-button"
90-
style={styles.mt6}
91-
pressOnEnter
92-
onPress={onButtonPress}
93-
/>
112+
{(shouldShowSecondaryButton || shouldShowButton) && (
113+
<FixedFooter style={footerStyle}>
114+
{shouldShowSecondaryButton && (
115+
<Button
116+
large
117+
text={secondaryButtonText}
118+
testID="confirmation-secondary-button"
119+
style={styles.mt3}
120+
onPress={onSecondaryButtonPress}
121+
/>
122+
)}
123+
{shouldShowButton && (
124+
<Button
125+
success
126+
large
127+
text={buttonText}
128+
testID="confirmation-primary-button"
129+
style={styles.mt3}
130+
pressOnEnter
131+
onPress={onButtonPress}
132+
/>
133+
)}
94134
</FixedFooter>
95135
)}
96136
</View>
@@ -100,3 +140,5 @@ function ConfirmationPage({
100140
ConfirmationPage.displayName = 'ConfirmationPage';
101141

102142
export default ConfirmationPage;
143+
144+
export type {ConfirmationPageProps};

src/components/Form/FormProvider.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ function FormProvider(
282282
resetForm,
283283
resetErrors,
284284
resetFormFieldError,
285+
submit,
285286
}));
286287

287288
const registerInput = useCallback<RegisterInput>(

src/components/Form/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ type FormRef<TFormID extends OnyxFormKey = OnyxFormKey> = {
175175
resetForm: (optionalValue: FormOnyxValues<TFormID>) => void;
176176
resetErrors: () => void;
177177
resetFormFieldError: (fieldID: keyof Form) => void;
178+
submit: () => void;
178179
};
179180

180181
type InputRefs = Record<string, MutableRefObject<InputComponentBaseProps>>;

src/components/Icon/Expensicons.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import AddReaction from '@assets/images/add-reaction.svg';
22
import All from '@assets/images/all.svg';
33
import Android from '@assets/images/android.svg';
44
import Apple from '@assets/images/apple.svg';
5+
import ArrowCollapse from '@assets/images/arrow-collapse.svg';
56
import ArrowDownLong from '@assets/images/arrow-down-long.svg';
67
import ArrowRightLong from '@assets/images/arrow-right-long.svg';
78
import ArrowRight from '@assets/images/arrow-right.svg';
@@ -228,6 +229,7 @@ export {
228229
AnnounceRoomAvatar,
229230
Apple,
230231
AppleLogo,
232+
ArrowCollapse,
231233
ArrowRight,
232234
ArrowRightLong,
233235
ArrowsUpDown,

src/components/Icon/Illustrations.ts

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import TeleScope from '@assets/images/product-illustrations/telescope.svg';
6565
import ThreeLeggedLaptopWoman from '@assets/images/product-illustrations/three_legged_laptop_woman.svg';
6666
import ToddBehindCloud from '@assets/images/product-illustrations/todd-behind-cloud.svg';
6767
import ToddWithPhones from '@assets/images/product-illustrations/todd-with-phones.svg';
68+
import RunningTurtle from '@assets/images/running-turtle.svg';
6869
import ReportReceipt from '@assets/images/simple-illustration__report-receipt.svg';
6970
import BigVault from '@assets/images/simple-illustrations/emptystate__big-vault.svg';
7071
import Puzzle from '@assets/images/simple-illustrations/emptystate__puzzlepieces.svg';
@@ -280,6 +281,7 @@ export {
280281
Filters,
281282
MagnifyingGlassMoney,
282283
Rules,
284+
RunningTurtle,
283285
CompanyCardsEmptyState,
284286
AmexCompanyCards,
285287
MasterCardCompanyCards,

src/components/ValidateCodeActionForm/index.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ function ValidateCodeActionForm({
1818
sendValidateCode,
1919
hasMagicCodeBeenSent,
2020
isLoading,
21+
submitButtonText,
2122
forwardedRef,
23+
shouldSkipInitialValidation,
2224
}: ValidateCodeActionFormProps) {
2325
const themeStyles = useThemeStyles();
2426

@@ -27,13 +29,15 @@ function ValidateCodeActionForm({
2729
const isUnmounted = useRef(false);
2830

2931
useEffect(() => {
30-
sendValidateCode();
32+
if (!shouldSkipInitialValidation) {
33+
sendValidateCode();
34+
}
3135

3236
return () => {
3337
isUnmounted.current = true;
3438
};
3539
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
36-
}, []);
40+
}, [shouldSkipInitialValidation]);
3741

3842
useEffect(() => {
3943
return () => {
@@ -59,6 +63,7 @@ function ValidateCodeActionForm({
5963
buttonStyles={[themeStyles.justifyContentEnd, themeStyles.flex1]}
6064
ref={forwardedRef}
6165
hasMagicCodeBeenSent={hasMagicCodeBeenSent}
66+
submitButtonText={submitButtonText}
6267
/>
6368
</View>
6469
);

src/components/ValidateCodeActionForm/type.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import type {ForwardedRef} from 'react';
1+
import type {ForwardedRef, ReactNode} from 'react';
22
import type {ValidateCodeFormHandle} from '@components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm';
33
import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
44

55
type ValidateCodeActionFormProps = {
66
/** Primary description of the modal */
7-
descriptionPrimary: string;
7+
descriptionPrimary: ReactNode;
88

99
/** Secondary description of the modal */
10-
descriptionSecondary?: string | null;
10+
descriptionSecondary?: ReactNode;
1111

1212
/** The pending action for submitting form */
1313
validatePendingAction?: PendingAction | null;
@@ -32,6 +32,12 @@ type ValidateCodeActionFormProps = {
3232

3333
/** Ref for validate code form */
3434
forwardedRef: ForwardedRef<ValidateCodeFormHandle>;
35+
36+
/** Text for submit button */
37+
submitButtonText?: string;
38+
39+
/** Skip the call to sendValidateCode fn on initial render */
40+
shouldSkipInitialValidation?: boolean;
3541
};
3642

3743
// eslint-disable-next-line import/prefer-default-export

src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ type ValidateCodeFormProps = {
6767
/** Whether to show the verify button */
6868
hideSubmitButton?: boolean;
6969

70+
/** Text for the verify button */
71+
submitButtonText?: string;
72+
7073
/** Function is called when validate code modal is mounted and on magic code resend */
7174
sendValidateCode: () => void;
7275

@@ -86,6 +89,7 @@ function BaseValidateCodeForm({
8689
sendValidateCode,
8790
buttonStyles,
8891
hideSubmitButton,
92+
submitButtonText,
8993
isLoading,
9094
}: ValidateCodeFormProps) {
9195
const {translate} = useLocalize();
@@ -284,7 +288,7 @@ function BaseValidateCodeForm({
284288
{!hideSubmitButton && (
285289
<Button
286290
isDisabled={isOffline}
287-
text={translate('common.verify')}
291+
text={submitButtonText ?? translate('common.verify')}
288292
onPress={validateAndSubmitForm}
289293
style={[styles.mt4]}
290294
success

0 commit comments

Comments
 (0)