Skip to content

Commit b3e0a64

Browse files
authored
fix(card): UI issues on Authentication/Delegation (MetaMask#22352)
<!-- 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** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> This PR fixes several issues affecting the Card feature to improve stability and UI consistency: - Fixed negative allowance values displayed on CardHome - Fixed token priority not updating correctly after delegation - Fixed text clipping in the Asset BottomSheet - Ensured all addresses are properly checksummed - Fixed BottomSheet layering issue where it appeared below the header ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Negative allowance values displayed on CardHome CHANGELOG entry: Token priority not updating after delegation CHANGELOG entry: Text clipping in Asset BottomSheet CHANGELOG entry: Missing address checksum formatting CHANGELOG entry: BottomSheet not appearing above header ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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] > Moves add-funds and asset-selection to dedicated modal routes, adds a spending limit progress bar with latest-allowance fetching, improves OTP error UX, and introduces a reusable priority-update hook. > > - **Navigation/Architecture**: > - Add `CardModals` stack with `CardAddFundsModal` and `CardAssetSelectionModal`; migrate CardHome/SpendingLimit to navigate to these modals. > - **CardHome**: > - Replace inline bottom sheets with modal navigation for Add Funds and Asset Selection. > - Add Spending Limit progress bar (with skeleton) when `AllowanceState.Limited`; hide for unsupported tokens (e.g., `aUSDC`). > - Gate "close to limit" warning by token support; minor style additions. > - **SpendingLimit**: > - Open Asset Selection as a modal (selection-only flow returning via `callerRoute`). > - Block back navigation while delegating; update token priority after successful delegation; clear cache when needed. > - **Bottom Sheets**: > - Refactor `AddFundsBottomSheet` and `AssetSelectionBottomSheet` to modal components using `navUtils` params; show balances, filter by network/location; close-on-navigate behavior. > - **Hooks**: > - New `useUpdateTokenPriority` to reorder wallet priorities and update Redux/cache. > - New `useGetLatestAllowanceForPriorityToken` to read latest approval from logs; integrated in `useLoadCardData` to populate `totalAllowance`. > - `useAssetBalances`: improved fiat fallbacks and proportional/zero handling. > - `useGetCardExternalWalletDetails`: stop bulk `totalAllowance` fetch; map details without it. > - **SDK**: > - Implement `getLatestAllowanceFromLogs` (ethers log scan); make some helpers sync; include tenant ID in `createOnboardingConsent` using API key. > - **Auth UX**: > - OTP error shown below fields; clear on input; tests updated. > - **Utils**: > - `truncateAddress` now checksums hex addresses. > - **Tests**: > - Extensive updates/new tests across views, hooks, SDK, and components. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 327555d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 0954f71 commit b3e0a64

40 files changed

Lines changed: 3273 additions & 1018 deletions

app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { fireEvent, screen, waitFor } from '@testing-library/react-native';
2+
import { TextInput } from 'react-native';
23
import { renderScreen } from '../../../../../util/test/renderWithProvider';
34
import CardAuthentication from './CardAuthentication';
45
import Routes from '../../../../../constants/navigation/Routes';
@@ -973,6 +974,50 @@ describe('CardAuthentication Component', () => {
973974
});
974975
});
975976

977+
it('displays error below OTP input fields', async () => {
978+
mockLogin.mockResolvedValue({
979+
isOtpRequired: true,
980+
userId: 'user-123',
981+
phoneNumber: '+1 (555) 123-****',
982+
});
983+
984+
render();
985+
const emailInput = screen.getByPlaceholderText(
986+
'Enter your email address',
987+
);
988+
const passwordInput = screen.getByPlaceholderText('Enter your password');
989+
const loginButton = screen.getByTestId(
990+
CardAuthenticationSelectors.VERIFY_ACCOUNT_BUTTON,
991+
);
992+
993+
fireEvent.changeText(emailInput, 'test@example.com');
994+
fireEvent.changeText(passwordInput, 'password123');
995+
fireEvent.press(loginButton);
996+
997+
await waitFor(() => {
998+
expect(
999+
screen.getByText('Enter your verification code'),
1000+
).toBeOnTheScreen();
1001+
});
1002+
1003+
mockUseCardProviderAuthentication.mockReturnValue({
1004+
login: mockLogin,
1005+
loading: false,
1006+
error: null,
1007+
clearError: mockClearError,
1008+
sendOtpLogin: mockSendOtpLogin,
1009+
otpLoading: false,
1010+
otpError: 'The code you entered is incorrect',
1011+
clearOtpError: mockClearOtpError,
1012+
});
1013+
1014+
await waitFor(() => {
1015+
const errorText = screen.getByText('The code you entered is incorrect');
1016+
expect(errorText).toBeOnTheScreen();
1017+
expect(screen.getByText('Verification Code')).toBeOnTheScreen();
1018+
});
1019+
});
1020+
9761021
it('does not display OTP error box when no error exists in OTP step', async () => {
9771022
mockLogin.mockResolvedValue({
9781023
isOtpRequired: true,
@@ -1002,6 +1047,66 @@ describe('CardAuthentication Component', () => {
10021047
screen.queryByText('Invalid verification code'),
10031048
).not.toBeOnTheScreen();
10041049
});
1050+
1051+
it('calls clearOtpError when user types in OTP field', async () => {
1052+
mockLogin.mockResolvedValue({
1053+
isOtpRequired: true,
1054+
userId: 'user-123',
1055+
});
1056+
1057+
render();
1058+
const emailInput = screen.getByPlaceholderText(
1059+
'Enter your email address',
1060+
);
1061+
const passwordInput = screen.getByPlaceholderText('Enter your password');
1062+
const loginButton = screen.getByTestId(
1063+
CardAuthenticationSelectors.VERIFY_ACCOUNT_BUTTON,
1064+
);
1065+
1066+
fireEvent.changeText(emailInput, 'test@example.com');
1067+
fireEvent.changeText(passwordInput, 'password123');
1068+
fireEvent.press(loginButton);
1069+
1070+
await waitFor(() => {
1071+
expect(
1072+
screen.getByText('Enter your verification code'),
1073+
).toBeOnTheScreen();
1074+
});
1075+
1076+
// Update mock to include OTP error after entering OTP step
1077+
mockUseCardProviderAuthentication.mockReturnValue({
1078+
login: mockLogin,
1079+
loading: false,
1080+
error: null,
1081+
clearError: mockClearError,
1082+
sendOtpLogin: mockSendOtpLogin,
1083+
otpLoading: false,
1084+
otpError: 'Invalid verification code',
1085+
clearOtpError: mockClearOtpError,
1086+
});
1087+
1088+
// Wait for error to appear
1089+
await waitFor(() => {
1090+
expect(screen.getByText('Invalid verification code')).toBeOnTheScreen();
1091+
});
1092+
1093+
// Clear the mock to track new calls
1094+
mockClearOtpError.mockClear();
1095+
1096+
// Find the OTP input (CodeField renders as TextInput with number-pad keyboard)
1097+
const allInputs = screen.UNSAFE_queryAllByType(TextInput);
1098+
const otpInput = allInputs.find(
1099+
(input) => input.props.keyboardType === 'number-pad',
1100+
);
1101+
1102+
expect(otpInput).toBeDefined();
1103+
1104+
if (otpInput) {
1105+
fireEvent.changeText(otpInput, '1');
1106+
1107+
expect(mockClearOtpError).toHaveBeenCalled();
1108+
}
1109+
});
10051110
});
10061111

10071112
describe('OTP Step - Loading States', () => {

app/components/UI/Card/Views/CardAuthentication/CardAuthentication.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,16 @@ const CardAuthentication = () => {
388388
)}
389389
/>
390390
</Box>
391+
{otpError && (
392+
<Box style={styles.errorBox}>
393+
<Text
394+
variant={TextVariant.BodySm}
395+
style={{ color: theme.colors.error.default }}
396+
>
397+
{otpError}
398+
</Text>
399+
</Box>
400+
)}
391401
<Box twClassName="mt-4 items-center">
392402
{resendCountdown > 0 ? (
393403
<Text
@@ -413,16 +423,6 @@ const CardAuthentication = () => {
413423
)}
414424
</Box>
415425
</Box>
416-
{otpError && (
417-
<Box style={styles.errorBox}>
418-
<Text
419-
variant={TextVariant.BodySm}
420-
style={{ color: theme.colors.error.default }}
421-
>
422-
{otpError}
423-
</Text>
424-
</Box>
425-
)}
426426
</Box>
427427
<Box twClassName="gap-2">
428428
<Button

app/components/UI/Card/Views/CardHome/CardHome.styles.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const createStyles = (theme: Theme) =>
4040
defaultHorizontalPadding: {
4141
paddingHorizontal: 16,
4242
},
43+
defaultMarginBottom: {
44+
marginBottom: 16,
45+
},
4346
cardBalanceContainer: {
4447
marginTop: 16,
4548
backgroundColor: theme.colors.background.muted,

0 commit comments

Comments
 (0)