diff --git a/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx b/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx
index 74f3a3418c5e..398acddd68d6 100644
--- a/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx
+++ b/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx
@@ -1,4 +1,5 @@
import { fireEvent, screen, waitFor } from '@testing-library/react-native';
+import { TextInput } from 'react-native';
import { renderScreen } from '../../../../../util/test/renderWithProvider';
import CardAuthentication from './CardAuthentication';
import Routes from '../../../../../constants/navigation/Routes';
@@ -973,6 +974,50 @@ describe('CardAuthentication Component', () => {
});
});
+ it('displays error below OTP input fields', async () => {
+ mockLogin.mockResolvedValue({
+ isOtpRequired: true,
+ userId: 'user-123',
+ phoneNumber: '+1 (555) 123-****',
+ });
+
+ render();
+ const emailInput = screen.getByPlaceholderText(
+ 'Enter your email address',
+ );
+ const passwordInput = screen.getByPlaceholderText('Enter your password');
+ const loginButton = screen.getByTestId(
+ CardAuthenticationSelectors.VERIFY_ACCOUNT_BUTTON,
+ );
+
+ fireEvent.changeText(emailInput, 'test@example.com');
+ fireEvent.changeText(passwordInput, 'password123');
+ fireEvent.press(loginButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Enter your verification code'),
+ ).toBeOnTheScreen();
+ });
+
+ mockUseCardProviderAuthentication.mockReturnValue({
+ login: mockLogin,
+ loading: false,
+ error: null,
+ clearError: mockClearError,
+ sendOtpLogin: mockSendOtpLogin,
+ otpLoading: false,
+ otpError: 'The code you entered is incorrect',
+ clearOtpError: mockClearOtpError,
+ });
+
+ await waitFor(() => {
+ const errorText = screen.getByText('The code you entered is incorrect');
+ expect(errorText).toBeOnTheScreen();
+ expect(screen.getByText('Verification Code')).toBeOnTheScreen();
+ });
+ });
+
it('does not display OTP error box when no error exists in OTP step', async () => {
mockLogin.mockResolvedValue({
isOtpRequired: true,
@@ -1002,6 +1047,66 @@ describe('CardAuthentication Component', () => {
screen.queryByText('Invalid verification code'),
).not.toBeOnTheScreen();
});
+
+ it('calls clearOtpError when user types in OTP field', async () => {
+ mockLogin.mockResolvedValue({
+ isOtpRequired: true,
+ userId: 'user-123',
+ });
+
+ render();
+ const emailInput = screen.getByPlaceholderText(
+ 'Enter your email address',
+ );
+ const passwordInput = screen.getByPlaceholderText('Enter your password');
+ const loginButton = screen.getByTestId(
+ CardAuthenticationSelectors.VERIFY_ACCOUNT_BUTTON,
+ );
+
+ fireEvent.changeText(emailInput, 'test@example.com');
+ fireEvent.changeText(passwordInput, 'password123');
+ fireEvent.press(loginButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Enter your verification code'),
+ ).toBeOnTheScreen();
+ });
+
+ // Update mock to include OTP error after entering OTP step
+ mockUseCardProviderAuthentication.mockReturnValue({
+ login: mockLogin,
+ loading: false,
+ error: null,
+ clearError: mockClearError,
+ sendOtpLogin: mockSendOtpLogin,
+ otpLoading: false,
+ otpError: 'Invalid verification code',
+ clearOtpError: mockClearOtpError,
+ });
+
+ // Wait for error to appear
+ await waitFor(() => {
+ expect(screen.getByText('Invalid verification code')).toBeOnTheScreen();
+ });
+
+ // Clear the mock to track new calls
+ mockClearOtpError.mockClear();
+
+ // Find the OTP input (CodeField renders as TextInput with number-pad keyboard)
+ const allInputs = screen.UNSAFE_queryAllByType(TextInput);
+ const otpInput = allInputs.find(
+ (input) => input.props.keyboardType === 'number-pad',
+ );
+
+ expect(otpInput).toBeDefined();
+
+ if (otpInput) {
+ fireEvent.changeText(otpInput, '1');
+
+ expect(mockClearOtpError).toHaveBeenCalled();
+ }
+ });
});
describe('OTP Step - Loading States', () => {
diff --git a/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.tsx b/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.tsx
index 1cda96795538..c56c605c9344 100644
--- a/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.tsx
+++ b/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.tsx
@@ -388,6 +388,16 @@ const CardAuthentication = () => {
)}
/>
+ {otpError && (
+
+
+ {otpError}
+
+
+ )}
{resendCountdown > 0 ? (
{
)}
- {otpError && (
-
-
- {otpError}
-
-
- )}
);
}
diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts
index fd1148c35951..d8b089dfc4e6 100644
--- a/app/constants/navigation/Routes.ts
+++ b/app/constants/navigation/Routes.ts
@@ -382,6 +382,11 @@ const Routes = {
KYC_FAILED: 'CardOnboardingKYCFailed',
WEBVIEW: 'CardOnboardingWebview',
},
+ MODALS: {
+ ID: 'CardModals',
+ ADD_FUNDS: 'CardAddFundsModal',
+ ASSET_SELECTION: 'CardAssetSelectionModal',
+ },
},
SEND: {
RECIPIENT: 'Recipient',
diff --git a/e2e/selectors/Card/CardHome.selectors.ts b/e2e/selectors/Card/CardHome.selectors.ts
index 9c81e3ecebc5..b7672f94594a 100644
--- a/e2e/selectors/Card/CardHome.selectors.ts
+++ b/e2e/selectors/Card/CardHome.selectors.ts
@@ -15,4 +15,5 @@ export const CardHomeSelectors = {
ADD_FUNDS_BOTTOM_SHEET_DEPOSIT_OPTION:
'add-funds-bottom-sheet-deposit-option',
ADD_FUNDS_BOTTOM_SHEET_SWAP_OPTION: 'add-funds-bottom-sheet-swap-option',
+ SPENDING_LIMIT_PROGRESS_BAR_SKELETON: 'spending-limit-progress-bar-skeleton',
};
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 0228c29f6938..01e2d584adb9 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -1792,6 +1792,7 @@
"outcomes_plural": "outcomes",
"outcome_winner": "Winner",
"outcome_loser": "Loser",
+ "outcome_draw": "Draw",
"resolved_outcomes": "Resolved outcomes",
"category": {
"trending": "Trending",