From b3e0a645b1b9c60f26f24bd3c3bd5cff97fffe16 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Sat, 8 Nov 2025 09:57:59 -0300 Subject: [PATCH 1/2] fix(card): UI issues on Authentication/Delegation (#22352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** 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** 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** ### **Before** ### **After** ## **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. --- > [!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. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 327555d1ab1b771399e2a55209de5874d93aec59. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../CardAuthentication.test.tsx | 105 ++++ .../CardAuthentication/CardAuthentication.tsx | 20 +- .../UI/Card/Views/CardHome/CardHome.styles.ts | 3 + .../UI/Card/Views/CardHome/CardHome.test.tsx | 242 ++++++++- .../UI/Card/Views/CardHome/CardHome.tsx | 88 ++-- .../SpendingLimit/SpendingLimit.test.tsx | 289 ++++++++++- .../Views/SpendingLimit/SpendingLimit.tsx | 98 +++- .../AddFundsBottomSheet.test.tsx | 116 ++--- .../AddFundsBottomSheet.tsx | 45 +- .../components/AddFundsBottomSheet/index.ts | 5 +- .../AssetSelectionBottomSheet.styles.ts | 102 ---- .../AssetSelectionBottomSheet.test.tsx | 485 +++++++----------- .../AssetSelectionBottomSheet.tsx | 201 +++----- .../AssetSelectionBottomSheet/index.ts | 4 + .../SpendingLimitProgressBar.styles.ts | 3 + .../SpendingLimitProgressBar.test.tsx | 14 + .../SpendingLimitProgressBar.tsx | 18 + app/components/UI/Card/constants.ts | 8 + .../UI/Card/hooks/useAssetBalances.test.ts | 10 +- .../UI/Card/hooks/useAssetBalances.tsx | 79 ++- .../UI/Card/hooks/useCardDelegation.test.ts | 105 +++- .../UI/Card/hooks/useCardDelegation.ts | 12 + .../useGetCardExternalWalletDetails.test.ts | 139 ----- .../hooks/useGetCardExternalWalletDetails.ts | 37 +- ...GetLatestAllowanceForPriorityToken.test.ts | 448 ++++++++++++++++ .../useGetLatestAllowanceForPriorityToken.ts | 120 +++++ .../UI/Card/hooks/useLoadCardData.test.ts | 186 ++++++- .../UI/Card/hooks/useLoadCardData.ts | 31 +- .../Card/hooks/useRegisterUserConsent.test.ts | 2 - .../UI/Card/hooks/useRegisterUserConsent.ts | 1 - .../Card/hooks/useUpdateTokenPriority.test.ts | 378 ++++++++++++++ .../UI/Card/hooks/useUpdateTokenPriority.ts | 139 +++++ app/components/UI/Card/routes/index.tsx | 42 +- app/components/UI/Card/sdk/CardSDK.test.ts | 393 +++++++++++++- app/components/UI/Card/sdk/CardSDK.ts | 143 +++--- app/components/UI/Card/types.ts | 2 +- .../UI/Card/util/truncateAddress.test.ts | 161 ++++-- .../UI/Card/util/truncateAddress.ts | 11 +- app/constants/navigation/Routes.ts | 5 + e2e/selectors/Card/CardHome.selectors.ts | 1 + 40 files changed, 3273 insertions(+), 1018 deletions(-) delete mode 100644 app/components/UI/Card/components/AssetSelectionBottomSheet/AssetSelectionBottomSheet.styles.ts create mode 100644 app/components/UI/Card/components/AssetSelectionBottomSheet/index.ts create mode 100644 app/components/UI/Card/hooks/useGetLatestAllowanceForPriorityToken.test.ts create mode 100644 app/components/UI/Card/hooks/useGetLatestAllowanceForPriorityToken.ts create mode 100644 app/components/UI/Card/hooks/useUpdateTokenPriority.test.ts create mode 100644 app/components/UI/Card/hooks/useUpdateTokenPriority.ts diff --git a/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx b/app/components/UI/Card/Views/CardAuthentication/CardAuthentication.test.tsx index 74f3a3418c5..398acddd68d 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 1cda9679553..c56c605c934 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} - - - )}