Skip to content

Commit 7df31cf

Browse files
authored
feat: add logout button to BasicInfo error banner for Transak phone already registered error (MetaMask#22183)
## **Description** This PR adds a logout button to the BasicInfo screen's error banner when users encounter Transak error code 2020 (phone number already registered with a different email). This allows users to logout and return to the email input screen to authenticate with the correct email address. **What is the reason for the change?** When users authenticate with the wrong email in the Transak deposit flow, if they enter a phone number that's already registered to a different email, they see an error message but have no way to logout and try again with the correct email. This forces them to restart the entire app or navigate to settings to logout manually. **What is the improvement/solution?** - Added error code 2020 detection from Transak API response structure - Added localized error message that extracts and displays the registered email - Added "Log in with email" button in the error banner that appears only for error code 2020 - Clicking the button clears authentication and navigates back to email input screen - Reuses existing logout functionality and localization strings - Added comprehensive test coverage ## **Changelog** CHANGELOG entry: Added logout button to Transak deposit flow when phone number is already registered ## **Related issues** Fixes: [TRAM-2799](https://consensyssoftware.atlassian.net/browse/TRAM-2799) ## **Manual testing steps** ### Option 1: Testing with Real Transak (E2E Validation) ```gherkin Feature: Logout on Transak phone already registered error Scenario: user logs out when encountering phone already registered error Given user has authenticated with email A in Transak deposit flow And user navigates to BasicInfo screen And user enters a phone number already registered to email B When user submits the form Then error banner displays with message "This phone number is already in use by {masked_email}. Log in using this email to continue." And "Log in with email" button is visible in the error banner When user clicks the "Log in with email" button Then user is logged out from Transak And user is navigated back to the email input screen And user can now enter the correct email to authenticate ``` **Note:** Requires two Transak accounts with the same phone number registered to different emails. --- ### Option 2: Testing with Mock Error (Quick Validation) **1. Add temporary mock** in `BasicInfo.tsx` around line 212: ```typescript try { // TEMPORARY: Simulate Transak error code 2020 const mockError = Object.assign(new Error('API Error'), { error: { errorCode: 2020, message: 'This phone number is already registered. It has been used by an account created with k****@pedalsup.com. Login with this email to continue.', }, }); throw mockError; // Comment out original code for testing: // setLoading(true); // const { ssn, ...formDataWithoutSsn } = formData; // await postKycForm({ personalDetails: { ... } }); ``` **2. Run on simulator:** ```bash yarn watch # Press 'i' for iOS or 'a' for Android ``` **3. Test the flow:** - Navigate to Deposit → Verify Identity → BasicInfo - Fill in form with any valid data - Click "Continue" - ✅ Verify error banner shows: "This phone number is already in use by k****@pedalsup.com. Log in using this email to continue." - ✅ Verify button shows: "Log in with email" - Click the button - ✅ Verify navigation back to email input screen - ✅ Verify no console errors **4. Clean up:** Remove the mock code before committing ## **Screenshots/Recordings** ### **Before** Error banner without logout button - user had no way to logout and try with correct email <img width="787" height="1704" alt="image" src="https://github.com/user-attachments/assets/58285702-078c-4c00-99b9-d36300e6649e" /> ### **After** **Error Object From Transak** <img width="2174" height="1330" alt="image" src="https://github.com/user-attachments/assets/3c175ebd-3df9-4447-8fac-3a3cbcca8e34" /> **Error banner with "Log in with email" button as shown in design mockup** https://github.com/user-attachments/assets/476ae836-beaf-454b-9806-9350920f3cfb ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. [TRAM-2799]: https://consensyssoftware.atlassian.net/browse/TRAM-2799?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds detection of Transak error 2020 on BasicInfo, displaying a localized message with the masked email and a "Log in with email" action that logs out and navigates to EnterEmail. > > - **Deposit › Verify Identity (`BasicInfo.tsx`)**: > - Detects Transak API `error.errorCode === 2020` (phone already registered). > - Extracts masked email from `error.message` and shows localized error `deposit.basic_info.phone_already_registered`. > - Displays BannerAlert action button "Log in with email" (`testID: "basic-info-logout-button"`). > - On click, calls `logoutFromProvider(false)` and navigates to `...createEnterEmailNavDetails()`. > - Resets error/UI flags on retry and field changes; integrates `logoutFromProvider` from `useDepositSDK`. > - **Tests (`BasicInfo.test.tsx`)**: > - Adds cases for: showing logout button on `2020` errors, formatting message with email, no action for generic errors, successful logout + navigation, and graceful logout failure logging. > - **Localization (`locales/languages/en.json`)**: > - Adds `deposit.basic_info.phone_already_registered` and `deposit.basic_info.login_with_email`; keeps `unexpected_error`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6bf8336. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 36df852 commit 7df31cf

3 files changed

Lines changed: 294 additions & 8 deletions

File tree

app/components/UI/Ramp/Deposit/Views/BasicInfo/BasicInfo.test.tsx

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,4 +373,235 @@ describe('BasicInfo Component', () => {
373373
render(BasicInfo);
374374
expect(screen.toJSON()).toMatchSnapshot();
375375
});
376+
377+
describe('when phone already registered error occurs', () => {
378+
const mockLogoutFromProvider = jest.fn();
379+
380+
beforeEach(() => {
381+
mockLogoutFromProvider.mockResolvedValue(undefined);
382+
mockPostKycForm.mockResolvedValue(undefined);
383+
mockSubmitSsnDetails.mockResolvedValue(undefined);
384+
mockUseDepositSDK.mockReturnValue({
385+
selectedRegion: mockSelectedRegion,
386+
logoutFromProvider: mockLogoutFromProvider,
387+
});
388+
});
389+
390+
afterEach(() => {
391+
mockLogoutFromProvider.mockClear();
392+
});
393+
394+
it('displays logout button when error has errorCode 2020', async () => {
395+
// Mock Transak API error structure: { error: { errorCode: 2020, message: "..." } }
396+
const error2020 = Object.assign(new Error('API Error'), {
397+
error: {
398+
errorCode: 2020,
399+
message:
400+
'This phone number is already registered. It has been used by an account created with k****@pedalsup.com. Login with this email to continue.',
401+
},
402+
});
403+
mockPostKycForm.mockRejectedValueOnce(error2020);
404+
405+
render(BasicInfo);
406+
407+
fireEvent.changeText(screen.getByTestId('first-name-input'), 'John');
408+
fireEvent.changeText(screen.getByTestId('last-name-input'), 'Smith');
409+
fireEvent.changeText(
410+
screen.getByTestId('deposit-phone-field-test-id'),
411+
'234567890',
412+
);
413+
fireEvent.changeText(
414+
screen.getByTestId('date-of-birth-input'),
415+
'01/01/1990',
416+
);
417+
fireEvent.changeText(screen.getByTestId('ssn-input'), '123456789');
418+
419+
await act(async () => {
420+
fireEvent.press(screen.getByTestId('continue-button'));
421+
});
422+
423+
// Wait for error message to appear
424+
await screen.findByText(
425+
'This phone number is already in use by k****@pedalsup.com. Log in using this email to continue.',
426+
);
427+
428+
// Verify logout button with correct label is displayed
429+
const logoutButton = await screen.findByTestId(
430+
'basic-info-logout-button',
431+
);
432+
expect(logoutButton).toBeOnTheScreen();
433+
expect(screen.getByText('Log in with email')).toBeOnTheScreen();
434+
});
435+
436+
it('displays formatted error message for errorCode 2020', async () => {
437+
// Mock Transak API error structure with email in message
438+
const error2020 = Object.assign(new Error('API Error'), {
439+
error: {
440+
errorCode: 2020,
441+
message:
442+
'This phone number is already registered. It has been used by an account created with k****@pedalsup.com. Login with this email to continue.',
443+
},
444+
});
445+
mockPostKycForm.mockRejectedValueOnce(error2020);
446+
447+
render(BasicInfo);
448+
449+
fireEvent.changeText(screen.getByTestId('first-name-input'), 'John');
450+
fireEvent.changeText(screen.getByTestId('last-name-input'), 'Smith');
451+
fireEvent.changeText(
452+
screen.getByTestId('deposit-phone-field-test-id'),
453+
'234567890',
454+
);
455+
fireEvent.changeText(
456+
screen.getByTestId('date-of-birth-input'),
457+
'01/01/1990',
458+
);
459+
fireEvent.changeText(screen.getByTestId('ssn-input'), '123456789');
460+
461+
await act(async () => {
462+
fireEvent.press(screen.getByTestId('continue-button'));
463+
});
464+
465+
// Verify formatted error message is displayed with localized string
466+
await screen.findByText(
467+
'This phone number is already in use by k****@pedalsup.com. Log in using this email to continue.',
468+
);
469+
470+
// Verify logout button with correct label is displayed
471+
const logoutButton = await screen.findByTestId(
472+
'basic-info-logout-button',
473+
);
474+
expect(logoutButton).toBeOnTheScreen();
475+
expect(screen.getByText('Log in with email')).toBeOnTheScreen();
476+
});
477+
478+
it('does not display logout button for generic errors', async () => {
479+
const genericError = new Error('Network error');
480+
mockPostKycForm.mockRejectedValueOnce(genericError);
481+
482+
render(BasicInfo);
483+
484+
fireEvent.changeText(screen.getByTestId('first-name-input'), 'John');
485+
fireEvent.changeText(screen.getByTestId('last-name-input'), 'Smith');
486+
fireEvent.changeText(
487+
screen.getByTestId('deposit-phone-field-test-id'),
488+
'234567890',
489+
);
490+
fireEvent.changeText(
491+
screen.getByTestId('date-of-birth-input'),
492+
'01/01/1990',
493+
);
494+
fireEvent.changeText(screen.getByTestId('ssn-input'), '123456789');
495+
496+
await act(async () => {
497+
fireEvent.press(screen.getByTestId('continue-button'));
498+
});
499+
500+
// Wait for generic error to appear
501+
await screen.findByText('Network error');
502+
503+
// Verify logout button is NOT displayed for generic errors
504+
expect(screen.queryByTestId('basic-info-logout-button')).toBeNull();
505+
});
506+
507+
it('calls logoutFromProvider and navigates to EnterEmail on logout click', async () => {
508+
const error2020 = Object.assign(new Error('API Error'), {
509+
error: {
510+
errorCode: 2020,
511+
message:
512+
'This phone number is already registered. It has been used by an account created with test@gmail.com. Login with this email to continue.',
513+
},
514+
});
515+
mockPostKycForm.mockRejectedValueOnce(error2020);
516+
517+
render(BasicInfo);
518+
519+
fireEvent.changeText(screen.getByTestId('first-name-input'), 'John');
520+
fireEvent.changeText(screen.getByTestId('last-name-input'), 'Smith');
521+
fireEvent.changeText(
522+
screen.getByTestId('deposit-phone-field-test-id'),
523+
'234567890',
524+
);
525+
fireEvent.changeText(
526+
screen.getByTestId('date-of-birth-input'),
527+
'01/01/1990',
528+
);
529+
fireEvent.changeText(screen.getByTestId('ssn-input'), '123456789');
530+
531+
await act(async () => {
532+
fireEvent.press(screen.getByTestId('continue-button'));
533+
});
534+
535+
// Wait for error and logout button to appear
536+
const logoutButton = await screen.findByTestId(
537+
'basic-info-logout-button',
538+
);
539+
540+
await act(async () => {
541+
fireEvent.press(logoutButton);
542+
});
543+
544+
expect(mockLogoutFromProvider).toHaveBeenCalledWith(false);
545+
expect(mockNavigate).toHaveBeenCalledWith(
546+
Routes.DEPOSIT.ENTER_EMAIL,
547+
undefined,
548+
);
549+
});
550+
551+
it('handles logout error gracefully', async () => {
552+
const mockLoggerError = jest.spyOn(Logger, 'error');
553+
const logoutError = new Error('Logout failed');
554+
mockLogoutFromProvider.mockRejectedValueOnce(logoutError);
555+
556+
const error2020 = Object.assign(new Error('API Error'), {
557+
error: {
558+
errorCode: 2020,
559+
message:
560+
'This phone number is already registered. It has been used by an account created with d***@example.com. Login with this email to continue.',
561+
},
562+
});
563+
mockPostKycForm.mockRejectedValueOnce(error2020);
564+
565+
render(BasicInfo);
566+
567+
fireEvent.changeText(screen.getByTestId('first-name-input'), 'John');
568+
fireEvent.changeText(screen.getByTestId('last-name-input'), 'Smith');
569+
fireEvent.changeText(
570+
screen.getByTestId('deposit-phone-field-test-id'),
571+
'234567890',
572+
);
573+
fireEvent.changeText(
574+
screen.getByTestId('date-of-birth-input'),
575+
'01/01/1990',
576+
);
577+
fireEvent.changeText(screen.getByTestId('ssn-input'), '123456789');
578+
579+
await act(async () => {
580+
fireEvent.press(screen.getByTestId('continue-button'));
581+
});
582+
583+
// Wait for error and logout button to appear
584+
const logoutButton = await screen.findByTestId(
585+
'basic-info-logout-button',
586+
);
587+
588+
await act(async () => {
589+
fireEvent.press(logoutButton);
590+
});
591+
592+
expect(mockLogoutFromProvider).toHaveBeenCalled();
593+
expect(mockNavigate).not.toHaveBeenCalled();
594+
expect(mockLoggerError).toHaveBeenCalledWith(
595+
logoutError,
596+
'Error logging out from BasicInfo error banner',
597+
);
598+
599+
// Error message stays visible
600+
await screen.findByText(
601+
'This phone number is already in use by d***@example.com. Log in using this email to continue.',
602+
);
603+
604+
mockLoggerError.mockRestore();
605+
});
606+
});
376607
});

app/components/UI/Ramp/Deposit/Views/BasicInfo/BasicInfo.tsx

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
createEnterAddressNavDetails,
2626
} from '../EnterAddress/EnterAddress';
2727
import { createSsnInfoModalNavigationDetails } from '../Modals/SsnInfoModal';
28+
import { createEnterEmailNavDetails } from '../EnterEmail/EnterEmail';
2829
import { BuyQuote } from '@consensys/native-ramps-sdk';
2930
import { useDepositSDK } from '../../sdk';
3031
import { VALIDATION_REGEX } from '../../constants/constants';
@@ -70,9 +71,10 @@ const BasicInfo = (): JSX.Element => {
7071
const { styles, theme } = useStyles(styleSheet, {});
7172
const trackEvent = useAnalytics();
7273
const { quote, previousFormData } = useParams<BasicInfoParams>();
73-
const { selectedRegion } = useDepositSDK();
74+
const { selectedRegion, logoutFromProvider } = useDepositSDK();
7475
const [loading, setLoading] = useState(false);
7576
const [error, setError] = useState<string | null>(null);
77+
const [isPhoneRegisteredError, setIsPhoneRegisteredError] = useState(false);
7678

7779
const firstNameInputRef = useRef<TextInput>(null);
7880
const lastNameInputRef = useRef<TextInput>(null);
@@ -197,6 +199,7 @@ const BasicInfo = (): JSX.Element => {
197199

198200
// Clear any previous errors when retrying
199201
setError(null);
202+
setIsPhoneRegisteredError(false);
200203

201204
trackEvent('RAMPS_BASIC_INFO_ENTERED', {
202205
region: selectedRegion?.isoCode || '',
@@ -227,11 +230,34 @@ const BasicInfo = (): JSX.Element => {
227230
}),
228231
);
229232
} catch (submissionError) {
230-
setError(
231-
submissionError instanceof Error && submissionError.message
232-
? submissionError.message
233-
: strings('deposit.basic_info.unexpected_error'),
234-
);
233+
// Check for Transak error code 2020 (phone already registered)
234+
// API returns: { error: { errorCode: 2020, message: "..." } }
235+
const errorWithCode = submissionError as unknown as {
236+
error?: { errorCode?: number; message?: string };
237+
};
238+
const isPhoneError = errorWithCode?.error?.errorCode === 2020;
239+
240+
setIsPhoneRegisteredError(isPhoneError);
241+
242+
// For error code 2020, extract email from message and format it
243+
let errorMessage = '';
244+
if (isPhoneError && errorWithCode?.error?.message) {
245+
// Extract email from message like "...created with k****@pedalsup.com..."
246+
const emailMatch = errorWithCode.error.message.match(
247+
/[\w*]+@[\w*]+(?:\.[\w*]+)*/,
248+
);
249+
const email = emailMatch ? emailMatch[0] : '';
250+
errorMessage = email
251+
? strings('deposit.basic_info.phone_already_registered', { email })
252+
: errorWithCode.error.message;
253+
} else {
254+
errorMessage =
255+
submissionError instanceof Error && submissionError.message
256+
? submissionError.message
257+
: strings('deposit.basic_info.unexpected_error');
258+
}
259+
260+
setError(errorMessage);
235261
Logger.error(
236262
submissionError as Error,
237263
'Unexpected error during basic info form submission',
@@ -240,17 +266,32 @@ const BasicInfo = (): JSX.Element => {
240266
setLoading(false);
241267
}
242268
}, [
243-
previousFormData,
244269
validateFormData,
245270
formData,
246271
postKycForm,
247272
submitSsnDetails,
248273
navigation,
249274
quote,
275+
previousFormData,
250276
selectedRegion?.isoCode,
251277
trackEvent,
252278
]);
253279

280+
const handleLogout = useCallback(async () => {
281+
try {
282+
await logoutFromProvider(false); // false = no server invalidation needed
283+
284+
// Navigate back to email entry screen
285+
navigation.navigate(...createEnterEmailNavDetails());
286+
} catch (logoutError) {
287+
Logger.error(
288+
logoutError as Error,
289+
'Error logging out from BasicInfo error banner',
290+
);
291+
// Keep error visible if logout fails
292+
}
293+
}, [logoutFromProvider, navigation]);
294+
254295
const focusNextField = useCallback(
255296
(nextRef: React.RefObject<TextInput>) => () => {
256297
nextRef.current?.focus();
@@ -262,6 +303,7 @@ const BasicInfo = (): JSX.Element => {
262303
(field: keyof BasicInfoFormData, nextAction?: () => void) =>
263304
(value: string) => {
264305
setError(null);
306+
setIsPhoneRegisteredError(false);
265307
const currentValue = formData[field] || '';
266308
const isAutofill = value.length - currentValue.length > 1;
267309

@@ -298,6 +340,17 @@ const BasicInfo = (): JSX.Element => {
298340
<BannerAlert
299341
description={error}
300342
severity={BannerAlertSeverity.Error}
343+
actionButtonProps={
344+
isPhoneRegisteredError
345+
? {
346+
variant: ButtonVariants.Link,
347+
label: strings('deposit.basic_info.login_with_email'),
348+
onPress: handleLogout,
349+
labelTextVariant: TextVariant.BodyMD,
350+
testID: 'basic-info-logout-button',
351+
}
352+
: undefined
353+
}
301354
/>
302355
</View>
303356
)}

locales/languages/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,9 @@
776776
"dob_required": "Date of birth is required",
777777
"dob_invalid": "Please enter a valid date of birth",
778778
"ssn_required": "Social security number is required",
779-
"unexpected_error": "An unexpected error occurred. Please try again."
779+
"unexpected_error": "An unexpected error occurred. Please try again.",
780+
"phone_already_registered": "This phone number is already in use by {{email}}. Log in using this email to continue.",
781+
"login_with_email": "Log in with email"
780782
},
781783
"enter_address": {
782784
"navbar_title": "Verify your identity",

0 commit comments

Comments
 (0)