Skip to content

Commit 5dfa0b8

Browse files
fix: surface api errors and improve resend UX on OTP screen (MetaMask#26727)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** The OTP screen was showing a generic fallback error regardless of what the API returned. This PR surfaces the actual Transak error messages to the user on both OTP submission and resend failures. The resend button state was also changed: instead of starting in the "resend" state (waiting for the user to trigger a resend), the screen now starts directly in cooldown mode because the OTP was already sent when the user lands on this screen. The cooldown is also bumped from 30s to 60s to match the actual resend window. ## **Changelog** CHANGELOG entry: Fixed OTP error messages to show the actual error from the server instead of a generic fallback. ## **Related issues** Fixes: TRAM-3291 ## **Manual testing steps** ```gherkin Feature: OTP error display Scenario: user submits an invalid OTP code Given the user is on the OTP screen after starting a buy flow When the user enters a wrong 6-digit code Then the error message from the server is displayed instead of a generic one Scenario: user lands on the OTP screen Given the user navigated to the OTP screen Then a 60-second cooldown is immediately shown And the resend button is not visible until the cooldown ends Scenario: user tries to resend after cooldown Given the 60-second cooldown has elapsed When the user taps the resend button Then a new OTP is sent And a new 60-second cooldown starts ``` ## **Screenshots/Recordings** ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> Surface error without reseting the countdown: https://github.com/user-attachments/assets/d09b03d5-44f1-4448-8805-e8d7c18f43b1 Countdown starts as soon as user navigates to OTP screen: https://github.com/user-attachments/assets/56016104-ad20-46d0-b7fb-88e88e066507 ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes OTP resend timing/state handling and error surfacing on a critical ramp authentication step; mistakes could prevent users from resending or understanding failures. > > **Overview** > **Improves OTP resend UX in the ramp native flow.** The OTP screen now starts in a *60s cooldown* (was 30s) since an OTP is already sent on entry, and the initial UI/snapshot reflects the cooldown text instead of an immediate resend link. > > **Resend failures now show user-facing API errors.** `handleResend` no longer transitions to a `resendError`/"contact support" UI state on API failure; it sets the screen `error` using `parseUserFacingError` with a fallback translation key, and tests add coverage for both message and no-message resend failures. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a6a1749. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: AxelGes <axelges9@gmail.com>
1 parent cb287f7 commit 5dfa0b8

3 files changed

Lines changed: 59 additions & 32 deletions

File tree

app/components/UI/Ramp/Views/NativeFlow/OtpCode.test.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,12 @@ describe('V2OtpCode', () => {
301301

302302
const { getByText } = renderWithTheme(<V2OtpCode />);
303303

304+
for (let i = 0; i < 60; i++) {
305+
act(() => {
306+
jest.advanceTimersByTime(1000);
307+
});
308+
}
309+
304310
await act(async () => {
305311
fireEvent.press(getByText('deposit.otp_code.resend_code_button'));
306312
});
@@ -315,6 +321,12 @@ describe('V2OtpCode', () => {
315321

316322
const { getByText } = renderWithTheme(<V2OtpCode />);
317323

324+
for (let i = 0; i < 60; i++) {
325+
act(() => {
326+
jest.advanceTimersByTime(1000);
327+
});
328+
}
329+
318330
await act(async () => {
319331
fireEvent.press(getByText('deposit.otp_code.resend_code_button'));
320332
});
@@ -370,4 +382,44 @@ describe('V2OtpCode', () => {
370382
expect(Clipboard.getString).toHaveBeenCalled();
371383
});
372384
});
385+
386+
it('shows error when sendUserOtp API fails', async () => {
387+
mockSendUserOtp.mockRejectedValue(new Error('Network error'));
388+
389+
const { getByText } = renderWithTheme(<V2OtpCode />);
390+
391+
for (let i = 0; i < 60; i++) {
392+
act(() => {
393+
jest.advanceTimersByTime(1000);
394+
});
395+
}
396+
397+
await act(async () => {
398+
fireEvent.press(getByText('deposit.otp_code.resend_code_button'));
399+
});
400+
401+
await waitFor(() => {
402+
expect(getByText('Network error')).toBeOnTheScreen();
403+
});
404+
});
405+
406+
it('shows fallback error when sendUserOtp fails without message', async () => {
407+
mockSendUserOtp.mockRejectedValue(new Error());
408+
409+
const { getByText } = renderWithTheme(<V2OtpCode />);
410+
411+
for (let i = 0; i < 60; i++) {
412+
act(() => {
413+
jest.advanceTimersByTime(1000);
414+
});
415+
}
416+
417+
await act(async () => {
418+
fireEvent.press(getByText('deposit.otp_code.resend_code_button'));
419+
});
420+
421+
await waitFor(() => {
422+
expect(getByText('deposit.otp_code.resend_code_error')).toBeOnTheScreen();
423+
});
424+
});
373425
});

app/components/UI/Ramp/Views/NativeFlow/OtpCode.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const createV2OtpCodeNavDetails =
5454
createNavigationDetails<V2OtpCodeParams>(Routes.RAMP.OTP_CODE);
5555

5656
const CELL_COUNT = 6;
57-
const COOLDOWN_TIME = 30;
57+
const COOLDOWN_TIME = 60;
5858
const MAX_RESET_ATTEMPTS = 3;
5959

6060
const ResendButton: FC<{
@@ -99,8 +99,8 @@ const V2OtpCode = () => {
9999
const [error, setError] = useState<string | null>(null);
100100
const [isLoading, setIsLoading] = useState(false);
101101
const [resendButtonState, setResendButtonState] = useState<
102-
'resend' | 'cooldown' | 'contactSupport' | 'resendError'
103-
>('resend');
102+
'resend' | 'cooldown' | 'contactSupport'
103+
>('cooldown');
104104
const [cooldownSeconds, setCooldownSeconds] = useState(COOLDOWN_TIME);
105105
const timerRef = useRef<NodeJS.Timeout | null>(null);
106106
const [resetAttemptCount, setResetAttemptCount] = useState(0);
@@ -191,7 +191,9 @@ const V2OtpCode = () => {
191191
.build(),
192192
);
193193
} catch (e) {
194-
setResendButtonState('resendError');
194+
setError(
195+
parseUserFacingError(e, strings('deposit.otp_code.resend_code_error')),
196+
);
195197
Logger.error(e as Error, 'Error resending OTP code');
196198
}
197199
}, [
@@ -396,13 +398,6 @@ const V2OtpCode = () => {
396398
button="deposit.otp_code.contact_support"
397399
/>
398400
) : null}
399-
{resendButtonState === 'resendError' ? (
400-
<ResendButton
401-
onPress={handleContactSupport}
402-
text="deposit.otp_code.resend_code_error"
403-
button="deposit.otp_code.contact_support"
404-
/>
405-
) : null}
406401
</Row>
407402
</ScreenLayout.Content>
408403
</ScreenLayout.Body>

app/components/UI/Ramp/Views/NativeFlow/__snapshots__/OtpCode.test.tsx.snap

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -429,28 +429,8 @@ exports[`V2OtpCode matches snapshot 1`] = `
429429
}
430430
}
431431
>
432-
deposit.otp_code.resend_code_description
432+
deposit.otp_code.resend_cooldown
433433
</Text>
434-
<TouchableOpacity
435-
onPress={[Function]}
436-
>
437-
<Text
438-
accessibilityRole="text"
439-
style={
440-
{
441-
"color": "#66676a",
442-
"fontFamily": "Geist-Regular",
443-
"fontSize": 16,
444-
"letterSpacing": 0,
445-
"lineHeight": 24,
446-
"marginLeft": 4,
447-
"textDecorationLine": "underline",
448-
}
449-
}
450-
>
451-
deposit.otp_code.resend_code_button
452-
</Text>
453-
</TouchableOpacity>
454434
</View>
455435
</View>
456436
</View>

0 commit comments

Comments
 (0)