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
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/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 f442eb268fd3..7dde0afb2d9b 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';
@@ -60,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 }[] =
@@ -87,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();
@@ -232,7 +235,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);
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