From 007a9ceaa715b8beb7420b38fc01734d431a73bc Mon Sep 17 00:00:00 2001
From: abretonc7s <107169956+abretonc7s@users.noreply.github.com>
Date: Wed, 5 Nov 2025 15:41:50 +0800
Subject: [PATCH 1/4] fix(perps): invalid decimals in pnl card (#22174)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Fixed incorrect decimal precision for Entry price display on the PnL
card and transaction details.
**Problem:** Entry prices were using `PRICE_RANGES_MINIMAL_VIEW` (2
decimal places, fiat-style) instead of `PRICE_RANGES_UNIVERSAL` (5
significant digits, up to 6 decimals) that Mark prices use. This caused:
- PUMP: Displayed "<$0.01" instead of actual value
- ETH: Displayed "xxxx.60" instead of "xxxx.6" (trailing zero)
- XRP: Displayed "$2.26" instead of "$2.2626" (insufficient precision)
**Solution:** Changed Entry price formatting to use
`PRICE_RANGES_UNIVERSAL` to match Mark price decimal logic, ensuring
consistent precision across all price displays.
## **Changelog**
CHANGELOG entry: Fixed Entry price decimal precision on PnL card to
match Mark price formatting
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/TAT-1976
## **Manual testing steps**
```gherkin
Feature: Entry price decimal consistency
Scenario: user views PnL card for low-value asset (PUMP)
Given user has an open position in PUMP
When user navigates to the PnL hero card (Share your trade view)
Then Entry price should display actual value with appropriate decimals
And Entry price should match Mark price decimal formatting
Scenario: user views PnL card for ETH
Given user has an open position in ETH
When user navigates to the PnL hero card
Then Entry price should display "xxxx.6" without trailing zeros
And Entry price should match Mark price decimal formatting
Scenario: user views PnL card for XRP
Given user has an open position in XRP
When user navigates to the PnL hero card
Then Entry price should display "$2.2626" with full precision
And Entry price should match Mark price decimal formatting
Scenario: user views transaction history
Given user has closed positions
When user views position transaction details
Then Entry/Close price should display with universal decimal precision
```
## **Screenshots/Recordings**
### **Before**
PUMP: Entry shows "<$0.01"
ETH: Entry shows "xxxx.60" (trailing zero)
XRP: Entry shows "$2.26" (insufficient precision)
### **After**
Entry prices now match Mark price formatting:
- PUMP: Shows actual value (e.g., "$0.003889")
- ETH: Shows "xxxx.6" (no trailing zero)
- XRP: Shows "$2.2626" (full precision)
https://github.com/user-attachments/assets/22528486-a079-41a3-abfa-1f344ef5dbb6
## **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 (existing tests pass)
- [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.
---
## **Technical Details**
### Files Modified:
1.
`app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx:235`
- Changed Entry price from `PRICE_RANGES_MINIMAL_VIEW` �
`PRICE_RANGES_UNIVERSAL`
2.
`app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx:107-109`
- Added `PRICE_RANGES_UNIVERSAL` config to Entry/Close price formatting
### Verification:
- ESLint: No errors
- Prettier: Formatting correct
- Unit tests: All tests pass (21 tests for PerpsHeroCardView, 2 tests
for PerpsPositionTransactionView)
### Reference Implementation:
- `PerpsPositionCard.tsx` already uses `PRICE_RANGES_UNIVERSAL` (lines
414-417)
- `PerpsTPSLView.tsx` already uses `PRICE_RANGES_UNIVERSAL` (lines
445-447)
- Consistent with `fix/perps/decimal-consistency` branch work
---
> [!NOTE]
> Switch Entry/Close price formatting to PRICE_RANGES_UNIVERSAL in PnL
hero card and transaction details for consistent precision.
>
> - **Perps UI**:
> - **PnL Hero Card (`PerpsHeroCardView.tsx`)**: Format `entryPrice`
with `formatPerpsFiat(..., { ranges: `PRICE_RANGES_UNIVERSAL` })`.
> - **Transaction Details (`PerpsPositionTransactionView.tsx`)**: Format
Entry/Close price with `PRICE_RANGES_UNIVERSAL` for consistent decimal
precision.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1a13254a364dab24096524eb4213383325d8affa. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx | 3 +--
.../PerpsTransactionsView/PerpsPositionTransactionView.tsx | 5 ++++-
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx
index f442eb268fd3..8f6c63ad4765 100644
--- a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx
+++ b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx
@@ -35,7 +35,6 @@ import RewardsReferralCodeTag from '../../../Rewards/components/RewardsReferralC
import {
formatPerpsFiat,
parseCurrencyString,
- PRICE_RANGES_MINIMAL_VIEW,
PRICE_RANGES_UNIVERSAL,
} from '../../utils/formatUtils';
import MetaMaskLogo from '../../../../../images/branding/metamask-name.png';
@@ -232,7 +231,7 @@ const PerpsHeroCardView: React.FC = () => {
variant={TextVariant.BodySMMedium}
>
{formatPerpsFiat(data.entryPrice, {
- ranges: PRICE_RANGES_MINIMAL_VIEW,
+ ranges: PRICE_RANGES_UNIVERSAL,
})}
diff --git a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx
index 83b03042643f..42f1dd396021 100644
--- a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx
+++ b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx
@@ -32,6 +32,7 @@ import {
import {
formatPerpsFiat,
formatTransactionDate,
+ PRICE_RANGES_UNIVERSAL,
} from '../../utils/formatUtils';
import { styleSheet } from './PerpsPositionTransactionView.styles';
@@ -103,7 +104,9 @@ const PerpsPositionTransactionView: React.FC = () => {
transaction.fill?.action === 'Closed'
? strings('perps.transactions.position.close_price')
: strings('perps.transactions.position.entry_price'),
- value: formatPerpsFiat(transaction.fill.entryPrice),
+ value: formatPerpsFiat(transaction.fill.entryPrice, {
+ ranges: PRICE_RANGES_UNIVERSAL,
+ }),
},
].filter(Boolean);
From 13f461cebb41ca7c97096cde44b07a46499e22c7 Mon Sep 17 00:00:00 2001
From: abretonc7s <107169956+abretonc7s@users.noreply.github.com>
Date: Wed, 5 Nov 2025 16:31:37 +0800
Subject: [PATCH 2/4] fix(perps): missing referral code inpnl card tat-1981
(#22175)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
This PR fixes the missing MetaMask referral code display in the Perps
PnL Hero Card (shareable card view).
### What is the reason for the change?
Users who are opted into MetaMask Rewards and have a referral code were
not seeing it displayed on the shareable PnL Hero Card when accessing
Perps directly, even though the UI design and display logic were already
implemented. The referral code would only appear if users had previously
visited the Rewards section of the app.
Issue: https://consensyssoftware.atlassian.net/browse/TAT-1981
### What is the improvement/solution?
The root cause was that the `useReferralDetails()` hook (which fetches
the user's referral code from the backend) was only being called in
Rewards components, not in the Perps Hero Card view. This meant:
- ✅ User visits Rewards first → code loads → shows on Hero Card
- ❌ User goes directly to Perps → code never fetched → missing from Hero
Card
**Fix implemented:**
- Added `useReferralDetails()` hook to `PerpsHeroCardView.tsx`
- The hook automatically fetches referral details when the Hero Card
opens
- Code gets stored in Redux and picked up by the existing UI (which was
already implemented at lines 262-281)
- No UI changes needed - the design with `RewardsReferralCodeTag`
component was already there
**Technical details:**
- Only 2 lines added (import + hook call)
- Uses existing `useReferralDetails` hook from Rewards
- Leverages existing `selectReferralCode` Redux selector
- Respects user opt-in status (only shows for opted-in users)
## **Changelog**
CHANGELOG entry: Fixed missing MetaMask referral code display in Perps
PnL shareable card for opted-in users
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/TAT-1981
## **Manual testing steps**
```gherkin
Feature: Display referral code on Perps PnL Hero Card
Scenario: User with referral code accesses PnL Hero Card directly
Given user is opted into MetaMask Rewards
And user has a referral code assigned (e.g., "MMCSI")
And user has NOT visited the Rewards section yet in this session
When user navigates to Perps tab
And user opens an existing position
And user taps the "Share" button to open PnL Hero Card
Then the referral code should appear in the top-right corner of the shareable card
And the referral code should display using the RewardsReferralCodeTag component
And the referral code should be included when sharing the card
Scenario: User without opt-in does not see referral code
Given user has NOT opted into MetaMask Rewards
And user does NOT have a referral code
When user navigates to Perps tab
And user opens an existing position
And user taps the "Share" button to open PnL Hero Card
Then no referral code should be displayed
And the card should render normally without the referral section
Scenario: Referral code persists across multiple card views
Given user is opted into MetaMask Rewards
And user has a referral code assigned
When user opens PnL Hero Card for position A
Then referral code should be displayed
When user goes back and opens PnL Hero Card for position B
Then referral code should still be displayed without refetching
```
## **Screenshots/Recordings**
### **Before**
Referral code missing from PnL Hero Card when accessing Perps directly
(only appeared if user visited Rewards section first).
### **After**
Referral code now displays correctly in top-right corner of PnL Hero
Card for all opted-in users, regardless of navigation path. The design
matches the provided mockup with referral code displayed as a tag (e.g.,
"MMCSI" or "BUDDHA" in the example).
https://github.com/user-attachments/assets/2bd2b1fe-faa0-4051-b1b0-41064455a7e3
## **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.
---
## Technical Implementation Details
### Files Modified
**Single file changed:**
-
`app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx`
- Added import: `useReferralDetails` hook from Rewards
- Added hook call: `useReferralDetails()` to fetch referral code on
component mount
### Code Changes
**Lines added: 2**
```typescript
// Import added at line 63
import { useReferralDetails } from '../../../Rewards/hooks/useReferralDetails';
// Hook call added at lines 91-92
// Fetch referral details to ensure code is available for display
useReferralDetails();
```
### Why This Works
1. **Existing Infrastructure**: The UI for displaying referral codes was
already implemented at lines 262-281 of the Hero Card
2. **Conditional Rendering**: Code already checks `rewardsReferralCode`
and only displays if present
3. **Redux Integration**: `useReferralDetails()` fetches data and stores
it via `setReferralDetails` action
4. **Automatic Updates**: The `useSelector(selectReferralCode)` hook
automatically re-renders when data arrives
5. **Opt-in Respect**: Only displays for users who have opted into
Rewards and have a referral code
### Data Flow
```
PerpsHeroCardView mounts
↓
useReferralDetails() called
↓
Fetches from RewardsController
↓
dispatch(setReferralDetails({ referralCode: "MMCSI" }))
↓
Redux state updated
↓
useSelector(selectReferralCode) re-renders
↓
RewardsReferralCodeTag component renders with code
```
### Performance Impact
- **Minimal**: Single API call on Hero Card mount (only if not already
cached)
- **Cached**: Data persists in Redux for the session
- **No extra renders**: Component already had the selector, just needed
the data
### Testing Verification
✅ ESLint passes - no linting errors
✅ TypeScript compilation - types are correct
✅ No breaking changes - purely additive
✅ Backward compatible - gracefully handles missing referral codes
---
> [!NOTE]
> Invoke `useReferralDetails()` in `PerpsHeroCardView` so referral code
loads and displays (and is used in share message); update tests to mock
the hook/design tokens.
>
> - **Perps PnL Hero Card (`PerpsHeroCardView.tsx`)**:
> - Fetch referral details on mount by calling `useReferralDetails()`
and importing the hook.
> - Leverages existing `selectReferralCode` usage to display the
referral tag and include code in share message.
> - **Tests (`PerpsHeroCardView.test.tsx`)**:
> - Mock `useReferralDetails` and add `brandColor` to design tokens
mock.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5f70c4002bcc09d46a208bc1d3fcd5dc6532d7da. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx | 7 +++++++
.../UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx | 4 ++++
2 files changed, 11 insertions(+)
diff --git a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx
index a5ff8d3193d2..315fad9e843f 100644
--- a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.test.tsx
@@ -88,7 +88,14 @@ jest.mock('../../../Rewards/utils', () => ({
(code) => `https://link.metamask.io/rewards?referral=${code}`,
),
}));
+jest.mock('../../../Rewards/hooks/useReferralDetails', () => ({
+ useReferralDetails: jest.fn(),
+}));
jest.mock('@metamask/design-tokens', () => ({
+ brandColor: {
+ black: '#000000',
+ white: '#FFFFFF',
+ },
darkTheme: {
colors: {
background: {
diff --git a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx
index 8f6c63ad4765..7dde0afb2d9b 100644
--- a/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx
+++ b/app/components/UI/Perps/Views/PerpsHeroCardView/PerpsHeroCardView.tsx
@@ -59,6 +59,7 @@ import {
PerpsHeroCardViewSelectorsIDs,
getPerpsHeroCardViewSelector,
} from '../../../../../../e2e/selectors/Perps/Perps.selectors';
+import { useReferralDetails } from '../../../Rewards/hooks/useReferralDetails';
// To add a new card, add the image to the array.
const CARD_IMAGES: { image: ImageSourcePropType; id: number; name: string }[] =
@@ -86,6 +87,9 @@ const PerpsHeroCardView: React.FC = () => {
const rewardsReferralCode = useSelector(selectReferralCode);
+ // Fetch referral details to ensure code is available for display
+ useReferralDetails();
+
const { track } = usePerpsEventTracking();
const { showToast, PerpsToastOptions } = usePerpsToasts();
From 34112b97b07c68e885d7a917d3c768f5274a6229 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Manuel?=
<6741785+jluque0101@users.noreply.github.com>
Date: Wed, 5 Nov 2025 11:03:59 +0100
Subject: [PATCH 3/4] chore(INFRA-3081): add branch name validation step for
changelog refresh (#21853)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Improve changelog management
## **Changelog**
CHANGELOG entry: null
## **Related issues**
INFRA-3031
Fixes:
- Run `Update Release Changelog PR` only when release branch matches
`release/x.y.z`
## **Manual testing steps**
- Test release branch name `release/x.y.z` ->
https://github.com/consensys-test/metamask-mobile-test-workflow/actions/runs/18942043247/job/54083117562
- Test release branch name `release/x.y.z-Changelog` ->
https://github.com/consensys-test/metamask-mobile-test-workflow/actions/runs/18942263401/job/54083874574
- Test invalid branch name `release/test-branch` ->
https://github.com/consensys-test/metamask-mobile-test-workflow/actions/runs/18942308096/job/54084029210
- Test invalid branch, incomplete semver `release/x.y` ->
https://github.com/consensys-test/metamask-mobile-test-workflow/actions/runs/18942339330/job/54084135741
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] 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).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] 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.
---
.../workflows/update-release-changelog.yml | 31 ++++++++++++++++++-
1 file changed, 30 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/update-release-changelog.yml b/.github/workflows/update-release-changelog.yml
index b5bfa166975b..3a68e31a1ed1 100644
--- a/.github/workflows/update-release-changelog.yml
+++ b/.github/workflows/update-release-changelog.yml
@@ -1,5 +1,7 @@
-# On every push to a release branch (Version-v* or release/*), this workflow rebuilds the matching
+# On every push to a release branch (release/x.y.z format), this workflow rebuilds the matching
# changelog branch, re-runs the auto-changelog script, and either updates or recreates the changelog PR.
+# Note: This workflow validates the branch name to ensure it matches the semantic versioning pattern
+# (release/x.y.z) and skips execution for other branch names like release/x.y.z-Changelog.
name: Update Release Changelog PR
on:
@@ -12,7 +14,34 @@ concurrency:
cancel-in-progress: false
jobs:
+ validate-branch:
+ name: Validate release branch format
+ runs-on: ubuntu-latest
+ outputs:
+ is-valid-release: ${{ steps.check.outputs.is-valid }}
+ version: ${{ steps.check.outputs.version }}
+ steps:
+ - name: Check if branch matches release/x.y.z pattern
+ id: check
+ run: |
+ BRANCH_NAME="${{ github.ref_name }}"
+ echo "Checking branch: $BRANCH_NAME"
+
+ # Validate branch matches release/x.y.z format (semantic versioning)
+ if [[ "$BRANCH_NAME" =~ ^release/[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ VERSION="${BRANCH_NAME#release/}"
+ echo "Valid release branch detected: $BRANCH_NAME (version: $VERSION)"
+ echo "is-valid=true" >> "$GITHUB_OUTPUT"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ else
+ echo "Branch '$BRANCH_NAME' does not match release/x.y.z pattern. Skipping changelog update."
+ echo "is-valid=false" >> "$GITHUB_OUTPUT"
+ fi
+
refresh-changelog:
+ name: Update changelog
+ needs: validate-branch
+ if: needs.validate-branch.outputs.is-valid-release == 'true'
permissions:
contents: write
pull-requests: write
From 0771424180b84b70bdd7034132529fc4c6e208d2 Mon Sep 17 00:00:00 2001
From: Gaurav Goel
Date: Wed, 5 Nov 2025 16:14:11 +0530
Subject: [PATCH 4/4] feat: updated import wallet ui for Add Wallet flow in
home screen (#21686)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
In this PR, I've aligned the Import Wallet UI in the 'Add Wallet' flow
with the existing 'Import SRP' onboarding design for consistency
Jira Link: https://consensyssoftware.atlassian.net/browse/SL-238,
https://consensyssoftware.atlassian.net/browse/SL-274,
https://consensyssoftware.atlassian.net/browse/SL-275
BugFixes:
https://consensyssoftware.atlassian.net/browse/SL-267
https://github.com/MetaMask/metamask-mobile/issues/15928
https://github.com/MetaMask/metamask-mobile/issues/15929
Figma Link:
https://www.figma.com/design/pViOUcmjwhEzFsdrwknpNc/Onboarding?node-id=18161-80663&t=1c5E4QKtEnnYjijl-0
## **Changelog**
CHANGELOG entry: Align Import Wallet UI in Add Wallet flow with Import
SRP onboarding design for consistency.
## **Related issues**
Fixes:
1) https://consensyssoftware.atlassian.net/browse/SL-267
2) https://github.com/MetaMask/metamask-mobile/issues/15928
3) https://github.com/MetaMask/metamask-mobile/issues/15929
## **Manual testing steps**
```gherkin
Feature: my feature name
1) Open the App.
2) Create a new wallet or import an existing one
3)On the home page, click "Add Wallet" from the account dropdown
4) Verify that the Import Wallet UI matches the design in the provided Figma link
```
## **Screenshots/Recordings**
### **Before**
https://www.figma.com/design/pViOUcmjwhEzFsdrwknpNc/Onboarding?node-id=18161-80216&t=1c5E4QKtEnnYjijl-0
### **After**
https://github.com/user-attachments/assets/76e3db92-27c3-4b3a-9623-263347712a33
https://github.com/user-attachments/assets/da90b1a1-e077-47bc-ba3a-5fc032b4b6a2
## **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**
- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] 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]
> Introduces a reusable `SrpInputGrid` with paste/clear, validation, and
QR support, and refactors SRP import screens and navigation to use it
with updated UI, tests, and locales.
>
> - **UI/Flows**:
> - Refactor `ImportFromSecretRecoveryPhrase` and
`ImportNewSecretRecoveryPhrase` to use new grid-based SRP input, with
updated layout, navbar, and SafeArea handling.
> - Disable/enable continue/import buttons based on SRP validity; show
inline errors; add paste/clear affordances.
> - **Components**:
> - Add reusable `app/components/UI/SrpInputGrid` (styles, types, logic)
supporting textarea→grid switch, keyboard navigation, word validation,
and QR-filled input.
> - Update `SrpInput` input style typings to `TextStyle`.
> - **Navigation**:
> - Update `ImportSRPView` stack to `mode="modal"`; add routes for
`QRTabSwitcher` and `SeedphraseModal` with transparent card style.
> - **Validation/Utils**:
> - Add `app/util/srp/srpInputUtils` (lengths, space handling, word
validation, helpers).
> - **Tests**:
> - Add unit snapshots for `SrpInputGrid`; expand tests for both import
screens (paste/clear, QR, errors, keyboard/backspace, focus); update E2E
selectors and page objects.
> - **Localization**:
> - Add/adjust strings for new import wallet copy, placeholders, and QR
error messages.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
6010425f1db48362e87029807336db497fdfe129. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---------
Co-authored-by: ieow <4881057+ieow@users.noreply.github.com>
---
app/components/Nav/App/App.tsx | 9 +
.../UI/SrpInputGrid/SrpInputGrid.styles.ts | 107 ++
.../UI/SrpInputGrid/SrpInputGrid.types.ts | 46 +
.../__snapshots__/index.test.tsx.snap | 1295 +++++++++++++++++
app/components/UI/SrpInputGrid/index.test.tsx | 59 +
app/components/UI/SrpInputGrid/index.tsx | 463 ++++++
.../__snapshots__/index.test.tsx.snap | 101 +-
.../ImportFromSecretRecoveryPhrase/index.js | 390 +----
.../index.test.tsx | 28 +-
.../index.test.tsx | 1128 +++++++++++---
.../ImportNewSecretRecoveryPhrase/index.tsx | 446 +++---
.../ImportNewSecretRecoveryPhrase/styles.ts | 85 +-
.../Views/SrpInput/Input/indes.styles.ts | 6 +-
app/components/Views/SrpInput/Input/index.tsx | 4 +-
app/components/Views/SrpInput/index.tsx | 4 +-
app/util/srp/srpInputUtils.ts | 45 +
e2e/pages/importSrp/ImportSrpView.ts | 56 +-
e2e/selectors/MultiSRP/SRPImport.selectors.js | 3 +
e2e/specs/multisrp/utils.ts | 11 +-
locales/languages/en.json | 5 +
20 files changed, 3361 insertions(+), 930 deletions(-)
create mode 100644 app/components/UI/SrpInputGrid/SrpInputGrid.styles.ts
create mode 100644 app/components/UI/SrpInputGrid/SrpInputGrid.types.ts
create mode 100644 app/components/UI/SrpInputGrid/__snapshots__/index.test.tsx.snap
create mode 100644 app/components/UI/SrpInputGrid/index.test.tsx
create mode 100644 app/components/UI/SrpInputGrid/index.tsx
create mode 100644 app/util/srp/srpInputUtils.ts
diff --git a/app/components/Nav/App/App.tsx b/app/components/Nav/App/App.tsx
index 7fc582cd9c74..3fc74038b913 100644
--- a/app/components/Nav/App/App.tsx
+++ b/app/components/Nav/App/App.tsx
@@ -597,6 +597,7 @@ const ImportPrivateKeyView = () => (
const ImportSRPView = () => (
(
name={Routes.MULTI_SRP.IMPORT}
component={ImportNewSecretRecoveryPhrase}
/>
+
+
);
diff --git a/app/components/UI/SrpInputGrid/SrpInputGrid.styles.ts b/app/components/UI/SrpInputGrid/SrpInputGrid.styles.ts
new file mode 100644
index 000000000000..85690f8ea063
--- /dev/null
+++ b/app/components/UI/SrpInputGrid/SrpInputGrid.styles.ts
@@ -0,0 +1,107 @@
+import { StyleSheet, Platform, TextStyle } from 'react-native';
+import { fontStyles, colors as commonColors } from '../../../styles/common';
+import { Colors } from '../../../util/theme/models';
+
+/**
+ * Creates styles for the SrpInputGrid component
+ * @param colors - Theme colors object
+ * @returns StyleSheet object with all component styles
+ */
+export const createStyles = (colors: Colors) =>
+ StyleSheet.create({
+ seedPhraseRoot: {
+ flexDirection: 'column' as const,
+ gap: 4,
+ marginBottom: 24,
+ },
+ seedPhraseContainer: {
+ paddingTop: 16,
+ backgroundColor: colors.background.section,
+ borderRadius: 10,
+ marginTop: 16,
+ minHeight: 264,
+ maxHeight: 'auto',
+ },
+ seedPhraseInnerContainer: {
+ paddingHorizontal: Platform.select({
+ ios: 16,
+ macos: 16,
+ default: 14,
+ }),
+ },
+ seedPhraseInputContainer: {
+ flexDirection: 'row' as const,
+ flexWrap: 'wrap' as const,
+ width: '100%',
+ },
+ seedPhraseDefaultInput: {
+ borderWidth: 0,
+ paddingHorizontal: 0,
+ display: 'flex' as const,
+ flex: 1,
+ backgroundColor: commonColors.transparent,
+ },
+ seedPhraseInputItem: {
+ width: '31.33%',
+ marginRight: '3%',
+ marginBottom: 8,
+ flex: 0,
+ minWidth: 0,
+ },
+ seedPhraseInputItemLast: {
+ marginRight: 0,
+ },
+ textAreaInput: {
+ display: 'flex' as const,
+ height: 66,
+ color: colors.text.alternative,
+ backgroundColor: commonColors.transparent,
+ ...fontStyles.normal,
+ fontSize: 16,
+ lineHeight: 20,
+ paddingTop: Platform.OS === 'ios' ? 12 : 8,
+ paddingBottom: 12,
+ } satisfies TextStyle,
+ inputItem: {
+ flex: 1,
+ minWidth: 0,
+ maxWidth: '100%',
+ paddingRight: 8,
+ } satisfies TextStyle,
+ input: {
+ paddingVertical: Platform.select({
+ ios: 4,
+ macos: 4,
+ default: 0,
+ }),
+ borderRadius: 8,
+ backgroundColor: colors.background.default,
+ flexDirection: 'row' as const,
+ alignItems: 'center' as const,
+ justifyContent: 'flex-start' as const,
+ height: 40,
+ fontSize: 16,
+ color: colors.text.default,
+ ...fontStyles.normal,
+ textAlignVertical: 'center' as const,
+ paddingLeft: 8,
+ overflow: 'hidden' as const,
+ } satisfies TextStyle,
+ inputIndex: {
+ marginRight: -4,
+ },
+ pasteText: {
+ textAlign: 'right' as const,
+ paddingTop: 12,
+ paddingBottom: 16,
+ alignSelf: 'flex-end' as const,
+ } satisfies TextStyle,
+
+ hiddenInput: {
+ opacity: 0,
+ position: 'absolute' as const,
+ height: 0,
+ top: 0,
+ left: 0,
+ } satisfies TextStyle,
+ });
diff --git a/app/components/UI/SrpInputGrid/SrpInputGrid.types.ts b/app/components/UI/SrpInputGrid/SrpInputGrid.types.ts
new file mode 100644
index 000000000000..ef68f100372a
--- /dev/null
+++ b/app/components/UI/SrpInputGrid/SrpInputGrid.types.ts
@@ -0,0 +1,46 @@
+/**
+ * Props for the SrpInputGrid component
+ * This component provides a reusable Secret Recovery Phrase input grid
+ * that handles both single textarea and multi-input modes
+ */
+export interface SrpInputGridProps {
+ /**
+ * Array of seed phrase words
+ */
+ seedPhrase: string[];
+
+ /**
+ * Callback when seed phrase array changes
+ */
+ onSeedPhraseChange: React.Dispatch>;
+
+ /**
+ * Callback when error state changes
+ */
+ onError?: (error: string) => void;
+
+ /**
+ * External error message to display from parent
+ */
+ externalError?: string;
+
+ /**
+ * Prefix for test IDs (e.g., 'seed-phrase-input' or 'import-from-seed-input')
+ */
+ testIdPrefix: string;
+
+ /**
+ * Placeholder text for the first input (textarea mode)
+ */
+ placeholderText: string;
+
+ /**
+ * Unique ID for key generation (optional, will generate if not provided)
+ */
+ uniqueId?: string;
+
+ /**
+ * Whether the inputs should be disabled
+ */
+ disabled?: boolean;
+}
diff --git a/app/components/UI/SrpInputGrid/__snapshots__/index.test.tsx.snap b/app/components/UI/SrpInputGrid/__snapshots__/index.test.tsx.snap
new file mode 100644
index 000000000000..d21fa24bec8f
--- /dev/null
+++ b/app/components/UI/SrpInputGrid/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,1295 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SrpInputGrid renders with custom uniqueId 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Paste
+
+
+`;
+
+exports[`SrpInputGrid renders with disabled state 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Paste
+
+
+`;
+
+exports[`SrpInputGrid renders with empty seed phrase 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Paste
+
+
+`;
+
+exports[`SrpInputGrid renders with multiple words 1`] = `
+
+
+
+
+
+
+
+ 1
+ .
+
+
+
+
+
+
+
+
+
+ 2
+ .
+
+
+
+
+
+
+
+
+
+ 3
+ .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Clear all
+
+
+`;
diff --git a/app/components/UI/SrpInputGrid/index.test.tsx b/app/components/UI/SrpInputGrid/index.test.tsx
new file mode 100644
index 000000000000..7efc2ef6b712
--- /dev/null
+++ b/app/components/UI/SrpInputGrid/index.test.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { ImportSRPIDs } from '../../../../e2e/selectors/MultiSRP/SRPImport.selectors';
+import renderWithProvider from '../../../util/test/renderWithProvider';
+import SrpInputGrid from './index';
+
+// Mock Keyboard
+jest.mock('react-native/Libraries/Components/Keyboard/Keyboard', () => ({
+ dismiss: jest.fn(),
+}));
+
+describe('SrpInputGrid', () => {
+ const mockOnSeedPhraseChange = jest.fn();
+ const mockOnError = jest.fn();
+
+ const defaultProps = {
+ seedPhrase: [''],
+ onSeedPhraseChange: mockOnSeedPhraseChange,
+ onError: mockOnError,
+ testIdPrefix: ImportSRPIDs.SEED_PHRASE_INPUT_ID,
+ placeholderText: 'Enter your Secret Recovery Phrase',
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+ });
+
+ it('renders with empty seed phrase', () => {
+ const { toJSON } = renderWithProvider();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('renders with multiple words', () => {
+ const seedPhrase = ['word1', 'word2', 'word3'];
+ const { toJSON } = renderWithProvider(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('renders with disabled state', () => {
+ const { toJSON } = renderWithProvider(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('renders with custom uniqueId', () => {
+ const { toJSON } = renderWithProvider(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/SrpInputGrid/index.tsx b/app/components/UI/SrpInputGrid/index.tsx
new file mode 100644
index 000000000000..446ea443fbe8
--- /dev/null
+++ b/app/components/UI/SrpInputGrid/index.tsx
@@ -0,0 +1,463 @@
+import React, {
+ useCallback,
+ useMemo,
+ useRef,
+ useState,
+ useEffect,
+} from 'react';
+import { View, Keyboard } from 'react-native';
+import Clipboard from '@react-native-clipboard/clipboard';
+import { v4 as uuidv4 } from 'uuid';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../component-library/components/Texts/Text';
+import SrpInput from '../../Views/SrpInput';
+import { TextFieldSize } from '../../../component-library/components/Form/TextField';
+import { useAppTheme } from '../../../util/theme';
+import { createStyles } from './SrpInputGrid.styles';
+import { SrpInputGridProps } from './SrpInputGrid.types';
+import { strings } from '../../../../locales/i18n';
+import {
+ getTrimmedSeedPhraseLength,
+ isFirstInput as isFirstInputUtil,
+ getInputValue,
+ SRP_LENGTHS,
+ SPACE_CHAR,
+ checkValidSeedWord,
+} from '../../../util/srp/srpInputUtils';
+import { isValidMnemonic } from '../../../util/validators';
+import { formatSeedPhraseToSingleLine } from '../../../util/string';
+import Logger from '../../../util/Logger';
+
+export interface SrpInputGridRef {
+ handleSeedPhraseChange: (seedPhraseText: string) => void;
+}
+/**
+ * SrpInputGrid Component
+ *
+ * A reusable component for Secret Recovery Phrase input that supports:
+ * - Single textarea mode for initial input
+ * - Dynamic grid mode after paste/input
+ * - Paste/Clear functionality
+ * - Error validation and display
+ * - Keyboard navigation
+ *
+ */
+const SrpInputGrid = React.forwardRef(
+ (
+ {
+ seedPhrase,
+ onSeedPhraseChange,
+ onError,
+ externalError = '',
+ testIdPrefix,
+ placeholderText,
+ uniqueId = uuidv4(),
+ disabled = false,
+ },
+ ref,
+ ) => {
+ const { colors } = useAppTheme();
+ const styles = createStyles(colors);
+
+ // Internal state
+ const [
+ nextSeedPhraseInputFocusedIndex,
+ setNextSeedPhraseInputFocusedIndex,
+ ] = useState(null);
+ const [errorWordIndexes, setErrorWordIndexes] = useState<
+ Record
+ >({});
+
+ const seedPhraseInputRefs = useRef