From f8af9139244d4017667f29b45093b25ef67d3b03 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 10:46:00 +0200
Subject: [PATCH 01/66] chore(runway): cherry-pick feat(rewards): benefits
preview uses Tag for available count cp-7.78.0 (#30211)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- feat(rewards): benefits preview uses Tag for available count (#30196)
## **Description**
> **Section header spacing alignment:**
[`CampaignsPreview.tsx`](app/components/UI/Rewards/components/Campaigns/CampaignsPreview.tsx)
— the campaigns preview **header row** now uses **`gap-1`** instead of
**`gap-2`** between the title row items (spinner when shown, heading,
chevron), matching the **Benefits** preview header spacing on the
Rewards dashboard.
The Rewards dashboard **Benefits** preview header previously used a
numeric `BadgeCount` beside the section title. This change replaces it
with a design-system **`Tag`** (`TagSeverity.Neutral`) that shows how
many benefits are available using the localized string
`rewards.benefits.available_count` (e.g. `%{count} available` in
English). Counts above 99 display as `99+`.
The header row is full width with **`justifyContent: space-between`**:
the title and chevron stay on the leading side; the tag sits on the
**trailing** edge so it matches the intended layout. Spacing uses
`gap-1` between the title and chevron. The empty-state header no longer
renders a redundant null badge slot.
**Motivation:** Align with design (muted pill copy and alignment) and
keep copy in i18n for the translation pipeline (English source string
only in `en.json`).
**Automated tests:** `yarn jest
app/components/UI/Rewards/components/Benefits/BenefitsPreview.test.tsx`
## **Changelog**
CHANGELOG entry: Updated the Rewards benefits preview header to show how
many benefits are available using a tag label next to the section title;
aligned campaigns preview section header spacing with the benefits
preview (`gap-1`).
## **Related issues**
Fixes:
## **Manual testing steps**
```gherkin
Feature: Rewards benefits preview header
Scenario: User sees benefits count when they have benefits
Given the user is opted into Rewards and the benefits API returns at least one benefit
When user opens the Rewards dashboard (home) and scrolls to the Benefits preview
Then a neutral tag shows the correct count and available wording for the current locale
And the tag is aligned to the far right of the header row with the title and chevron grouped on the left
And tapping the header row still navigates to the full benefits view
Scenario: User has no benefits
Given the user has no benefits in the list
When user opens the Rewards dashboard and views the Benefits preview
Then the count tag is not shown and the empty state behaves as before
Feature: Rewards campaigns preview header spacing
Scenario: User compares section headers on Rewards home
Given the user is on the Rewards dashboard home tab
When user views the Campaigns preview section header
Then the title row uses gap-1 between the title row elements, matching the Benefits preview header spacing
```
## **Screenshots/Recordings**
### **Before**
Numeric badge style (`BadgeCount`) adjacent to the Benefits title
(previous implementation).
### **After**
Neutral `Tag` with “{count} available” (per locale), title + chevron on
the left, tag aligned to the trailing edge. (Screenshot also attached in
an earlier PR update.)
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Low risk UI/i18n change with unit test updates; primary risk is minor
layout/regression in rewards preview headers and count formatting around
the 99/99+ boundary.
>
> **Overview**
> The Benefits preview header now replaces the numeric `BadgeCount` with
a neutral design-system `Tag` that shows a localized
`rewards.benefits.available_count` label (capped to `99+`), and adjusts
header layout to keep the title+chevron grouped left with the tag
right-aligned.
>
> Campaigns preview header spacing is tightened (`gap-1`), and tests
were expanded to cover the new benefits count behavior (including empty
state and 99/99+ cases) plus treating `undefined` campaigns as an empty
list. Adds the new English i18n key `rewards.benefits.available_count`.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5ece706b382678bd32219e005e2de6b43ac80ec6. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[f8f126d](https://github.com/MetaMask/metamask-mobile/commit/f8f126ddc6204a7a03fb7e4660f063dc2aa0d4eb)
Co-authored-by: Andrew Cohen
---
.../Benefits/BenefitsPreview.test.tsx | 96 ++++++++++++++++++-
.../components/Benefits/BenefitsPreview.tsx | 48 ++++++----
.../Campaigns/CampaignsPreview.test.tsx | 14 +++
.../components/Campaigns/CampaignsPreview.tsx | 2 +-
locales/languages/en.json | 3 +-
5 files changed, 140 insertions(+), 23 deletions(-)
diff --git a/app/components/UI/Rewards/components/Benefits/BenefitsPreview.test.tsx b/app/components/UI/Rewards/components/Benefits/BenefitsPreview.test.tsx
index 72fa503ec026..047572a8c00c 100644
--- a/app/components/UI/Rewards/components/Benefits/BenefitsPreview.test.tsx
+++ b/app/components/UI/Rewards/components/Benefits/BenefitsPreview.test.tsx
@@ -12,7 +12,14 @@ const mockNavigate = jest.fn();
const mockUseSelector = jest.fn();
const mockUseBenefits = jest.fn();
-const mockStrings = jest.fn((key: string) => {
+const mockStrings = jest.fn((key: string, params?: Record) => {
+ if (
+ key === 'rewards.benefits.available_count' &&
+ params &&
+ typeof params.count === 'string'
+ ) {
+ return `${params.count} available`;
+ }
const translations: Record = {
'rewards.benefits.title': 'Benefits',
'rewards.benefits.empty-list': 'No benefits available yet',
@@ -42,7 +49,8 @@ jest.mock('../../hooks/useBenefits', () => ({
}));
jest.mock('../../../../../../locales/i18n', () => ({
- strings: (key: string) => mockStrings(key),
+ strings: (key: string, params?: Record) =>
+ mockStrings(key, params),
}));
jest.mock('@metamask/design-system-twrnc-preset', () => {
@@ -126,7 +134,10 @@ describe('BenefitsPreview', () => {
it('requests rewards benefits title copy from i18n', () => {
render();
- expect(mockStrings).toHaveBeenCalledWith('rewards.benefits.title');
+ expect(mockStrings).toHaveBeenCalledWith(
+ 'rewards.benefits.title',
+ undefined,
+ );
});
it('reads subscription benefits and loading state from the store', () => {
@@ -169,7 +180,20 @@ describe('BenefitsPreview', () => {
expect(getByTestId('benefit-empty-list')).toBeOnTheScreen();
expect(getByText('No benefits available yet')).toBeOnTheScreen();
- expect(mockStrings).toHaveBeenCalledWith('rewards.benefits.empty-list');
+ expect(mockStrings).toHaveBeenCalledWith(
+ 'rewards.benefits.empty-list',
+ undefined,
+ );
+ });
+
+ it('does not request available_count copy when there are no benefits', () => {
+ render();
+
+ const availableCountCalls = mockStrings.mock.calls.filter(
+ (call) => call[0] === 'rewards.benefits.available_count',
+ );
+
+ expect(availableCountCalls).toHaveLength(0);
});
it('does not render benefit details container without benefits', () => {
@@ -225,6 +249,70 @@ describe('BenefitsPreview', () => {
Routes.REWARD_BENEFITS_FULL_VIEW,
);
});
+
+ it('shows available benefits count in the header tag', () => {
+ const { getByText } = render();
+
+ expect(getByText('2 available')).toBeOnTheScreen();
+ expect(mockStrings).toHaveBeenCalledWith(
+ 'rewards.benefits.available_count',
+ {
+ count: '2',
+ },
+ );
+ });
+
+ it('caps displayed benefits count at 99+ in the header tag', () => {
+ mockBenefits = Array.from({ length: 100 }, (_, index) => ({
+ id: index + 1,
+ longTitle: `Benefit ${index + 1}`,
+ shortDescription: 'd',
+ }));
+
+ const { getByText } = render();
+
+ expect(getByText('99+ available')).toBeOnTheScreen();
+ expect(mockStrings).toHaveBeenCalledWith(
+ 'rewards.benefits.available_count',
+ {
+ count: '99+',
+ },
+ );
+ });
+
+ it('displays numeric count 99 in the header tag when there are exactly 99 benefits', () => {
+ mockBenefits = Array.from({ length: 99 }, (_, index) => ({
+ id: index + 1,
+ longTitle: `Benefit ${index + 1}`,
+ shortDescription: 'd',
+ }));
+
+ const { getByText } = render();
+
+ expect(getByText('99 available')).toBeOnTheScreen();
+ expect(mockStrings).toHaveBeenCalledWith(
+ 'rewards.benefits.available_count',
+ {
+ count: '99',
+ },
+ );
+ });
+
+ it('renders a single benefit card when the list has one item', () => {
+ mockBenefits = [
+ { id: 42, longTitle: 'Solo benefit', shortDescription: 'only one' },
+ ];
+
+ const { getByTestId, getByText, queryByTestId } = render(
+ ,
+ );
+
+ expect(
+ getByTestId(REWARDS_VIEW_SELECTORS.TOP_BENEFIT_DETAILS),
+ ).toBeOnTheScreen();
+ expect(getByText('Solo benefit')).toBeOnTheScreen();
+ expect(queryByTestId('benefit-card-2')).toBeNull();
+ });
});
describe('header without benefits', () => {
diff --git a/app/components/UI/Rewards/components/Benefits/BenefitsPreview.tsx b/app/components/UI/Rewards/components/Benefits/BenefitsPreview.tsx
index 6e026a20f7db..ddad43b5d7ef 100644
--- a/app/components/UI/Rewards/components/Benefits/BenefitsPreview.tsx
+++ b/app/components/UI/Rewards/components/Benefits/BenefitsPreview.tsx
@@ -1,14 +1,15 @@
import {
- BadgeCount,
- BadgeCountSize,
Box,
BoxAlignItems,
- BoxBackgroundColor,
BoxFlexDirection,
+ BoxJustifyContent,
Icon,
+ IconColor,
IconName,
IconSize,
Skeleton,
+ Tag,
+ TagSeverity,
Text,
TextColor,
TextVariant,
@@ -41,15 +42,18 @@ const BenefitsPreview = () => {
const hasBenefits = benefits.length > 0;
const topBenefits = benefits.slice(0, 3);
+ const benefitsCountLabel =
+ benefits.length > 99 ? '99+' : String(benefits.length);
+
const benefitsCountBadge =
benefits.length > 0 ? (
-
+
+
+ {strings('rewards.benefits.available_count', {
+ count: benefitsCountLabel,
+ })}
+
+
) : null;
const displayHeader = hasBenefits ? (
@@ -57,25 +61,35 @@ const BenefitsPreview = () => {
-
- {strings('rewards.benefits.title')}
-
+
+
+ {strings('rewards.benefits.title')}
+
+
+
{benefitsCountBadge}
-
) : (
{strings('rewards.benefits.title')}
- {benefitsCountBadge}
);
diff --git a/app/components/UI/Rewards/components/Campaigns/CampaignsPreview.test.tsx b/app/components/UI/Rewards/components/Campaigns/CampaignsPreview.test.tsx
index a42d94b194c4..eb96d627b5db 100644
--- a/app/components/UI/Rewards/components/Campaigns/CampaignsPreview.test.tsx
+++ b/app/components/UI/Rewards/components/Campaigns/CampaignsPreview.test.tsx
@@ -129,6 +129,20 @@ describe('CampaignsPreview', () => {
expect(queryByTestId('campaign-tile-campaign-1')).toBeNull();
});
+ it('treats undefined campaigns as an empty list for featured selection', () => {
+ mockUseRewardCampaigns.mockReturnValue({
+ ...mockHookDefaults,
+ campaigns: undefined as unknown as CampaignDto[],
+ });
+
+ const { getByTestId, queryByTestId } = render();
+
+ expect(
+ getByTestId(REWARDS_VIEW_SELECTORS.CAMPAIGNS_PREVIEW),
+ ).toBeOnTheScreen();
+ expect(queryByTestId('campaign-tile-campaign-1')).toBeNull();
+ });
+
it('renders loading skeleton when campaigns have never been loaded', () => {
mockUseRewardCampaigns.mockReturnValue({
...mockHookDefaults,
diff --git a/app/components/UI/Rewards/components/Campaigns/CampaignsPreview.tsx b/app/components/UI/Rewards/components/Campaigns/CampaignsPreview.tsx
index d553229cb573..96ca2c9a3252 100644
--- a/app/components/UI/Rewards/components/Campaigns/CampaignsPreview.tsx
+++ b/app/components/UI/Rewards/components/Campaigns/CampaignsPreview.tsx
@@ -57,7 +57,7 @@ const CampaignsPreview: React.FC = () => {
{(isLoading || !hasLoaded) && !hasFeaturedCampaigns && (
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 62df536e54e9..8e99265ab3c0 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -8592,7 +8592,8 @@
"title_claim": "Claim Benefit",
"action": "Claim",
"empty-list": "You don’t have any benefits right now.",
- "powered_by": "Powered by"
+ "powered_by": "Powered by",
+ "available_count": "{{count}} available"
},
"end_of_season_rewards": {
"confirm_label_default": "Confirm",
From fc4026abfc609f768f486e02be32a14460d41bde Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 11:00:42 +0200
Subject: [PATCH 02/66] chore(runway): cherry-pick fix: cp-7.78.0 add correct
permissions to running perf builds (#30224)
- fix: cp-7.78.0 add correct permissions to running perf builds (#30223)
## **Description**
> Updates the `run-performance-e2e-experimental.yml` and
`run-performance-e2e-release.yml` GitHub Actions workflows to grant
`permissions.contents: write` (instead of read-only) so they are at
least as permissive as the reusable `run-performance-e2e.yml` workflow
they call.
>
> Adds an inline comment documenting the transitive requirement (via the
BrowserStack upload workflows).
>
## **Changelog**
CHANGELOG entry:
## **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**
- [ ] 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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Low risk workflow-only change that broadens GitHub token permissions
to match the called reusable workflow; main risk is over-permissioning
if not actually required.
>
> **Overview**
> Updates the `run-performance-e2e-experimental.yml` and
`run-performance-e2e-release.yml` GitHub Actions workflows to grant
`permissions.contents: write` (instead of read-only) so they are at
least as permissive as the reusable `run-performance-e2e.yml` workflow
they call.
>
> Adds an inline comment documenting the transitive requirement (via the
BrowserStack upload workflows).
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
54b2260e237e7e1412f1632e689b89d343920880. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[8fbb9a4](https://github.com/MetaMask/metamask-mobile/commit/8fbb9a4e47b33ee7a9803367670774c846ead901)
Co-authored-by: Curtis David
---
.github/workflows/run-performance-e2e-experimental.yml | 4 +++-
.github/workflows/run-performance-e2e-release.yml | 5 +++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/run-performance-e2e-experimental.yml b/.github/workflows/run-performance-e2e-experimental.yml
index f4bfd8779d21..61e25143e67c 100644
--- a/.github/workflows/run-performance-e2e-experimental.yml
+++ b/.github/workflows/run-performance-e2e-experimental.yml
@@ -18,8 +18,10 @@ on:
branches:
- main
+# Must be at least as permissive as the called reusable workflow (run-performance-e2e.yml),
+# which transitively requires contents: write via build-{android,ios}-upload-to-browserstack.yml.
permissions:
- contents: read
+ contents: write
id-token: write
actions: write
diff --git a/.github/workflows/run-performance-e2e-release.yml b/.github/workflows/run-performance-e2e-release.yml
index ad4b652f7354..198b50b59914 100644
--- a/.github/workflows/run-performance-e2e-release.yml
+++ b/.github/workflows/run-performance-e2e-release.yml
@@ -16,9 +16,10 @@ on:
branches:
- 'release/*'
-# Required so the reusable workflow run-performance-e2e.yml can use id-token and actions
+# Must be at least as permissive as the called reusable workflow (run-performance-e2e.yml),
+# which transitively requires contents: write via build-{android,ios}-upload-to-browserstack.yml.
permissions:
- contents: read
+ contents: write
id-token: write
actions: write
From ec7059213ab801b7aa79b5a4e9d4e1e17409fcc1 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Fri, 15 May 2026 09:02:35 +0000
Subject: [PATCH 03/66] [skip ci] Bump version number to 5004
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 0307e2fd43c7..42eb954f9880 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 4532
+ versionCode 5004
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 84431c1ae49a..cdd29ba5be7f 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 4823
+ VERSION_NUMBER: 5004
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 4823
+ FLASK_VERSION_NUMBER: 5004
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 445cffdce220..a975fb3f6923 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 4823;
+ CURRENT_PROJECT_VERSION = 5004;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 4823;
+ CURRENT_PROJECT_VERSION = 5004;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 4823;
+ CURRENT_PROJECT_VERSION = 5004;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 4823;
+ CURRENT_PROJECT_VERSION = 5004;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From b35b7f445303c1d93c263dac1b590b7a381256be Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 12:52:47 +0200
Subject: [PATCH 04/66] chore(runway): cherry-pick fix: cp-7.77.0 cp-7.78.0
missing metamask pay transactions in activity (#30217)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- fix: cp-7.77.0 cp-7.78.0 missing metamask pay transactions in activity
(#30145)
## **Description**
The Activity tab had several bugs causing MetaMask Pay transactions to
be missing, duplicated, or unreachable from the source chain. This PR
addresses four root causes in production code plus a test alignment for
the bridge smoke E2E:
1. **Source-chain visibility.** Submitted EVM transactions were filtered
strictly by `tx.chainId`, so a MetaMask Pay parent was only visible on
its destination chain. The source chain is recorded on
`metamaskPay.chainId` (for gasless flows) or on linked child
transactions via `requiredTransactionIds` (for non-gasless flows). A new
`selectRelatedChainIdsByTransactionId` selector returns the full set of
chain IDs a transaction relates to, and the Activity list now matches
against that set.
2. **Dedupe fallback collapsed internal MetaMask Pay transactions.**
When a transaction had no nonce, `selectLocalTransactions` fell back to
`txParams.actionId` as the dedupe key. `actionId` is a top-level field
on `TransactionMeta`, not on `txParams`, so for MetaMask Pay internal
transactions (which have no nonce) every entry collapsed onto the same
`undefined` key and all but one were dropped. The fallback now uses the
top-level `id`, which is always present.
3. **Local transactions were scoped to the wrong account.**
`selectLocalTransactions` gated on `selectEvmAddress` — the EVM address
of the **currently selected internal account**. When the user picked a
non-EVM account (e.g. Solana), this was `undefined` and the selector
returned an empty list. Switching to "All popular networks" did not
restore the address because that toggle changes enabled networks, not
the selected account. It now uses
`selectSelectedAccountGroupEvmInternalAccount`, the same source already
used by the Activity tab's API query.
4. **Incoming-transaction duplicates.** The `TransactionController`
incoming-transactions feature stores incoming transfers as separate
`TransactionMeta` entries marked with `isTransfer !== undefined`. The
accounts API also returns these transactions in its confirmed history,
producing duplicate rows in the Activity tab. The dedupe step now skips
entries with `isTransfer !== undefined`, leaving the accounts-API row as
the canonical source.
5. **Bridge smoke E2E row alignment.** The Activity list merges pending
smart transactions in alongside the real `TransactionMeta` row,
producing a stale shell entry that lands at row 0. `bridge-action-smoke`
was asserting on row 0 and timing out. The test now asserts on row 1,
with a TODO to remove the STX-state merge from the Activity selectors
and restore row 0.
## **Changelog**
CHANGELOG entry: Fixed MetaMask Pay transactions appearing duplicated or
missing from the Activity tab, including on the source chain and when
the selected account is non-EVM.
## **Related issues**
Fixes:
[#30066](https://github.com/MetaMask/metamask-mobile/issues/30066)
## **Manual testing steps**
```gherkin
Feature: MetaMask Pay Activity visibility
Scenario: User views Activity on the chain that funded a MetaMask Pay transaction
Given the user has completed a MetaMask Pay transaction funded by a token on chain X with destination chain Y
And both chains X and Y are enabled networks
When the user opens the Activity tab with chain X selected
Then the MetaMask Pay transaction is visible in the list
When the user opens the Activity tab with chain Y selected
Then the MetaMask Pay transaction is also visible in the list
Scenario: User views Activity after switching to a non-EVM account
Given the user has pending MetaMask Pay transactions visible in the Activity tab
When the user switches to a non-EVM account in the same account group
And switches back to "All popular networks"
Then the pending MetaMask Pay transactions remain visible
Scenario: User views a single on-chain MetaMask Pay transaction
Given the user has completed a single-chain MetaMask Pay transaction (for example an mUSD conversion)
When the user opens the Activity tab
Then the transaction appears exactly once
```
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- [ ] I've tested with a power user scenario
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
## **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]
> [Cursor Bugbot](https://cursor.com/bugbot) is generating a
summary for commit f45d17e677133d0820e49bdda78f52479f3d922d. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[ff95f16](https://github.com/MetaMask/metamask-mobile/commit/ff95f16cefc0d98530478d53d70a25fa4c77fb4d)
Co-authored-by: Matthew Walsh
---
.../UnifiedTransactionsView.tsx | 18 +-
app/selectors/transactionController.test.ts | 162 +++++++++++++++---
app/selectors/transactionController.ts | 45 ++++-
tests/smoke/swap/bridge-action-smoke.spec.ts | 8 +-
4 files changed, 198 insertions(+), 35 deletions(-)
diff --git a/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.tsx b/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.tsx
index 0f799418554a..c666fcd88e2d 100644
--- a/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.tsx
+++ b/app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.tsx
@@ -28,7 +28,10 @@ import {
selectEVMEnabledNetworks,
selectNonEVMEnabledNetworks,
} from '../../../selectors/networkEnablementController';
-import { selectLocalTransactions } from '../../../selectors/transactionController';
+import {
+ selectLocalTransactions,
+ selectRelatedChainIdsByTransactionId,
+} from '../../../selectors/transactionController';
import { baseStyles } from '../../../styles/common';
import { areAddressesEqual, isHardwareAccount } from '../../../util/address';
import { getBlockExplorerAddressUrl } from '../../../util/networks';
@@ -167,6 +170,10 @@ const UnifiedTransactionsView = ({
[enabledNonEVMNetworks],
);
+ const relatedChainIdsByTransactionId = useSelector(
+ selectRelatedChainIdsByTransactionId,
+ );
+
/** Drop confirmed rows not on currently enabled EVM chains (guards stale query pages). */
const allConfirmedForEnabledChains = useMemo(() => {
const chains = enabledEVMChainIds ?? [];
@@ -207,12 +214,18 @@ const UnifiedTransactionsView = ({
}
const { chainId: _chainId, txParams } = tx;
+
if (!enabledEvmSet.size) {
return false;
}
- if (!_chainId || !enabledEvmSet.has(String(_chainId).toLowerCase())) {
+
+ const relatedChainIds = relatedChainIdsByTransactionId.get(tx.id) ?? [
+ String(_chainId ?? '').toLowerCase(),
+ ];
+ if (!relatedChainIds.some((id) => enabledEvmSet.has(id))) {
return false;
}
+
const isBridgeTransaction = isBridgeHistoryForEvmTransaction(
tx,
bridgeHistoryValues,
@@ -283,6 +296,7 @@ const UnifiedTransactionsView = ({
enabledEVMChainIds,
enabledNonEVMChainIds,
bridgeHistory,
+ relatedChainIdsByTransactionId,
]);
const { data, nonEvmTransactionsForSelectedChain } = useMemo<{
diff --git a/app/selectors/transactionController.test.ts b/app/selectors/transactionController.test.ts
index 4684567699ce..05342181226e 100644
--- a/app/selectors/transactionController.test.ts
+++ b/app/selectors/transactionController.test.ts
@@ -6,6 +6,7 @@ import {
selectLastWithdrawTokenByType,
selectLocalTransactions,
selectNonReplacedTransactions,
+ selectRelatedChainIdsByTransactionId,
selectRequiredTransactionIds,
selectRequiredTransactionHashes,
selectRequiredTransactions,
@@ -27,6 +28,17 @@ jest.mock('./smartTransactionsController', () => ({
}) => state.pendingSmartTransactionsForGroup || [],
}));
+jest.mock('./multichainAccounts/accountTreeController', () => ({
+ selectSelectedAccountGroupEvmInternalAccount: (state: {
+ groupEvmAccount?: { address: string } | null;
+ }) => state.groupEvmAccount ?? null,
+}));
+
+jest.mock('./accountsController', () => ({
+ selectEvmAddress: (state: { fallbackEvmAddress?: string }) =>
+ state.fallbackEvmAddress,
+}));
+
describe('TransactionController Selectors', () => {
describe('selectTransactions', () => {
it('returns transactions from TransactionController state', () => {
@@ -181,35 +193,102 @@ describe('TransactionController Selectors', () => {
});
});
+ describe('selectRelatedChainIdsByTransactionId', () => {
+ const buildState = (transactions: unknown[]) =>
+ ({
+ engine: {
+ backgroundState: {
+ TransactionController: { transactions },
+ },
+ },
+ }) as unknown as RootState;
+
+ it('returns the transaction own chain id, lower-cased', () => {
+ const state = buildState([{ id: 'lone', chainId: '0xA4B1' }]);
+
+ expect(selectRelatedChainIdsByTransactionId(state).get('lone')).toEqual([
+ '0xa4b1',
+ ]);
+ });
+
+ it('includes metamaskPay.chainId for gasless MetaMask Pay parents', () => {
+ const state = buildState([
+ {
+ id: 'pay-parent',
+ chainId: '0xA4B1',
+ metamaskPay: { chainId: '0x1' },
+ },
+ ]);
+
+ expect(
+ selectRelatedChainIdsByTransactionId(state).get('pay-parent')?.sort(),
+ ).toEqual(['0x1', '0xa4b1']);
+ });
+
+ it('includes chain ids of required (child) transactions', () => {
+ const state = buildState([
+ {
+ id: 'parent',
+ chainId: '0xA4B1',
+ requiredTransactionIds: ['child-1', 'child-2'],
+ },
+ { id: 'child-1', chainId: '0x1' },
+ { id: 'child-2', chainId: '0xA' },
+ ]);
+
+ expect(
+ selectRelatedChainIdsByTransactionId(state).get('parent')?.sort(),
+ ).toEqual(['0x1', '0xa', '0xa4b1']);
+ });
+
+ it('dedupes overlapping chain ids', () => {
+ const state = buildState([
+ {
+ id: 'parent',
+ chainId: '0x1',
+ metamaskPay: { chainId: '0x1' },
+ requiredTransactionIds: ['child'],
+ },
+ { id: 'child', chainId: '0x1' },
+ ]);
+
+ expect(selectRelatedChainIdsByTransactionId(state).get('parent')).toEqual(
+ ['0x1'],
+ );
+ });
+
+ it('ignores required ids that point to missing children', () => {
+ const state = buildState([
+ { id: 'parent', chainId: '0x1', requiredTransactionIds: ['ghost'] },
+ ]);
+
+ expect(selectRelatedChainIdsByTransactionId(state).get('parent')).toEqual(
+ ['0x1'],
+ );
+ });
+ });
+
describe('selectLocalTransactions', () => {
- it('filters required child transactions before nonce dedupe', () => {
- const activeEvmAddress = '0x0000000000000000000000000000000000000001';
- const state = {
+ const evmAddress = '0x0000000000000000000000000000000000000001';
+
+ const buildLocalTxState = ({
+ groupEvmAccount = { address: evmAddress },
+ transactions,
+ }: {
+ groupEvmAccount?: { address: string } | null;
+ transactions?: unknown[];
+ } = {}) =>
+ ({
engine: {
backgroundState: {
- AccountsController: {
- internalAccounts: {
- selectedAccount: 'account-1',
- accounts: {
- 'account-1': {
- id: 'account-1',
- address: activeEvmAddress,
- type: 'eip155:eoa',
- },
- },
- },
- },
TransactionController: {
- transactions: [
+ transactions: transactions ?? [
{
id: 'child',
hash: '0xCHILD',
chainId: '0x1',
time: 200,
- txParams: {
- from: activeEvmAddress,
- nonce: '0x1',
- },
+ txParams: { from: evmAddress, nonce: '0x1' },
},
{
id: 'parent',
@@ -217,22 +296,53 @@ describe('TransactionController Selectors', () => {
requiredTransactionIds: ['child'],
time: 100,
type: TransactionType.predictDeposit,
- txParams: {
- from: activeEvmAddress,
- nonce: '0x1',
- },
+ txParams: { from: evmAddress, nonce: '0x1' },
},
],
},
},
},
+ groupEvmAccount,
pendingSmartTransactionsForGroup: [],
- } as unknown as RootState;
+ }) as unknown as RootState;
- expect(selectLocalTransactions(state)).toStrictEqual([
+ it('filters required child transactions before nonce dedupe', () => {
+ expect(selectLocalTransactions(buildLocalTxState())).toStrictEqual([
expect.objectContaining({ id: 'parent' }),
]);
});
+
+ it('returns no transactions when the selected group has no EVM account', () => {
+ expect(
+ selectLocalTransactions(buildLocalTxState({ groupEvmAccount: null })),
+ ).toStrictEqual([]);
+ });
+
+ it('excludes incoming transactions populated by the TransactionController incoming-transactions feature', () => {
+ const state = buildLocalTxState({
+ transactions: [
+ {
+ id: 'outgoing',
+ hash: '0xOUTGOING',
+ chainId: '0x1',
+ time: 200,
+ txParams: { from: evmAddress, nonce: '0x1' },
+ },
+ {
+ id: 'incoming-duplicate',
+ hash: '0xOUTGOING',
+ chainId: '0x1',
+ time: 100,
+ isTransfer: true,
+ txParams: { from: evmAddress },
+ },
+ ],
+ });
+
+ expect(selectLocalTransactions(state)).toStrictEqual([
+ expect.objectContaining({ id: 'outgoing' }),
+ ]);
+ });
});
describe('selectTransactionMetadataById', () => {
diff --git a/app/selectors/transactionController.ts b/app/selectors/transactionController.ts
index fc0ba1b6abdc..2c40968dac8f 100644
--- a/app/selectors/transactionController.ts
+++ b/app/selectors/transactionController.ts
@@ -6,6 +6,7 @@ import {
selectPendingSmartTransactionsForSelectedAccountGroup,
} from './smartTransactionsController';
import { selectEvmAddress } from './accountsController';
+import { selectSelectedAccountGroupEvmInternalAccount } from './multichainAccounts/accountTreeController';
import {
TransactionMeta,
TransactionType,
@@ -26,13 +27,14 @@ function dedupeTransactions(transactions: LocalTransaction[]) {
const seenTransactions = new Set();
return transactions.filter((transaction) => {
- const { chainId, txParams } = transaction;
- const { from, nonce, actionId } = txParams || {};
+ const { chainId, txParams, id, isTransfer } =
+ transaction as TransactionMeta;
+ const { from, nonce } = txParams || {};
const hash = 'hash' in transaction ? transaction.hash : undefined;
const isBridgeTransaction = transaction.type === TransactionType.bridge;
const hasNonce = nonce !== undefined && nonce !== null;
- if (!from) {
+ if (!from || isTransfer !== undefined) {
return false;
}
@@ -42,7 +44,7 @@ function dedupeTransactions(transactions: LocalTransaction[]) {
? `${dedupeKeyPrefix}-bridge-${hash.toLowerCase()}`
: hasNonce
? `${dedupeKeyPrefix}-${nonce}`
- : `${dedupeKeyPrefix}-${actionId}`;
+ : `${dedupeKeyPrefix}-${id}`;
// Keep only the first local transaction for each dedupe key
if (seenTransactions.has(dedupeKey)) {
@@ -115,6 +117,35 @@ export const selectRequiredTransactionHashes = createSelector(
),
);
+export const selectRelatedChainIdsByTransactionId = createSelector(
+ selectTransactionsStrict,
+ (transactions) => {
+ const transactionsById = new Map(
+ transactions.map((tx) => [tx.id, tx]),
+ );
+
+ return new Map(
+ transactions
+ .map((tx) => {
+ const childChainIds = (tx.requiredTransactionIds ?? []).map(
+ (childId) => transactionsById.get(childId)?.chainId,
+ );
+
+ const chainIds = [
+ tx.chainId,
+ tx.metamaskPay?.chainId,
+ ...childChainIds,
+ ]
+ .filter((chainId): chainId is Hex => Boolean(chainId))
+ .map((chainId) => chainId.toLowerCase());
+
+ return [tx.id, [...new Set(chainIds)]] satisfies [string, string[]];
+ })
+ .filter(([, chainIds]) => chainIds.length > 0),
+ );
+ },
+);
+
export const selectTransactions = createDeepEqualSelector(
selectTransactionsStrict,
(transactions) => transactions,
@@ -189,15 +220,19 @@ export const selectLocalTransactions = createDeepEqualSelector(
[
selectNonReplacedTransactions,
selectPendingSmartTransactionsForSelectedAccountGroup,
+ selectSelectedAccountGroupEvmInternalAccount,
selectEvmAddress,
selectRequiredTransactionIds,
],
(
nonReplacedTransactions,
pendingSmartTransactions,
- activeEvmAddress,
+ groupEvmAccount,
+ fallbackEvmAddress,
requiredTransactionIds,
) => {
+ const activeEvmAddress = groupEvmAccount?.address ?? fallbackEvmAddress;
+
const transactions = nonReplacedTransactions.filter((transaction) => {
if (requiredTransactionIds.has(transaction.id)) {
return false;
diff --git a/tests/smoke/swap/bridge-action-smoke.spec.ts b/tests/smoke/swap/bridge-action-smoke.spec.ts
index a70b00d452b6..f1e93701c27a 100644
--- a/tests/smoke/swap/bridge-action-smoke.spec.ts
+++ b/tests/smoke/swap/bridge-action-smoke.spec.ts
@@ -26,7 +26,11 @@ describe(SmokeSwap('Bridge functionality'), () => {
const sourceSymbol: string = 'ETH';
const chainId = '0x1';
const destChainId = '0x2105';
- const FIRST_ROW: number = 0;
+
+ // Row 0 is a stale STX-shaped entry; the confirmed bridge tx is on row 1.
+ // TODO: stop merging SmartTransactionsController state into
+ // selectLocalTransactions / selectSortedTransactions, then assert row 0.
+ const BRIDGE_ROW: number = 1;
await withFixtures(
{
@@ -103,7 +107,7 @@ describe(SmokeSwap('Bridge functionality'), () => {
);
await Assertions.expectElementToHaveText(
- ActivitiesView.transactionStatus(FIRST_ROW),
+ ActivitiesView.transactionStatus(BRIDGE_ROW),
ActivitiesViewSelectorsText.CONFIRM_TEXT,
{
timeout: 120000,
From e6d22b9e2c4cbbde0f9dde2334efdebca2528d1f Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Fri, 15 May 2026 10:54:33 +0000
Subject: [PATCH 05/66] [skip ci] Bump version number to 5006
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 42eb954f9880..d1e7b9fda931 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5004
+ versionCode 5006
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index cdd29ba5be7f..dfdff6106ff4 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5004
+ VERSION_NUMBER: 5006
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5004
+ FLASK_VERSION_NUMBER: 5006
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index a975fb3f6923..5c92bb42716f 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5004;
+ CURRENT_PROJECT_VERSION = 5006;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5004;
+ CURRENT_PROJECT_VERSION = 5006;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5004;
+ CURRENT_PROJECT_VERSION = 5006;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5004;
+ CURRENT_PROJECT_VERSION = 5006;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 619e4a4dee021f0f16c2924808f8d95c5a57fe79 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 13:27:02 +0200
Subject: [PATCH 06/66] chore(runway): cherry-pick chore: align carousel card
heights for accessibility cp-7.78.0 (#30233)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- chore: align carousel card heights for accessibility (#30201)
## **Description**
Use fluid heights on cards so large system text doesn’t clip.
Problem before: fixed heights + large accessibility text scaling =
content clipping inside cards.
Now: cards and skeletons are fully fluid (no height prop needed). Only
ViewMoreCard keeps a height, and only as a min-h directly inline in
WhatsHappeningSection, so it sits at the same height as a typical card
at normal font scale.
How it looks now with huge font:
And normal/default font size:
## **Changelog**
CHANGELOG entry: null
## **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**
- [ ] 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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Low risk UI/layout change to remove fixed card heights; main risk is
minor visual regression in carousel alignment across devices/font
scales.
>
> **Overview**
> Makes the What’s Happening carousel cards and loading skeletons
**fluid-height** by removing the fixed Tailwind height prop
(`twHeightClassName`) from `WhatsHappeningCard` and
`WhatsHappeningCardSkeleton`, preventing content clipping with large
accessibility text.
>
> Keeps the “View more” tile visually aligned by switching it to a
`min-h-[230px]` constraint, and updates the skeleton test to assert the
expected 2-line title and 3-line description placeholder configuration.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
00186c9d1d4989a0872504c881f194d0d1e9c673. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---------
Co-authored-by: Devin Stewart
<49423028+Bigshmow@users.noreply.github.com>
[a55aeb3](https://github.com/MetaMask/metamask-mobile/commit/a55aeb339c7aff8b21c6a114bd9e4c17ed244184)
Co-authored-by: António Regadas
Co-authored-by: Devin Stewart <49423028+Bigshmow@users.noreply.github.com>
---
.../WhatsHappening/WhatsHappeningSection.tsx | 12 ++-----
.../components/WhatsHappeningCard.tsx | 6 ++--
.../WhatsHappeningCardSkeleton.test.tsx | 31 ++++++++++---------
.../components/WhatsHappeningCardSkeleton.tsx | 11 ++-----
4 files changed, 23 insertions(+), 37 deletions(-)
diff --git a/app/components/UI/WhatsHappening/WhatsHappeningSection.tsx b/app/components/UI/WhatsHappening/WhatsHappeningSection.tsx
index c7b831089c82..b5afcb29c3fb 100644
--- a/app/components/UI/WhatsHappening/WhatsHappeningSection.tsx
+++ b/app/components/UI/WhatsHappening/WhatsHappeningSection.tsx
@@ -37,7 +37,7 @@ import { MetaMetricsEvents } from '../../../core/Analytics/MetaMetrics.events';
import { getWhatsHappeningEventProps } from './eventProperties';
const CARD_WIDTH = 280;
-const CARD_HEIGHT_CLASS = 'h-[230px]';
+const VIEW_MORE_MIN_HEIGHT_CLASS = 'min-h-[230px]';
const GAP = 12;
const SNAP_OFFSETS = Array.from(
@@ -163,12 +163,7 @@ const WhatsHappeningSection = forwardRef<
testID={WhatsHappeningSelectorsIDs.CAROUSEL}
>
{isLoading ? (
- SKELETON_KEYS.map((key) => (
-
- ))
+ SKELETON_KEYS.map((key) => )
) : (
<>
{items.map((item: WhatsHappeningItem, index: number) => (
@@ -178,12 +173,11 @@ const WhatsHappeningSection = forwardRef<
cardIndex={index}
source={source}
onPress={() => handleCardPress(index)}
- twHeightClassName={CARD_HEIGHT_CLASS}
/>
))}
>
diff --git a/app/components/UI/WhatsHappening/components/WhatsHappeningCard.tsx b/app/components/UI/WhatsHappening/components/WhatsHappeningCard.tsx
index af269d0ead2c..c645f48827c7 100644
--- a/app/components/UI/WhatsHappening/components/WhatsHappeningCard.tsx
+++ b/app/components/UI/WhatsHappening/components/WhatsHappeningCard.tsx
@@ -32,8 +32,6 @@ interface WhatsHappeningCardProps {
cardIndex: number;
source: WhatsHappeningSourceValue;
onPress?: (item: WhatsHappeningItem) => void;
- /** Tailwind height class so the carousel can keep all cards visually aligned. */
- twHeightClassName?: string;
}
const MAX_VISIBLE_ASSET_ICONS = 3;
@@ -43,7 +41,6 @@ const WhatsHappeningCard: React.FC = ({
cardIndex,
source,
onPress,
- twHeightClassName = '',
}) => {
const tw = useTailwind();
const formattedDate = useMemo(
@@ -93,7 +90,7 @@ const WhatsHappeningCard: React.FC = ({
onPress={handlePress}
activeOpacity={0.7}
style={tw.style(
- `w-[280px] ${twHeightClassName} rounded-2xl bg-background-muted overflow-hidden p-4 justify-between gap-3`,
+ 'w-[280px] rounded-2xl bg-background-muted overflow-hidden p-4',
)}
>
@@ -134,6 +131,7 @@ const WhatsHappeningCard: React.FC = ({
alignItems={BoxAlignItems.Center}
justifyContent={BoxJustifyContent.Between}
gap={2}
+ twClassName="mt-3"
>
{assetLabel && (
({
WhatsHappeningSkeletonShimmer: ({
children,
}: {
children: React.ReactNode;
}) => children,
- WhatsHappeningSkeletonLineStack: () => null,
+ WhatsHappeningSkeletonLineStack: (props: { lineClassNames: string[] }) => {
+ mockLineStack(props);
+ return null;
+ },
}));
describe('WhatsHappeningCardSkeleton', () => {
+ beforeEach(() => {
+ mockLineStack.mockClear();
+ });
+
it('renders without crashing', () => {
renderWithProvider();
expect(screen.toJSON()).not.toBeNull();
});
- it('applies the twHeightClassName when provided', () => {
- renderWithProvider(
- ,
- );
- expect(screen.toJSON()).not.toBeNull();
- });
+ it('renders a two-line title placeholder and a three-line description placeholder', () => {
+ renderWithProvider();
- it('renders correctly without twHeightClassName (default empty string)', () => {
- const { toJSON: withoutProp } = renderWithProvider(
- ,
- );
- const { toJSON: withEmptyProp } = renderWithProvider(
- ,
- );
- expect(withoutProp()).toEqual(withEmptyProp());
+ expect(mockLineStack).toHaveBeenCalledTimes(2);
+ const [titleCall, descriptionCall] = mockLineStack.mock.calls;
+ expect(titleCall[0].lineClassNames).toHaveLength(2);
+ expect(descriptionCall[0].lineClassNames).toHaveLength(3);
});
});
diff --git a/app/components/UI/WhatsHappening/components/WhatsHappeningCardSkeleton.tsx b/app/components/UI/WhatsHappening/components/WhatsHappeningCardSkeleton.tsx
index 495535543685..742cb81c9479 100644
--- a/app/components/UI/WhatsHappening/components/WhatsHappeningCardSkeleton.tsx
+++ b/app/components/UI/WhatsHappening/components/WhatsHappeningCardSkeleton.tsx
@@ -6,20 +6,13 @@ import {
WhatsHappeningSkeletonShimmer,
} from './whatsHappeningSkeletonShared';
-interface WhatsHappeningCardSkeletonProps {
- /** Tailwind height class so skeletons match real cards in the carousel. */
- twHeightClassName?: string;
-}
-
-const WhatsHappeningCardSkeleton: React.FC = ({
- twHeightClassName = '',
-}) => {
+const WhatsHappeningCardSkeleton: React.FC = () => {
const tw = useTailwind();
return (
From f0e88a89949e858fc00bf5b0c2d87353462cd41e Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Fri, 15 May 2026 11:28:57 +0000
Subject: [PATCH 07/66] [skip ci] Bump version number to 5007
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index d1e7b9fda931..cf50cf9d29d4 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5006
+ versionCode 5007
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index dfdff6106ff4..a5750c260fd8 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5006
+ VERSION_NUMBER: 5007
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5006
+ FLASK_VERSION_NUMBER: 5007
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 5c92bb42716f..751e6f8ed095 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5006;
+ CURRENT_PROJECT_VERSION = 5007;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5006;
+ CURRENT_PROJECT_VERSION = 5007;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5006;
+ CURRENT_PROJECT_VERSION = 5007;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5006;
+ CURRENT_PROJECT_VERSION = 5007;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From c942f8a87bdeb402235a86cfdcec55af3f08c9c2 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 17:56:26 +0200
Subject: [PATCH 08/66] chore(runway): cherry-pick feat: cp-7.78.0 support
deposit-wallet polymarket withdraw (#30250)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- feat: cp-7.78.0 support deposit-wallet polymarket withdraw (#29953)
## **Description**
Adopts the new Polymarket deposit-wallet support landed in
[@metamask/transaction-pay-controller@22.5.0](https://github.com/MetaMask/core/pull/8754)
so Polymarket users whose pUSD lives in a deposit wallet (a per-user
batch contract on Polygon) can withdraw cross-chain through MetaMask
Pay.
Highlights:
- Lets Polymarket deposit-wallet users withdraw cross-chain through
MetaMask Pay.
- Gated behind a new remote feature flag, with the existing "withdraw
unavailable" sheet preserved when off.
- Polishes Predict withdraw activity rendering.
## **Changelog**
CHANGELOG entry: null
## **Related issues**
## **Manual testing steps**
```gherkin
Feature: Polymarket deposit-wallet withdraw
Scenario: deposit-wallet user with the flag on
Given enableDepositWalletWithdraw is on
And the user has a Polymarket deposit wallet with pUSD balance on Polygon
When the user taps Withdraw on the Predict balance
Then the standard Pay confirmation opens
And confirming submits via the Polymarket strategy with no Polygon gas
Scenario: deposit-wallet user with the flag off
Given enableDepositWalletWithdraw is off
When the user taps Withdraw on the Predict balance
Then the existing "Withdraw unavailable" sheet is shown
```
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- [ ] I've tested with a power user scenario
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
## **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]
> **Medium Risk**
> Changes withdrawal behavior and MetaMask Pay transaction configuration
for Polymarket `predictWithdraw`, including new controller callbacks and
retry logic; mistakes could impact withdraw routing/fees for affected
users. Gated by a remote feature flag, limiting blast radius.
>
> **Overview**
> Enables Polymarket *deposit-wallet* users to run `predictWithdraw`
through MetaMask Pay when the new
`confirmations_pay_extended.enableDepositWalletWithdraw` flag is on;
when off, the existing “withdraw unavailable” handling remains.
>
> Updates Predict/Pay plumbing for deposit-wallet withdraws:
`PredictController.prepareWithdraw` now omits `gasFeeToken` for
deposit-wallet accounts, `useTransactionPayPostQuote` skips `refundTo`
and marks `isPolymarketDepositWallet`, and Transaction Pay
initialization wires new Polymarket callbacks that can derive
deposit-wallet addresses and submit deposit-wallet batches (with “wallet
busy” retries + keyring signing support).
>
> Polishes confirmations activity rendering for `predictWithdraw` by
adding a dedicated `predict_withdraw` title and treating it as a
receive-summary type using the source token/network metadata. Tests are
added/updated accordingly, and `@metamask/transaction-pay-controller` is
bumped to `22.5.0`.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
054697c21ec5e65a5069e6199f6e7ef902e4649a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[4997055](https://github.com/MetaMask/metamask-mobile/commit/4997055ba34a24c404b34d249d48474304656bfb)
Co-authored-by: Matthew Walsh
---
.../PredictBalance/PredictBalance.test.tsx | 56 +++++
.../PredictBalance/PredictBalance.tsx | 13 +-
.../controllers/PredictController.test.ts | 53 +++++
.../Predict/controllers/PredictController.ts | 13 +-
.../receive-summary-line.test.tsx | 34 +++
.../receive-summary-line.tsx | 32 ++-
.../source-hash-summary-line.test.tsx | 37 +++-
.../source-hash-summary-line.tsx | 48 ++--
.../transaction-details-summary.tsx | 1 +
.../transaction-details.test.tsx | 5 +-
.../transaction-details.tsx | 1 +
.../custom-amount-info.test.tsx | 6 +
.../pay/useTransactionPayPostQuote.test.ts | 95 ++++++++
.../hooks/pay/useTransactionPayPostQuote.ts | 25 ++-
.../transaction-controller-init.test.ts | 3 +
.../polymarket-callbacks.test.ts | 205 ++++++++++++++++++
.../polymarket-callbacks.ts | 101 +++++++++
.../transaction-pay-controller-init.test.ts | 18 ++
.../transaction-pay-controller-init.ts | 2 +
.../transaction-controller-messenger.ts | 6 +
.../transaction-pay-controller-messenger.ts | 12 +-
.../confirmations/index.test.ts | 21 ++
.../confirmations/index.ts | 17 +-
locales/languages/en.json | 1 +
package.json | 2 +-
yarn.lock | 10 +-
26 files changed, 774 insertions(+), 43 deletions(-)
create mode 100644 app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.test.ts
create mode 100644 app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.ts
diff --git a/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx b/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx
index 6729e67f2dea..d72ba723e550 100644
--- a/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx
+++ b/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx
@@ -76,6 +76,25 @@ const initialState = {
},
};
+function stateWithDepositWalletWithdrawEnabled(enabled: boolean) {
+ return {
+ engine: {
+ backgroundState: {
+ ...initialState.engine.backgroundState,
+ RemoteFeatureFlagController: {
+ ...backgroundState.RemoteFeatureFlagController,
+ remoteFeatureFlags: {
+ ...backgroundState.RemoteFeatureFlagController?.remoteFeatureFlags,
+ confirmations_pay_extended: {
+ enableDepositWalletWithdraw: enabled,
+ },
+ },
+ },
+ },
+ },
+ };
+}
+
describe('PredictBalance', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -387,6 +406,43 @@ describe('PredictBalance', () => {
expect(mockExecuteGuardedAction).not.toHaveBeenCalled();
});
+ it('calls withdraw for Deposit Wallet users when enableDepositWalletWithdraw flag is on', () => {
+ // Arrange
+ const mockWithdraw = jest.fn();
+ const mockOnDepositWalletWithdrawPress = jest.fn();
+ mockUsePredictBalance.mockReturnValue({
+ data: 100,
+ isLoading: false,
+ });
+ mockUsePredictAccountState.mockReturnValue({
+ data: {
+ address: '0x2222222222222222222222222222222222222222',
+ isDeployed: true,
+ walletType: 'deposit-wallet',
+ },
+ isLoading: false,
+ });
+ mockUsePredictWithdraw.mockReturnValue({
+ withdraw: mockWithdraw,
+ });
+
+ // Act
+ const { getByText } = renderWithProvider(
+ ,
+ {
+ state: stateWithDepositWalletWithdrawEnabled(true),
+ },
+ );
+ const withdrawButton = getByText(/Withdraw/i);
+ fireEvent.press(withdrawButton);
+
+ // Assert
+ expect(mockWithdraw).toHaveBeenCalledTimes(1);
+ expect(mockOnDepositWalletWithdrawPress).not.toHaveBeenCalled();
+ });
+
it('calls temporary unavailable handler instead of withdrawing for Deposit Wallet users', () => {
// Arrange
const mockWithdraw = jest.fn();
diff --git a/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx b/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx
index bc68f1121a72..bbb71ca05d26 100644
--- a/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx
+++ b/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx
@@ -41,6 +41,7 @@ import { PredictNavigationParamList } from '../../types/navigation';
import { usePredictWithdraw } from '../../hooks/usePredictWithdraw';
import { usePredictAccountState } from '../../hooks/usePredictAccountState';
import { PredictEventValues } from '../../constants/eventNames';
+import { selectMetaMaskPayFlags } from '../../../../../selectors/featureFlagController/confirmations';
import { PREDICT_BALANCE_TEST_IDS } from './PredictBalance.testIds';
// This is a temporary component that will be removed when the deposit flow is fully implemented
@@ -55,6 +56,7 @@ const PredictBalance: React.FC = ({
}) => {
const tw = useTailwind();
const privacyMode = useSelector(selectPrivacyMode);
+ const { enableDepositWalletWithdraw } = useSelector(selectMetaMaskPayFlags);
const navigation =
useNavigation>();
@@ -103,15 +105,18 @@ const PredictBalance: React.FC = ({
return;
}
- // Temporary Deposit Wallet migration guard. Remove this branch and sheet
- // once Deposit Wallet withdrawals are implemented.
- if (walletType === 'deposit-wallet') {
+ if (walletType === 'deposit-wallet' && !enableDepositWalletWithdraw) {
onDepositWalletWithdrawPress?.();
return;
}
withdraw();
- }, [onDepositWalletWithdrawPress, walletType, withdraw]);
+ }, [
+ enableDepositWalletWithdraw,
+ onDepositWalletWithdrawPress,
+ walletType,
+ withdraw,
+ ]);
if (isLoading) {
return (
diff --git a/app/components/UI/Predict/controllers/PredictController.test.ts b/app/components/UI/Predict/controllers/PredictController.test.ts
index e26ec86de636..2c3db2effb91 100644
--- a/app/components/UI/Predict/controllers/PredictController.test.ts
+++ b/app/components/UI/Predict/controllers/PredictController.test.ts
@@ -288,6 +288,11 @@ describe('PredictController', () => {
syncDepositWalletBalanceAllowanceForDepositTransaction: jest.fn(),
} as unknown as jest.Mocked;
+ mockPolymarketProvider.getAccountState.mockResolvedValue({
+ address: '0xProxyAddress' as `0x${string}`,
+ isDeployed: true,
+ walletType: 'safe' as const,
+ });
mockPolymarketProvider.beforePublishDepositWalletDeposit.mockResolvedValue(
true,
);
@@ -6076,6 +6081,54 @@ describe('PredictController', () => {
});
});
+ it('sets gasFeeToken when account walletType is not deposit-wallet', async () => {
+ mockPolymarketProvider.prepareWithdraw.mockResolvedValue(
+ mockWithdrawResponse,
+ );
+ mockPolymarketProvider.getAccountState.mockResolvedValue({
+ address: '0xProxyAddress' as `0x${string}`,
+ isDeployed: true,
+ walletType: 'safe' as const,
+ });
+ (addTransactionBatch as jest.Mock).mockResolvedValue({
+ batchId: 'batch-safe',
+ });
+
+ await withController(async ({ controller }) => {
+ await controller.prepareWithdraw({});
+
+ expect(addTransactionBatch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ gasFeeToken: MATIC_CONTRACTS_V2.collateral,
+ }),
+ );
+ });
+ });
+
+ it('omits gasFeeToken when account walletType is deposit-wallet', async () => {
+ mockPolymarketProvider.prepareWithdraw.mockResolvedValue(
+ mockWithdrawResponse,
+ );
+ mockPolymarketProvider.getAccountState.mockResolvedValue({
+ address: '0xDepositWalletAddress' as `0x${string}`,
+ isDeployed: true,
+ walletType: 'deposit-wallet' as const,
+ });
+ (addTransactionBatch as jest.Mock).mockResolvedValue({
+ batchId: 'batch-deposit',
+ });
+
+ await withController(async ({ controller }) => {
+ await controller.prepareWithdraw({});
+
+ expect(addTransactionBatch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ gasFeeToken: undefined,
+ }),
+ );
+ });
+ });
+
it('update transaction ID when batch ID is returned', async () => {
const mockBatchId = 'tx-batch-update';
diff --git a/app/components/UI/Predict/controllers/PredictController.ts b/app/components/UI/Predict/controllers/PredictController.ts
index dff17733a080..cb7c140a2fcd 100644
--- a/app/components/UI/Predict/controllers/PredictController.ts
+++ b/app/components/UI/Predict/controllers/PredictController.ts
@@ -2595,6 +2595,16 @@ export class PredictController extends BaseController<
signer,
});
+ const accountState = await provider.getAccountState({
+ ownerAddress: signer.address,
+ });
+
+ const isDepositWallet = accountState.walletType === 'deposit-wallet';
+
+ const gasFeeToken = isDepositWallet
+ ? undefined
+ : (MATIC_CONTRACTS_V2.collateral as Hex);
+
this.update((state) => {
state.withdrawTransaction = {
chainId: hexToNumber(chainId),
@@ -2616,9 +2626,8 @@ export class PredictController extends BaseController<
disableHook: true,
disableSequential: true,
requireApproval: true,
- // Temporarily breaking abstraction, can instead be abstracted via provider.
- gasFeeToken: MATIC_CONTRACTS_V2.collateral as Hex,
transactions: [transaction],
+ gasFeeToken,
});
this.update((state) => {
diff --git a/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.test.tsx b/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.test.tsx
index 129a93dfa891..83d293d43855 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.test.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.test.tsx
@@ -13,6 +13,7 @@ import { selectBridgeHistoryForAccount } from '../../../../../../selectors/bridg
import { useBridgeTxHistoryData } from '../../../../../../util/bridge/hooks/useBridgeTxHistoryData';
import { useTokenAmount } from '../../../hooks/useTokenAmount';
import { useTransactionDetails } from '../../../hooks/activity/useTransactionDetails';
+import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance';
import { ReceiveSummaryLine } from './receive-summary-line';
jest.mock('../../../../../UI/Bridge/hooks/useMultichainBlockExplorerTxUrl');
@@ -21,6 +22,7 @@ jest.mock('../../../../../../selectors/bridgeStatusController');
jest.mock('../../../../../../util/bridge/hooks/useBridgeTxHistoryData');
jest.mock('../../../hooks/useTokenAmount');
jest.mock('../../../hooks/activity/useTransactionDetails');
+jest.mock('../../../hooks/tokens/useTokenWithBalance');
jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
@@ -161,4 +163,36 @@ describe('ReceiveSummaryLine', () => {
),
).toBeDefined();
});
+
+ it('renders predict withdraw title using source token symbol and source network', () => {
+ useNetworkNameMock.mockImplementation((chainId?: Hex) =>
+ chainId === '0x1' ? 'Ethereum' : 'Polygon',
+ );
+ jest
+ .mocked(useTokenWithBalance)
+ .mockReturnValue({ symbol: 'USDC' } as ReturnType<
+ typeof useTokenWithBalance
+ >);
+
+ const { getByText } = render({
+ id: 'tx-id',
+ chainId: '0x89' as Hex,
+ hash: '0x123',
+ submittedTime: 1755719285723,
+ type: TransactionType.predictWithdraw,
+ metamaskPay: {
+ chainId: '0x1' as Hex,
+ tokenAddress: '0xabc' as Hex,
+ },
+ } as Partial);
+
+ expect(
+ getByText(
+ strings('transaction_details.summary_title.bridge_receive', {
+ targetSymbol: 'USDC',
+ targetChain: 'Ethereum',
+ }),
+ ),
+ ).toBeDefined();
+ });
});
diff --git a/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.tsx b/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.tsx
index 8aacc51634cb..11a120014611 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.tsx
@@ -10,6 +10,7 @@ import { hasTransactionType } from '../../../utils/transaction';
import { useNetworkName } from '../../../hooks/useNetworkName';
import { POLYGON_PUSD } from '../../../constants/predict';
import { TransactionSummaryLine } from './transaction-summary-line';
+import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance';
const HYPERLIQUID_EXPLORER_URL = 'https://app.hyperliquid.xyz/explorer/tx';
const HYPERLIQUID_EXPLORER_NAME = 'Hyperliquid';
@@ -19,7 +20,10 @@ export function ReceiveSummaryLine({
}: {
transactionMeta: TransactionMeta;
}) {
- const { chainId } = transactionMeta;
+ const { chainId: targetChainId, metamaskPay } = transactionMeta;
+ const sourceChainId = metamaskPay?.chainId;
+ const sourceTokenAddress = metamaskPay?.tokenAddress;
+
const isPerpsDeposit = hasTransactionType(transactionMeta, [
TransactionType.perpsDeposit,
]);
@@ -28,25 +32,39 @@ export function ReceiveSummaryLine({
TransactionType.predictDeposit,
]);
- const networkName = useNetworkName(chainId);
+ const isPredictWithdraw = hasTransactionType(transactionMeta, [
+ TransactionType.predictWithdraw,
+ ]);
+
+ const targetNetworkName = useNetworkName(targetChainId);
+ const sourceNetworkName = useNetworkName(sourceChainId ?? '0x0');
+
+ const sourceToken = useTokenWithBalance(
+ sourceTokenAddress ?? '0x0',
+ sourceChainId ?? '0x0',
+ );
let targetSymbol = 'mUSD';
- let targetNetworkName: string | undefined = networkName;
- let receiveChainId: Hex = chainId;
+ let finalTargetNetworkName: string | undefined = targetNetworkName;
+ let receiveChainId: Hex = targetChainId;
if (isPerpsDeposit) {
targetSymbol = 'USDC';
- targetNetworkName = 'Hyperliquid';
+ finalTargetNetworkName = 'Hyperliquid';
receiveChainId = CHAIN_IDS.ARBITRUM;
} else if (isPredictDeposit) {
targetSymbol = POLYGON_PUSD.symbol;
+ } else if (isPredictWithdraw) {
+ targetSymbol = sourceToken?.symbol ?? 'Unknown';
+ finalTargetNetworkName = sourceNetworkName;
+ receiveChainId = sourceChainId ?? '0x0';
}
const title =
- targetSymbol && targetNetworkName
+ targetSymbol && finalTargetNetworkName
? strings('transaction_details.summary_title.bridge_receive', {
targetSymbol,
- targetChain: targetNetworkName,
+ targetChain: finalTargetNetworkName,
})
: strings('transaction_details.summary_title.bridge_receive_loading');
diff --git a/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.test.tsx b/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.test.tsx
index 696513c3528d..a9d215105b9a 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.test.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.test.tsx
@@ -1,6 +1,9 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';
-import { TransactionMeta } from '@metamask/transaction-controller';
+import {
+ TransactionMeta,
+ TransactionType,
+} from '@metamask/transaction-controller';
import { Hex } from '@metamask/utils';
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import { strings } from '../../../../../../../locales/i18n';
@@ -31,11 +34,11 @@ jest.mock('@react-navigation/native', () => ({
}),
}));
-function render() {
+function render(parentTransaction?: Partial) {
return renderWithProvider(
,
@@ -118,4 +121,30 @@ describe('SourceHashSummaryLine', () => {
},
});
});
+
+ it('renders predict-withdraw title with pUSD and target network', () => {
+ useNetworkNameMock.mockImplementation((chainId?: Hex) =>
+ chainId === '0x89' ? 'Polygon' : 'Ethereum',
+ );
+
+ const { getByText } = render({
+ id: 'parent-id',
+ chainId: '0x89' as Hex,
+ submittedTime: 1755719285723,
+ type: TransactionType.predictWithdraw,
+ metamaskPay: {
+ tokenAddress: '0x123' as Hex,
+ chainId: '0x1' as Hex,
+ },
+ } as Partial);
+
+ expect(
+ getByText(
+ strings('transaction_details.summary_title.predict_withdraw', {
+ sourceSymbol: 'pUSD',
+ sourceChain: 'Polygon',
+ }),
+ ),
+ ).toBeDefined();
+ });
});
diff --git a/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.tsx b/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.tsx
index 87733f43eabc..e5e62fe251ba 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.tsx
@@ -1,10 +1,15 @@
import React from 'react';
-import { TransactionMeta } from '@metamask/transaction-controller';
+import {
+ TransactionMeta,
+ TransactionType,
+} from '@metamask/transaction-controller';
import { Hex } from '@metamask/utils';
import { strings } from '../../../../../../../locales/i18n';
import { useNetworkName } from '../../../hooks/useNetworkName';
import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance';
import { TransactionSummaryLine } from './transaction-summary-line';
+import { hasTransactionType } from '../../../utils/transaction';
+import { POLYGON_PUSD } from '../../../constants/predict';
export function SourceHashSummaryLine({
parentTransaction,
@@ -13,24 +18,39 @@ export function SourceHashSummaryLine({
parentTransaction: TransactionMeta;
sourceHash: Hex;
}) {
- const tokenAddress = parentTransaction.metamaskPay?.tokenAddress;
- const tokenChainId = parentTransaction.metamaskPay?.chainId;
+ const { chainId: targetChainId, metamaskPay } = parentTransaction;
+ const sourceTokenAddress = metamaskPay?.tokenAddress;
+ const sourceTokenChainId = metamaskPay?.chainId;
const sourceToken = useTokenWithBalance(
- tokenAddress ?? '0x0',
- tokenChainId ?? '0x0',
+ sourceTokenAddress ?? '0x0',
+ sourceTokenChainId ?? '0x0',
);
- const sourceNetworkName = useNetworkName(tokenChainId);
- const chainId = tokenChainId ?? parentTransaction.chainId;
+ const sourceNetworkName = useNetworkName(sourceTokenChainId);
+ const targetNetworkName = useNetworkName(targetChainId);
- const title =
- sourceToken?.symbol && sourceNetworkName
- ? strings('transaction_details.summary_title.bridge_send', {
- sourceSymbol: sourceToken.symbol,
- sourceChain: sourceNetworkName,
- })
- : strings('transaction_details.summary_title.bridge_send_loading');
+ const isPredictWithdraw = hasTransactionType(parentTransaction, [
+ TransactionType.predictWithdraw,
+ ]);
+
+ const chainId = isPredictWithdraw ? targetChainId : sourceTokenChainId;
+
+ let title = strings('transaction_details.summary_title.bridge_send_loading');
+
+ if (sourceToken?.symbol && sourceNetworkName) {
+ title = strings('transaction_details.summary_title.bridge_send', {
+ sourceSymbol: sourceToken.symbol,
+ sourceChain: sourceNetworkName,
+ });
+
+ if (isPredictWithdraw) {
+ title = strings('transaction_details.summary_title.predict_withdraw', {
+ sourceSymbol: POLYGON_PUSD.symbol,
+ sourceChain: targetNetworkName,
+ });
+ }
+ }
return (
;
diff --git a/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.test.tsx b/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.test.tsx
index 1847378f80b0..0941e6d8583d 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.test.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.test.tsx
@@ -360,12 +360,13 @@ describe('TransactionDetails', () => {
TransactionType.moneyAccountWithdraw,
TransactionType.perpsDeposit,
TransactionType.predictDeposit,
+ TransactionType.predictWithdraw,
])('includes %s', (type) => {
expect(SUMMARY_SECTION_TYPES).toContain(type);
});
- it('contains exactly 6 transaction types', () => {
- expect(SUMMARY_SECTION_TYPES).toHaveLength(6);
+ it('contains exactly 7 transaction types', () => {
+ expect(SUMMARY_SECTION_TYPES).toHaveLength(7);
});
});
});
diff --git a/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.tsx b/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.tsx
index 4fbe94fef1e2..1901102cbc70 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.tsx
@@ -32,6 +32,7 @@ export const SUMMARY_SECTION_TYPES = [
TransactionType.moneyAccountWithdraw,
TransactionType.perpsDeposit,
TransactionType.predictDeposit,
+ TransactionType.predictWithdraw,
];
export function TransactionDetails() {
diff --git a/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx b/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx
index 33ac381a6050..f933a750ce38 100644
--- a/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx
+++ b/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx
@@ -54,6 +54,12 @@ jest.mock('../../../context/alert-system-context');
jest.mock('../../../hooks/transactions/useTransactionCustomAmountAlerts');
jest.mock('../../../hooks/pay/useTransactionPayMetrics');
jest.mock('../../../hooks/send/useAccountTokens');
+jest.mock('../../../../../UI/Predict/hooks/usePredictAccountState', () => ({
+ usePredictAccountState: () => ({
+ data: undefined,
+ isLoading: false,
+ }),
+}));
jest.mock('../../../hooks/pay/useTransactionPayAvailableTokens');
jest.mock('../../../hooks/pay/useTransactionPayData');
jest.mock('../../../hooks/pay/useTransactionPayHasSourceAmount');
diff --git a/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.test.ts b/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.test.ts
index 03f6313da56d..a740b930b217 100644
--- a/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.test.ts
+++ b/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.test.ts
@@ -6,6 +6,7 @@ import { useTransactionMetadataRequest } from '../transactions/useTransactionMet
import { useTransactionPayWithdraw } from './useTransactionPayWithdraw';
import Engine from '../../../../../core/Engine';
import { computeProxyAddress } from '../../../../UI/Predict/providers/polymarket/safe/utils';
+import { usePredictAccountState } from '../../../../UI/Predict/hooks/usePredictAccountState';
jest.mock('../transactions/useTransactionMetadataRequest');
jest.mock('./useTransactionPayWithdraw');
@@ -19,6 +20,7 @@ jest.mock('../../../../../core/Engine', () => ({
jest.mock('../../../../UI/Predict/providers/polymarket/safe/utils', () => ({
computeProxyAddress: jest.fn(),
}));
+jest.mock('../../../../UI/Predict/hooks/usePredictAccountState');
const TRANSACTION_ID_MOCK = 'transaction-123';
const FROM_MOCK = '0x1234567890123456789012345678901234567890' as Hex;
@@ -32,11 +34,24 @@ describe('useTransactionPayPostQuote', () => {
const setTransactionConfigMock = jest.mocked(
Engine.context.TransactionPayController.setTransactionConfig,
);
+ const usePredictAccountStateMock = jest.mocked(usePredictAccountState);
const computeProxyAddressMock = jest.mocked(computeProxyAddress);
+ function mockAccountState(walletType: 'safe' | 'deposit-wallet'): void {
+ usePredictAccountStateMock.mockReturnValue({
+ data: {
+ address: '0xProxyAddress',
+ isDeployed: true,
+ walletType,
+ },
+ isLoading: false,
+ } as never);
+ }
+
beforeEach(() => {
jest.clearAllMocks();
computeProxyAddressMock.mockReturnValue(PROXY_ADDRESS_MOCK);
+ mockAccountState('safe');
useTransactionPayWithdrawMock.mockReturnValue({
isWithdraw: false,
canSelectWithdrawToken: false,
@@ -263,4 +278,84 @@ describe('useTransactionPayPostQuote', () => {
expect(config.isHyperliquidSource).toBeUndefined();
expect(computeProxyAddressMock).not.toHaveBeenCalled();
});
+
+ describe('Polymarket deposit-wallet predictWithdraw', () => {
+ beforeEach(() => {
+ setTransactionConfigMock.mockReset();
+ useTransactionMetadataRequestMock.mockReturnValue({
+ id: TRANSACTION_ID_MOCK,
+ txParams: { from: FROM_MOCK },
+ type: TransactionType.predictWithdraw,
+ } as never);
+ useTransactionPayWithdrawMock.mockReturnValue({
+ isWithdraw: true,
+ canSelectWithdrawToken: true,
+ });
+ });
+
+ it('flags transaction and skips refundTo when walletType is deposit-wallet', () => {
+ mockAccountState('deposit-wallet');
+
+ renderHook(() => useTransactionPayPostQuote());
+
+ expect(setTransactionConfigMock).toHaveBeenCalledTimes(1);
+
+ const callback = setTransactionConfigMock.mock.calls[0][1];
+ const config = {} as {
+ isPostQuote?: boolean;
+ isPolymarketDepositWallet?: boolean;
+ refundTo?: Hex;
+ };
+ callback(config);
+
+ expect(config.isPostQuote).toBe(true);
+ expect(config.isPolymarketDepositWallet).toBe(true);
+ expect(config.refundTo).toBeUndefined();
+ expect(computeProxyAddressMock).not.toHaveBeenCalled();
+ });
+
+ it('does not set deposit-wallet flag when walletType is safe', () => {
+ mockAccountState('safe');
+
+ renderHook(() => useTransactionPayPostQuote());
+
+ expect(setTransactionConfigMock).toHaveBeenCalledTimes(1);
+
+ const callback = setTransactionConfigMock.mock.calls[0][1];
+ const config = {} as {
+ isPostQuote?: boolean;
+ isPolymarketDepositWallet?: boolean;
+ refundTo?: Hex;
+ };
+ callback(config);
+
+ expect(config.isPolymarketDepositWallet).toBeUndefined();
+ });
+
+ it('defers setTransactionConfig until predict account state resolves', () => {
+ usePredictAccountStateMock.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ } as never);
+
+ renderHook(() => useTransactionPayPostQuote());
+
+ expect(setTransactionConfigMock).not.toHaveBeenCalled();
+ });
+
+ it('does not resolve account state for non-predictWithdraw flows', () => {
+ useTransactionMetadataRequestMock.mockReturnValue({
+ id: TRANSACTION_ID_MOCK,
+ txParams: { from: FROM_MOCK },
+ type: TransactionType.perpsWithdraw,
+ } as never);
+
+ renderHook(() => useTransactionPayPostQuote());
+
+ expect(usePredictAccountStateMock).toHaveBeenCalledWith({
+ enabled: false,
+ });
+ expect(setTransactionConfigMock).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.ts b/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.ts
index fd8a9ebe0f62..5eb34b2c678e 100644
--- a/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.ts
+++ b/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.ts
@@ -6,6 +6,7 @@ import { useTransactionPayWithdraw } from './useTransactionPayWithdraw';
import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest';
import { computeProxyAddress } from '../../../../UI/Predict/providers/polymarket/safe/utils';
import { hasTransactionType } from '../../utils/transaction';
+import { usePredictAccountState } from '../../../../UI/Predict/hooks/usePredictAccountState';
const log = createProjectLogger('transaction-pay-post-quote');
@@ -33,6 +34,16 @@ export function useTransactionPayPostQuote(): void {
const isMoneyAccountWithdraw = hasTransactionType(transactionMeta, [
TransactionType.moneyAccountWithdraw,
]);
+ const isPredictWithdraw = hasTransactionType(transactionMeta, [
+ TransactionType.predictWithdraw,
+ ]);
+
+ const { data: accountState } = usePredictAccountState({
+ enabled: isPredictWithdraw,
+ });
+
+ const isDepositWalletWithdraw =
+ isPredictWithdraw && accountState?.walletType === 'deposit-wallet';
useEffect(() => {
if (
@@ -43,6 +54,10 @@ export function useTransactionPayPostQuote(): void {
return;
}
+ if (isPredictWithdraw && !accountState) {
+ return;
+ }
+
try {
const { TransactionPayController } = Engine.context;
const from = transactionMeta?.txParams?.from as Hex | undefined;
@@ -52,7 +67,7 @@ export function useTransactionPayPostQuote(): void {
// on the user's address directly (HyperCore -> Relay for perps; vault
// teller -> user for money account).
const refundTo =
- isPerpsWithdraw || isMoneyAccountWithdraw
+ isPerpsWithdraw || isMoneyAccountWithdraw || isDepositWalletWithdraw
? undefined
: from
? computeProxyAddress(from)
@@ -68,6 +83,10 @@ export function useTransactionPayPostQuote(): void {
if (isPerpsWithdraw) {
config.isHyperliquidSource = true;
}
+
+ if (isDepositWalletWithdraw) {
+ config.isPolymarketDepositWallet = true;
+ }
});
isSet.current = transactionId;
@@ -77,6 +96,7 @@ export function useTransactionPayPostQuote(): void {
refundTo,
isPerpsWithdraw,
isMoneyAccountWithdraw,
+ isDepositWalletWithdraw,
});
} catch (error) {
log('Error initializing post-quote transaction', {
@@ -85,9 +105,12 @@ export function useTransactionPayPostQuote(): void {
});
}
}, [
+ accountState,
canSelectWithdrawToken,
+ isDepositWalletWithdraw,
isMoneyAccountWithdraw,
isPerpsWithdraw,
+ isPredictWithdraw,
transactionId,
transactionMeta?.txParams?.from,
]);
diff --git a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts
index d811d27d6613..6e83cb4a4415 100644
--- a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts
+++ b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts
@@ -235,6 +235,7 @@ describe('Transaction Controller Init', () => {
bufferSubsequent: 0.05,
slippage: 0.005,
stxDisabled: false,
+ enableDepositWalletWithdraw: false,
});
payHookClassMock.mockReturnValue({
@@ -454,6 +455,7 @@ describe('Transaction Controller Init', () => {
bufferSubsequent: 0.05,
slippage: 0.005,
stxDisabled: true,
+ enableDepositWalletWithdraw: false,
});
const hooks = testConstructorOption('hooks');
@@ -471,6 +473,7 @@ describe('Transaction Controller Init', () => {
bufferSubsequent: 0.05,
slippage: 0.005,
stxDisabled: false,
+ enableDepositWalletWithdraw: false,
});
const hooks = testConstructorOption('hooks');
diff --git a/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.test.ts b/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.test.ts
new file mode 100644
index 000000000000..fc0d99dac255
--- /dev/null
+++ b/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.test.ts
@@ -0,0 +1,205 @@
+import {
+ SignTypedDataVersion,
+ type PersonalMessageParams,
+ type TypedMessageParams,
+} from '@metamask/keyring-controller';
+import type { Hex } from '@metamask/utils';
+
+import {
+ deriveDepositWalletAddress,
+ executeDepositWalletBatchAndWaitForCompletion,
+} from '../../../../components/UI/Predict/providers/polymarket/depositWallet';
+import type { TransactionPayControllerInitMessenger } from '../../messengers/transaction-pay-controller-messenger';
+import { createPolymarketCallbacks } from './polymarket-callbacks';
+
+jest.mock(
+ '../../../../components/UI/Predict/providers/polymarket/depositWallet',
+);
+
+const EOA_MOCK = '0x1111111111111111111111111111111111111111' as Hex;
+const DEPOSIT_WALLET_MOCK = '0x2222222222222222222222222222222222222222' as Hex;
+const SOURCE_HASH_MOCK = `0x${'aa'.repeat(32)}` as Hex;
+
+const CALLS_MOCK = [
+ {
+ target: '0x3333333333333333333333333333333333333333' as Hex,
+ data: '0x' as Hex,
+ value: '0',
+ },
+];
+
+function buildInitMessenger() {
+ return {
+ call: jest.fn(),
+ } as unknown as jest.Mocked;
+}
+
+describe('createPolymarketCallbacks', () => {
+ const deriveDepositWalletAddressMock = jest.mocked(
+ deriveDepositWalletAddress,
+ );
+ const executeDepositWalletBatchAndWaitForCompletionMock = jest.mocked(
+ executeDepositWalletBatchAndWaitForCompletion,
+ );
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ deriveDepositWalletAddressMock.mockReturnValue(DEPOSIT_WALLET_MOCK);
+ executeDepositWalletBatchAndWaitForCompletionMock.mockResolvedValue(
+ SOURCE_HASH_MOCK,
+ );
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ describe('getDepositWalletAddress', () => {
+ it('returns the derived deposit-wallet address for the given EOA', async () => {
+ const callbacks = createPolymarketCallbacks(buildInitMessenger());
+
+ const result = await callbacks.getDepositWalletAddress({ eoa: EOA_MOCK });
+
+ expect(result).toBe(DEPOSIT_WALLET_MOCK);
+ expect(deriveDepositWalletAddressMock).toHaveBeenCalledWith(EOA_MOCK);
+ });
+ });
+
+ describe('submitDepositWalletBatch', () => {
+ it('returns the relayer source hash on success', async () => {
+ const initMessenger = buildInitMessenger();
+ const callbacks = createPolymarketCallbacks(initMessenger);
+
+ const result = await callbacks.submitDepositWalletBatch({
+ eoa: EOA_MOCK,
+ depositWallet: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ });
+
+ expect(result).toStrictEqual({ sourceHash: SOURCE_HASH_MOCK });
+ expect(
+ executeDepositWalletBatchAndWaitForCompletionMock,
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ executeDepositWalletBatchAndWaitForCompletionMock.mock.calls[0][0],
+ ).toMatchObject({
+ walletAddress: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ signer: expect.objectContaining({ address: EOA_MOCK }),
+ });
+ });
+
+ it('signs typed and personal messages via the init messenger', async () => {
+ const initMessenger = buildInitMessenger();
+ initMessenger.call.mockImplementation(((action: string) => {
+ if (action === 'KeyringController:signTypedMessage') {
+ return Promise.resolve('0xtyped');
+ }
+ if (action === 'KeyringController:signPersonalMessage') {
+ return Promise.resolve('0xpersonal');
+ }
+ return undefined;
+ }) as never);
+ const callbacks = createPolymarketCallbacks(initMessenger);
+
+ await callbacks.submitDepositWalletBatch({
+ eoa: EOA_MOCK,
+ depositWallet: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ });
+
+ const signer =
+ executeDepositWalletBatchAndWaitForCompletionMock.mock.calls[0][0]
+ .signer;
+
+ const typedParams = {} as TypedMessageParams;
+ const typedResult = await signer.signTypedMessage(
+ typedParams,
+ SignTypedDataVersion.V4,
+ );
+ const personalParams = {} as PersonalMessageParams;
+ const personalResult = await signer.signPersonalMessage(personalParams);
+
+ expect(typedResult).toBe('0xtyped');
+ expect(personalResult).toBe('0xpersonal');
+ expect(initMessenger.call).toHaveBeenCalledWith(
+ 'KeyringController:signTypedMessage',
+ typedParams,
+ SignTypedDataVersion.V4,
+ );
+ expect(initMessenger.call).toHaveBeenCalledWith(
+ 'KeyringController:signPersonalMessage',
+ personalParams,
+ );
+ });
+
+ it('retries when the relayer reports "wallet busy" and eventually succeeds', async () => {
+ executeDepositWalletBatchAndWaitForCompletionMock
+ .mockRejectedValueOnce(new Error('wallet busy: try again'))
+ .mockRejectedValueOnce(new Error('Wallet Busy: still pending'))
+ .mockResolvedValueOnce(SOURCE_HASH_MOCK);
+
+ const callbacks = createPolymarketCallbacks(buildInitMessenger());
+
+ const promise = callbacks.submitDepositWalletBatch({
+ eoa: EOA_MOCK,
+ depositWallet: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ });
+
+ await jest.runAllTimersAsync();
+
+ await expect(promise).resolves.toStrictEqual({
+ sourceHash: SOURCE_HASH_MOCK,
+ });
+ expect(
+ executeDepositWalletBatchAndWaitForCompletionMock,
+ ).toHaveBeenCalledTimes(3);
+ });
+
+ it('rethrows non wallet-busy errors immediately without retrying', async () => {
+ executeDepositWalletBatchAndWaitForCompletionMock.mockRejectedValue(
+ new Error('relayer rejected'),
+ );
+
+ const callbacks = createPolymarketCallbacks(buildInitMessenger());
+
+ await expect(
+ callbacks.submitDepositWalletBatch({
+ eoa: EOA_MOCK,
+ depositWallet: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ }),
+ ).rejects.toThrow('relayer rejected');
+
+ expect(
+ executeDepositWalletBatchAndWaitForCompletionMock,
+ ).toHaveBeenCalledTimes(1);
+ });
+
+ it('gives up after exhausting wallet-busy retries', async () => {
+ executeDepositWalletBatchAndWaitForCompletionMock.mockRejectedValue(
+ new Error('wallet busy: persistent'),
+ );
+
+ const callbacks = createPolymarketCallbacks(buildInitMessenger());
+
+ const promise = callbacks.submitDepositWalletBatch({
+ eoa: EOA_MOCK,
+ depositWallet: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ });
+
+ const captured = promise.catch((error) => error);
+ await jest.runAllTimersAsync();
+ const error = await captured;
+
+ expect(error).toBeInstanceOf(Error);
+ expect((error as Error).message).toBe('wallet busy: persistent');
+ expect(
+ executeDepositWalletBatchAndWaitForCompletionMock,
+ ).toHaveBeenCalledTimes(5);
+ });
+ });
+});
diff --git a/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.ts b/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.ts
new file mode 100644
index 000000000000..b4aa6cb17989
--- /dev/null
+++ b/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.ts
@@ -0,0 +1,101 @@
+import {
+ SignTypedDataVersion,
+ type PersonalMessageParams,
+ type TypedMessageParams,
+} from '@metamask/keyring-controller';
+import type { PolymarketCallbacks } from '@metamask/transaction-pay-controller';
+import type { Hex } from '@metamask/utils';
+
+import {
+ deriveDepositWalletAddress,
+ executeDepositWalletBatchAndWaitForCompletion,
+} from '../../../../components/UI/Predict/providers/polymarket/depositWallet';
+import type { Signer } from '../../../../components/UI/Predict/providers/types';
+import type { TransactionPayControllerInitMessenger } from '../../messengers/transaction-pay-controller-messenger';
+
+const WALLET_BUSY_RETRY_ATTEMPTS = 5;
+const WALLET_BUSY_RETRY_DELAY_MS = 3000;
+const WALLET_BUSY_ERROR_MARKER = 'wallet busy';
+
+export function createPolymarketCallbacks(
+ initMessenger: TransactionPayControllerInitMessenger,
+): PolymarketCallbacks {
+ return {
+ getDepositWalletAddress: ({ eoa }) => getDepositWalletAddress(eoa),
+
+ submitDepositWalletBatch: ({ eoa, depositWallet, calls }) =>
+ submitDepositWalletBatch(initMessenger, { eoa, depositWallet, calls }),
+ };
+}
+
+async function getDepositWalletAddress(eoa: Hex): Promise {
+ return deriveDepositWalletAddress(eoa) as Hex;
+}
+
+async function submitDepositWalletBatch(
+ initMessenger: TransactionPayControllerInitMessenger,
+ {
+ eoa,
+ depositWallet,
+ calls,
+ }: {
+ eoa: Hex;
+ depositWallet: Hex;
+ calls: { target: Hex; data: Hex; value: string }[];
+ },
+): Promise<{ sourceHash: Hex }> {
+ return withWalletBusyRetry(async () => {
+ const sourceHash = await executeDepositWalletBatchAndWaitForCompletion({
+ signer: createSigner(initMessenger, eoa),
+ walletAddress: depositWallet,
+ calls,
+ });
+ return { sourceHash };
+ });
+}
+
+function createSigner(
+ initMessenger: TransactionPayControllerInitMessenger,
+ address: Hex,
+): Signer {
+ return {
+ address,
+ signTypedMessage: (
+ params: TypedMessageParams,
+ version: SignTypedDataVersion,
+ ) =>
+ initMessenger.call('KeyringController:signTypedMessage', params, version),
+ signPersonalMessage: (params: PersonalMessageParams) =>
+ initMessenger.call('KeyringController:signPersonalMessage', params),
+ };
+}
+
+async function withWalletBusyRetry(action: () => Promise): Promise {
+ let lastError: unknown;
+ for (let attempt = 0; attempt < WALLET_BUSY_RETRY_ATTEMPTS; attempt++) {
+ try {
+ return await action();
+ } catch (error) {
+ lastError = error;
+ if (
+ !isWalletBusyError(error) ||
+ attempt === WALLET_BUSY_RETRY_ATTEMPTS - 1
+ ) {
+ throw error;
+ }
+ await sleep(WALLET_BUSY_RETRY_DELAY_MS);
+ }
+ }
+ throw lastError;
+}
+
+function isWalletBusyError(error: unknown): boolean {
+ if (!(error instanceof Error)) {
+ return false;
+ }
+ return error.message.toLowerCase().includes(WALLET_BUSY_ERROR_MARKER);
+}
+
+function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
diff --git a/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.test.ts b/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.test.ts
index 79bcbeb8628b..b0c5c2b542bd 100644
--- a/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.test.ts
+++ b/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.test.ts
@@ -9,8 +9,10 @@ import {
} from '@metamask/transaction-pay-controller';
import { TransactionPayControllerInit } from './transaction-pay-controller-init';
import { TransactionPayControllerInitMessenger } from '../../messengers/transaction-pay-controller-messenger';
+import { createPolymarketCallbacks } from './polymarket-callbacks';
jest.mock('@metamask/transaction-pay-controller');
+jest.mock('./polymarket-callbacks');
function buildInitRequestMock(
initRequestProperties: Record = {},
@@ -99,4 +101,20 @@ describe('Transaction Pay Controller Init', () => {
expect(getStrategy).toBeUndefined();
expect(getStrategies).toBeUndefined();
});
+
+ it('wires Polymarket callbacks into the controller', () => {
+ const polymarketCallbacksMock = { __polymarketCallbacks: true };
+ jest
+ .mocked(createPolymarketCallbacks)
+ .mockReturnValue(
+ polymarketCallbacksMock as unknown as ReturnType<
+ typeof createPolymarketCallbacks
+ >,
+ );
+
+ const polymarket = testConstructorOption('polymarket');
+
+ expect(createPolymarketCallbacks).toHaveBeenCalledTimes(1);
+ expect(polymarket).toBe(polymarketCallbacksMock);
+ });
});
diff --git a/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.ts b/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.ts
index b42d20c38a40..9fededcc906b 100644
--- a/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.ts
+++ b/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.ts
@@ -6,6 +6,7 @@ import {
} from '@metamask/transaction-pay-controller';
import { TransactionPayControllerInitMessenger } from '../../messengers/transaction-pay-controller-messenger';
import { getDelegationTransaction } from '../../../../util/transactions/delegation';
+import { createPolymarketCallbacks } from './polymarket-callbacks';
export const TransactionPayControllerInit: MessengerClientInitFunction<
TransactionPayController,
@@ -19,6 +20,7 @@ export const TransactionPayControllerInit: MessengerClientInitFunction<
getDelegationTransaction: ({ transaction }) =>
getDelegationTransaction(initMessenger, transaction),
messenger: controllerMessenger,
+ polymarket: createPolymarketCallbacks(initMessenger),
state: persistedState.TransactionPayController,
});
diff --git a/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts b/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts
index 9d029afd5481..ceaceb669fd3 100644
--- a/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts
+++ b/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts
@@ -47,6 +47,8 @@ import {
TransactionPayControllerGetDelegationTransactionAction,
TransactionPayControllerGetStateAction,
TransactionPayControllerGetStrategyAction,
+ TransactionPayControllerPolymarketGetDepositWalletAddressAction,
+ TransactionPayControllerPolymarketSubmitDepositWalletBatchAction,
} from '@metamask/transaction-pay-controller';
import { RootMessenger } from '../../types';
import { AnalyticsControllerActions } from '@metamask/analytics-controller';
@@ -118,6 +120,8 @@ type InitMessengerActions =
| TransactionPayControllerGetDelegationTransactionAction
| TransactionPayControllerGetStateAction
| TransactionPayControllerGetStrategyAction
+ | TransactionPayControllerPolymarketGetDepositWalletAddressAction
+ | TransactionPayControllerPolymarketSubmitDepositWalletBatchAction
| AnalyticsControllerActions
| PredictControllerBeforePublishAction
| PredictControllerPublishAction;
@@ -178,6 +182,8 @@ export function getTransactionControllerInitMessenger(
'TransactionPayController:getDelegationTransaction',
'TransactionPayController:getState',
'TransactionPayController:getStrategy',
+ 'TransactionPayController:polymarketGetDepositWalletAddress',
+ 'TransactionPayController:polymarketSubmitDepositWalletBatch',
'AnalyticsController:trackEvent',
'PredictController:beforePublish',
'PredictController:publish',
diff --git a/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts b/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts
index 62f26d46b5c6..52ad9f30c6d0 100644
--- a/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts
+++ b/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts
@@ -6,7 +6,11 @@ import {
MessengerEvents,
} from '@metamask/messenger';
import { DelegationControllerSignDelegationAction } from '@metamask/delegation-controller';
-import { KeyringControllerSignEip7702AuthorizationAction } from '@metamask/keyring-controller';
+import {
+ KeyringControllerSignEip7702AuthorizationAction,
+ KeyringControllerSignPersonalMessageAction,
+ KeyringControllerSignTypedMessageAction,
+} from '@metamask/keyring-controller';
export function getTransactionPayControllerMessenger(
rootMessenger: RootMessenger,
@@ -59,7 +63,9 @@ export function getTransactionPayControllerMessenger(
type InitMessengerActions =
| DelegationControllerSignDelegationAction
- | KeyringControllerSignEip7702AuthorizationAction;
+ | KeyringControllerSignEip7702AuthorizationAction
+ | KeyringControllerSignPersonalMessageAction
+ | KeyringControllerSignTypedMessageAction;
type InitMessengerEvents = never;
export type TransactionPayControllerInitMessenger = ReturnType<
@@ -83,6 +89,8 @@ export function getTransactionPayControllerInitMessenger(
actions: [
'DelegationController:signDelegation',
'KeyringController:signEip7702Authorization',
+ 'KeyringController:signPersonalMessage',
+ 'KeyringController:signTypedMessage',
],
events: [],
messenger,
diff --git a/app/selectors/featureFlagController/confirmations/index.test.ts b/app/selectors/featureFlagController/confirmations/index.test.ts
index dd7c7906f8d5..f2f79b557921 100644
--- a/app/selectors/featureFlagController/confirmations/index.test.ts
+++ b/app/selectors/featureFlagController/confirmations/index.test.ts
@@ -16,6 +16,7 @@ import {
PAY_FIAT_ENABLED_TRANSACTION_TYPES,
PAY_FIAT_MAX_DELAY_MINUTES_FOR_PAYMENT_METHODS,
selectMetaMaskPayHardwareFlags,
+ PAY_ENABLE_DEPOSIT_WALLET_WITHDRAW_DEFAULT,
PAY_HARDWARE_ENABLED_DEFAULT,
PreferredToken,
getPreferredTokensForTransactionType,
@@ -563,3 +564,23 @@ describe('selectMetaMaskPayHardwareFlags', () => {
expect(selectMetaMaskPayHardwareFlags(state)).toEqual({ enabled: true });
});
});
+
+describe('selectMetaMaskPayFlags extended flags', () => {
+ it('returns default enableDepositWalletWithdraw when flag is absent', () => {
+ expect(
+ selectMetaMaskPayFlags(mockedEmptyFlagsState).enableDepositWalletWithdraw,
+ ).toEqual(PAY_ENABLE_DEPOSIT_WALLET_WITHDRAW_DEFAULT);
+ });
+
+ it('returns enableDepositWalletWithdraw from flag value', () => {
+ const state = cloneDeep(mockedEmptyFlagsState);
+ state.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags =
+ {
+ confirmations_pay_extended: { enableDepositWalletWithdraw: true },
+ };
+
+ expect(selectMetaMaskPayFlags(state).enableDepositWalletWithdraw).toEqual(
+ true,
+ );
+ });
+});
diff --git a/app/selectors/featureFlagController/confirmations/index.ts b/app/selectors/featureFlagController/confirmations/index.ts
index ba5e16f951d0..f1915b20bdf0 100644
--- a/app/selectors/featureFlagController/confirmations/index.ts
+++ b/app/selectors/featureFlagController/confirmations/index.ts
@@ -11,6 +11,7 @@ export const BUFFER_SUBSEQUENT_DEFAULT = 0.05;
export const PAY_FIAT_ENABLED_TRANSACTION_TYPES = [];
export const PAY_FIAT_MAX_DELAY_MINUTES_FOR_PAYMENT_METHODS = 10;
export const PAY_HARDWARE_ENABLED_DEFAULT = false;
+export const PAY_ENABLE_DEPOSIT_WALLET_WITHDRAW_DEFAULT = false;
export const SLIPPAGE_DEFAULT = 0.005;
export const STX_DISABLED_DEFAULT = false;
@@ -49,6 +50,10 @@ export interface MetaMaskPayFlags {
stxDisabled: boolean;
}
+export interface MetaMaskPayExtendedFlags {
+ enableDepositWalletWithdraw: boolean;
+}
+
export interface MetaMaskPayTokensFlags {
preferredTokens: PreferredTokensConfig;
blockedTokens: BlockedTokensConfig;
@@ -88,11 +93,16 @@ export interface MetaMaskPayHardwareFlags {
export const selectMetaMaskPayFlags = createSelector(
selectRemoteFeatureFlags,
- (featureFlags): MetaMaskPayFlags => {
+ (featureFlags): MetaMaskPayFlags & MetaMaskPayExtendedFlags => {
const metaMaskPayFlags = featureFlags?.confirmations_pay as
| Record
| undefined;
+ const metaMaskPayExtendedFlags =
+ featureFlags?.confirmations_pay_extended as
+ | Record
+ | undefined;
+
const attemptsMax =
(metaMaskPayFlags?.attemptsMax as number) ?? ATTEMPTS_MAX_DEFAULT;
@@ -111,6 +121,10 @@ export const selectMetaMaskPayFlags = createSelector(
const stxDisabled =
(metaMaskPayFlags?.stxDisabled as boolean) ?? STX_DISABLED_DEFAULT;
+ const enableDepositWalletWithdraw =
+ (metaMaskPayExtendedFlags?.enableDepositWalletWithdraw as boolean) ??
+ PAY_ENABLE_DEPOSIT_WALLET_WITHDRAW_DEFAULT;
+
return {
attemptsMax,
bufferInitial,
@@ -118,6 +132,7 @@ export const selectMetaMaskPayFlags = createSelector(
bufferSubsequent,
slippage,
stxDisabled,
+ enableDepositWalletWithdraw,
};
},
);
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 8e99265ab3c0..99a4494c2bae 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -8911,6 +8911,7 @@
"musd_claim": "Claim mUSD",
"perps_deposit": "Add funds",
"perps_withdraw": "Withdrawal",
+ "predict_withdraw": "Withdraw {{sourceSymbol}} from {{sourceChain}}",
"predict_deposit": "Add funds",
"swap": "Swap tokens",
"swap_approval": "Approve tokens",
diff --git a/package.json b/package.json
index b00ebf643514..2d1c08419dc1 100644
--- a/package.json
+++ b/package.json
@@ -349,7 +349,7 @@
"@metamask/superstruct": "^3.2.1",
"@metamask/swappable-obj-proxy": "^2.1.0",
"@metamask/transaction-controller": "^65.4.0",
- "@metamask/transaction-pay-controller": "^22.4.0",
+ "@metamask/transaction-pay-controller": "^22.5.0",
"@metamask/tron-wallet-snap": "^1.25.3",
"@metamask/utils": "^11.11.0",
"@myx-trade/sdk": "^0.1.265",
diff --git a/yarn.lock b/yarn.lock
index ffa65483b04b..11c9ccab7261 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10427,9 +10427,9 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/transaction-pay-controller@npm:^22.4.0":
- version: 22.4.0
- resolution: "@metamask/transaction-pay-controller@npm:22.4.0"
+"@metamask/transaction-pay-controller@npm:^22.5.0":
+ version: 22.5.0
+ resolution: "@metamask/transaction-pay-controller@npm:22.5.0"
dependencies:
"@ethersproject/abi": "npm:^5.7.0"
"@ethersproject/contracts": "npm:^5.7.0"
@@ -10452,7 +10452,7 @@ __metadata:
bn.js: "npm:^5.2.1"
immer: "npm:^9.0.6"
lodash: "npm:^4.17.21"
- checksum: 10/3de949b9525531bedcbc7fb24ab3470f38f014f3239b1e32d47654a2d25bffa67aab6ad3da39784a6321fdf36757a7f9c0e85946dfb33bb53d00a2a8ba728d34
+ checksum: 10/611103e0f4a8c2783f8196302830d9befee8973f64e0fdb050f658de3519c6dc01c1bf104788b2d726e8f22e22ed529ac19b7b3e9d53aea945e6676d71bef7e2
languageName: node
linkType: hard
@@ -35442,7 +35442,7 @@ __metadata:
"@metamask/test-dapp-multichain": "npm:^0.17.1"
"@metamask/test-dapp-solana": "npm:^0.3.0"
"@metamask/transaction-controller": "npm:^65.4.0"
- "@metamask/transaction-pay-controller": "npm:^22.4.0"
+ "@metamask/transaction-pay-controller": "npm:^22.5.0"
"@metamask/tron-wallet-snap": "npm:^1.25.3"
"@metamask/utils": "npm:^11.11.0"
"@myx-trade/sdk": "npm:^0.1.265"
From c40f285ff87688fde165f88260ad1850917e7cc3 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Fri, 15 May 2026 15:58:33 +0000
Subject: [PATCH 09/66] [skip ci] Bump version number to 5010
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index cf50cf9d29d4..210462bcef0f 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5007
+ versionCode 5010
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index a5750c260fd8..d02e9c908893 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5007
+ VERSION_NUMBER: 5010
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5007
+ FLASK_VERSION_NUMBER: 5010
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 751e6f8ed095..52c24ccf7eaa 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5007;
+ CURRENT_PROJECT_VERSION = 5010;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5007;
+ CURRENT_PROJECT_VERSION = 5010;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5007;
+ CURRENT_PROJECT_VERSION = 5010;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5007;
+ CURRENT_PROJECT_VERSION = 5010;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 4277b3ff6d5eca3a289e74320ed6972d2d79f799 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 17:58:40 +0200
Subject: [PATCH 10/66] chore(runway): cherry-pick fix: cp-7.78.0 ensure perf
e2e run on release branch (#30262)
- fix: cp-7.78.0 ensure perf e2e run on release branch (#30258)
## **Description**
## **Changelog**
CHANGELOG entry:
## **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**
- [ ] 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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Low risk: only adjusts GitHub Actions gating logic for when the
performance E2E workflow runs, without touching app/runtime code.
>
> **Overview**
> Ensures the `run-performance-e2e-release.yml` workflow runs for
**all** pushes to `release/*` branches (in addition to
`workflow_dispatch`), instead of only running on push when the commit
was a `metamaskbot` version bump.
>
> Keeps the scheduled trigger behavior (checking for recent
`metamaskbot` version bumps) but updates comments/logging to reflect the
new push trigger conditions.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3cdedbe29de0eb1227ddf8db629b8f13cfb2e38e. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[c1071c4](https://github.com/MetaMask/metamask-mobile/commit/c1071c4479d2a43c300501576de38d0d225e5f5b)
Co-authored-by: Curtis David
---
.../workflows/run-performance-e2e-release.yml | 21 ++++++-------------
1 file changed, 6 insertions(+), 15 deletions(-)
diff --git a/.github/workflows/run-performance-e2e-release.yml b/.github/workflows/run-performance-e2e-release.yml
index 198b50b59914..79ca5f8bd941 100644
--- a/.github/workflows/run-performance-e2e-release.yml
+++ b/.github/workflows/run-performance-e2e-release.yml
@@ -38,21 +38,10 @@ jobs:
- name: Check release trigger conditions
id: check
run: |
- # Always run for manual dispatch
- if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
+ # Always run for manual dispatch or release branch pushes
+ if [[ "${{ github.event_name }}" == "workflow_dispatch" || ("${{ github.event_name }}" == "push" && "${{ github.ref_name }}" =~ ^release/) ]]; then
echo "should-run=true" >> "$GITHUB_OUTPUT"
- echo "Performance tests triggered by manual dispatch"
- # For push events, only run if the triggering commit is a metamaskbot version bump
- elif [[ "${{ github.event_name }}" == "push" ]]; then
- COMMIT_MESSAGE=$(git log -1 --format="%s" "${{ github.sha }}")
- COMMIT_AUTHOR=$(git log -1 --format="%ae" "${{ github.sha }}")
- if [[ "$COMMIT_MESSAGE" =~ "Bump version number" && "$COMMIT_AUTHOR" =~ ^metamaskbot@ ]]; then
- echo "should-run=true" >> "$GITHUB_OUTPUT"
- echo "Push triggered by metamaskbot version bump: $COMMIT_MESSAGE"
- else
- echo "should-run=false" >> "$GITHUB_OUTPUT"
- echo "Push is not a metamaskbot version bump — skipping. Author: $COMMIT_AUTHOR | Message: $COMMIT_MESSAGE"
- fi
+ echo "Performance tests triggered by ${{ github.event_name }}"
# For scheduled runs, check for recent metamaskbot version bumps
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
git fetch --all
@@ -67,11 +56,13 @@ jobs:
# Check if the commit message contains "Bump version number" (ignore [skip ci])
COMMIT_MESSAGE=$(git log -1 --format="%s" "$COMMIT_HASH")
if [[ "$COMMIT_MESSAGE" =~ "Bump version number" ]]; then
- # Only run if the commit is from the last 30 minutes (to avoid re-running the same commit)
+ # Check if we've already processed this commit by looking for a workflow run with this commit
+ # We'll use a simple approach: check if the commit is from the last 30 minutes
COMMIT_TIME=$(git log -1 --format="%ct" "$COMMIT_HASH")
CURRENT_TIME=$(date +%s)
TIME_DIFF=$((CURRENT_TIME - COMMIT_TIME))
+ # Only run if the commit is from the last 30 minutes (to avoid re-running the same commit)
if [[ $TIME_DIFF -lt 1800 ]]; then
echo "should-run=true" >> "$GITHUB_OUTPUT"
echo "Recent metamaskbot version bump found (within last 30 min): $RECENT_VERSION_BUMP"
From b7c0b5703f8917a34492bcca77f33df924542dcf Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Fri, 15 May 2026 16:01:20 +0000
Subject: [PATCH 11/66] [skip ci] Bump version number to 5011
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 210462bcef0f..6dd43c95a647 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5010
+ versionCode 5011
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index d02e9c908893..e4b258cffa62 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5010
+ VERSION_NUMBER: 5011
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5010
+ FLASK_VERSION_NUMBER: 5011
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 52c24ccf7eaa..edd0b8fb0bae 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5010;
+ CURRENT_PROJECT_VERSION = 5011;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5010;
+ CURRENT_PROJECT_VERSION = 5011;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5010;
+ CURRENT_PROJECT_VERSION = 5011;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5010;
+ CURRENT_PROJECT_VERSION = 5011;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From a9021a5324dff7304d92a44e173f82afe18db569 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 23:40:32 +0200
Subject: [PATCH 12/66] chore(runway): cherry-pick fix(Rewards): Update
theMiracle logo and make it theme-aware cp-7.78.0 (#30273)
- fix(Rewards): Update theMiracle logo and make it theme-aware cp-7.78.0
(#30213)
## **Description**
The logo for theMiracle displayed at the bottom of the Benefits screen
was incorrect and was hard to see in dark theme
This uses the updated, correct logo as an SVG and swaps the color when
in dark mode.
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/RWDS-1302
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Low risk UI-only change: swaps an SVG asset and passes a theme-derived
`color` prop to improve contrast; minimal logic impact covered by unit
test updates.
>
> **Overview**
> Updates the Rewards Benefits footer to render the TheMiracle logo
using the current theme text color by wiring `useTheme()` into
`TheMiracleFooter` and passing a `color` prop to the SVG component.
>
> Replaces `themiracle-logo.svg` with the corrected artwork and adjusts
the unit test to mock/verify the new `color` prop is set to
`colors.text.default`.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
0ee0184deea937dd31b45e3c6768b32a22ccda18. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[ea0f772](https://github.com/MetaMask/metamask-mobile/commit/ea0f77220e156ce3457c4738106bee148f8818d7)
Co-authored-by: Christian Montoya
---
.../Benefits/TheMiracleFooter.test.tsx | 5 +++
.../components/Benefits/TheMiracleFooter.tsx | 26 +++++++++----
app/images/benefits/themiracle-logo.svg | 39 +++++++++----------
3 files changed, 41 insertions(+), 29 deletions(-)
diff --git a/app/components/UI/Rewards/components/Benefits/TheMiracleFooter.test.tsx b/app/components/UI/Rewards/components/Benefits/TheMiracleFooter.test.tsx
index 82e5bc00e0bc..fefda845c0b4 100644
--- a/app/components/UI/Rewards/components/Benefits/TheMiracleFooter.test.tsx
+++ b/app/components/UI/Rewards/components/Benefits/TheMiracleFooter.test.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import TheMiracleFooter from './TheMiracleFooter';
+import { mockTheme } from '../../../../../util/theme';
const mockStrings = jest.fn((key: string) => {
const translations: Record = {
@@ -20,16 +21,19 @@ jest.mock('images/benefits/themiracle-logo.svg', () => {
width,
height,
name,
+ color,
}: {
width: number;
height: number;
name: string;
+ color: string;
}) =>
ReactActual.createElement(View, {
testID: 'the-miracle-logo',
width,
height,
name,
+ color,
});
});
@@ -58,5 +62,6 @@ describe('TheMiracleFooter', () => {
expect(logo.props.name).toBe('TheMiracleLogo');
expect(logo.props.width).toBe(90);
expect(logo.props.height).toBe(26);
+ expect(logo.props.color).toBe(mockTheme.colors.text.default);
});
});
diff --git a/app/components/UI/Rewards/components/Benefits/TheMiracleFooter.tsx b/app/components/UI/Rewards/components/Benefits/TheMiracleFooter.tsx
index 33fc71a7b93c..fedd6e7a1a85 100644
--- a/app/components/UI/Rewards/components/Benefits/TheMiracleFooter.tsx
+++ b/app/components/UI/Rewards/components/Benefits/TheMiracleFooter.tsx
@@ -6,15 +6,25 @@ import {
} from '@metamask/design-system-react-native';
import React from 'react';
import TheMiracleLogo from 'images/benefits/themiracle-logo.svg';
+import { useTheme } from '../../../../../util/theme';
import { strings } from '../../../../../../locales/i18n';
-const TheMiracleFooter = () => (
-
-
- {strings('rewards.benefits.powered_by')}
-
-
-
-);
+const TheMiracleFooter = () => {
+ const { colors } = useTheme();
+
+ return (
+
+
+ {strings('rewards.benefits.powered_by')}
+
+
+
+ );
+};
export default TheMiracleFooter;
diff --git a/app/images/benefits/themiracle-logo.svg b/app/images/benefits/themiracle-logo.svg
index c9ee0bcdefc7..d98cd57f88ce 100644
--- a/app/images/benefits/themiracle-logo.svg
+++ b/app/images/benefits/themiracle-logo.svg
@@ -1,23 +1,20 @@
-
);
});
diff --git a/app/components/UI/WhatsHappening/components/WhatsHappeningAssetPill.test.tsx b/app/components/UI/WhatsHappening/components/WhatsHappeningAssetPill.test.tsx
new file mode 100644
index 000000000000..1e54f05619ed
--- /dev/null
+++ b/app/components/UI/WhatsHappening/components/WhatsHappeningAssetPill.test.tsx
@@ -0,0 +1,160 @@
+import React from 'react';
+import { screen, fireEvent } from '@testing-library/react-native';
+import renderWithProvider from '../../../../util/test/renderWithProvider';
+import WhatsHappeningAssetPill from './WhatsHappeningAssetPill';
+
+const mockHandleTrade = jest.fn();
+const mockTrackEvent = jest.fn();
+const mockCreateEventBuilder = jest.fn((eventName: string) => ({
+ addProperties: jest.fn(() => ({
+ build: jest.fn(() => ({ category: eventName })),
+ })),
+ build: jest.fn(() => ({ category: eventName })),
+}));
+
+jest.mock('../../../hooks/useAnalytics/useAnalytics', () => ({
+ useAnalytics: () => ({
+ trackEvent: mockTrackEvent,
+ createEventBuilder: mockCreateEventBuilder,
+ }),
+}));
+
+jest.mock(
+ '../../../Views/WhatsHappeningDetailView/hooks/useTradeNavigation',
+ () => ({
+ __esModule: true,
+ default: jest.fn(() => ({
+ handleTrade: mockHandleTrade,
+ canTrade: true,
+ })),
+ }),
+);
+
+jest.mock(
+ '../../../Views/WhatsHappeningDetailView/components/RelatedAssetAvatar',
+ () => 'RelatedAssetAvatar',
+);
+
+jest.mock(
+ '../../../Views/WhatsHappeningDetailView/utils/getRelatedAssetImageSource',
+ () => ({
+ getRelatedAssetImageSource: jest.fn(() => undefined),
+ }),
+);
+
+jest.mock('@metamask/perps-controller', () => ({
+ getPerpsDisplaySymbol: (symbol: string) => symbol,
+}));
+
+const baseAsset = {
+ sourceAssetId: 'btc',
+ symbol: 'BTC',
+ name: 'Bitcoin',
+ caip19: ['eip155:1/slip44:0'] as string[],
+ hlPerpsMarket: ['BTC'] as string[],
+};
+
+const baseItem = {
+ id: 'trend-0',
+ title: 'Test headline',
+ description: 'Test description',
+ date: '2026-01-01T00:00:00.000Z',
+ impact: 'positive' as const,
+ relatedAssets: [baseAsset],
+ articles: [],
+};
+
+const defaultProps = {
+ asset: baseAsset,
+ item: baseItem,
+ cardIndex: 0,
+ source: 'homepage' as const,
+};
+
+describe('WhatsHappeningAssetPill', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ const useTradeNavigation = jest.requireMock(
+ '../../../Views/WhatsHappeningDetailView/hooks/useTradeNavigation',
+ ).default;
+ useTradeNavigation.mockReturnValue({
+ handleTrade: mockHandleTrade,
+ canTrade: true,
+ });
+ });
+
+ it('renders display symbol', () => {
+ renderWithProvider();
+ expect(screen.getByText('BTC')).toBeOnTheScreen();
+ });
+
+ it('calls handleTrade when canTrade and pressed', () => {
+ renderWithProvider();
+ fireEvent.press(screen.getByLabelText('BTC'));
+ expect(mockHandleTrade).toHaveBeenCalledTimes(1);
+ });
+
+ it('fires WHATS_HAPPENING_INTERACTED with related_asset_pressed when pressed', () => {
+ renderWithProvider();
+ fireEvent.press(screen.getByLabelText('BTC'));
+ expect(mockTrackEvent).toHaveBeenCalledTimes(1);
+ const builderCall = mockCreateEventBuilder.mock.results[0].value;
+ expect(builderCall.addProperties).toHaveBeenCalledWith(
+ expect.objectContaining({
+ interaction_type: 'related_asset_pressed',
+ view: 'carousel',
+ asset_symbol: 'BTC',
+ perps_market: 'BTC',
+ asset_caip19: 'eip155:1/slip44:0',
+ }),
+ );
+ });
+
+ it('does not wrap in button when canTrade is false', () => {
+ const useTradeNavigation = jest.requireMock(
+ '../../../Views/WhatsHappeningDetailView/hooks/useTradeNavigation',
+ ).default;
+ useTradeNavigation.mockReturnValue({
+ handleTrade: mockHandleTrade,
+ canTrade: false,
+ });
+ renderWithProvider();
+ expect(screen.queryByLabelText('BTC')).toBeNull();
+ expect(screen.getByText('BTC')).toBeOnTheScreen();
+ });
+
+ it('shows positive percent change when perpsPriceEntry has a positive value', () => {
+ renderWithProvider(
+ ,
+ );
+ expect(screen.getByText('+1.23%')).toBeOnTheScreen();
+ });
+
+ it('shows negative percent change when perpsPriceEntry has a negative value', () => {
+ renderWithProvider(
+ ,
+ );
+ expect(screen.getByText('-2.50%')).toBeOnTheScreen();
+ });
+
+ it('does not render change text when perpsPriceEntry is undefined', () => {
+ renderWithProvider();
+ expect(screen.queryByText(/%/)).toBeNull();
+ });
+
+ it('does not render change text when percentChange24h is undefined', () => {
+ renderWithProvider(
+ ,
+ );
+ expect(screen.queryByText(/%/)).toBeNull();
+ });
+});
diff --git a/app/components/UI/WhatsHappening/components/WhatsHappeningAssetPill.tsx b/app/components/UI/WhatsHappening/components/WhatsHappeningAssetPill.tsx
new file mode 100644
index 000000000000..5caf37d85833
--- /dev/null
+++ b/app/components/UI/WhatsHappening/components/WhatsHappeningAssetPill.tsx
@@ -0,0 +1,135 @@
+import React, { memo, useCallback, useMemo } from 'react';
+import { Pressable } from 'react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+import type { RelatedAsset } from '@metamask/ai-controllers';
+import {
+ Box,
+ BoxAlignItems,
+ BoxBackgroundColor,
+ BoxFlexDirection,
+ Text,
+ TextColor,
+ TextVariant,
+ FontWeight,
+} from '@metamask/design-system-react-native';
+import { getPerpsDisplaySymbol } from '@metamask/perps-controller';
+import RelatedAssetAvatar from '../../../Views/WhatsHappeningDetailView/components/RelatedAssetAvatar';
+import { getRelatedAssetImageSource } from '../../../Views/WhatsHappeningDetailView/utils/getRelatedAssetImageSource';
+import useTradeNavigation from '../../../Views/WhatsHappeningDetailView/hooks/useTradeNavigation';
+import { formatPercentageChange } from '../../../Views/WhatsHappeningDetailView/utils/formatAssetPrice';
+import type { PerpsPriceEntry } from '../../../Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices';
+import { MetaMetricsEvents } from '../../../../core/Analytics';
+import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
+import {
+ WhatsHappeningInteractionType,
+ WhatsHappeningView,
+ type WhatsHappeningSourceValue,
+} from '../constants';
+import { getWhatsHappeningEventProps } from '../eventProperties';
+import type { WhatsHappeningItem } from '../types';
+
+const AVATAR_SIZE = 16;
+
+export interface WhatsHappeningAssetPillProps {
+ asset: RelatedAsset;
+ perpsPriceEntry?: PerpsPriceEntry;
+ item: WhatsHappeningItem;
+ cardIndex: number;
+ source: WhatsHappeningSourceValue;
+}
+
+const WhatsHappeningAssetPill: React.FC = ({
+ asset,
+ perpsPriceEntry,
+ item,
+ cardIndex,
+ source,
+}) => {
+ const tw = useTailwind();
+ const { handleTrade, canTrade } = useTradeNavigation(asset);
+ const { trackEvent, createEventBuilder } = useAnalytics();
+ const image = useMemo(() => getRelatedAssetImageSource(asset), [asset]);
+ const displaySymbol = useMemo(
+ () => getPerpsDisplaySymbol(asset.symbol),
+ [asset.symbol],
+ );
+ const { text: changeText, color: changeColor } = useMemo(
+ () => formatPercentageChange(perpsPriceEntry?.percentChange24h),
+ [perpsPriceEntry?.percentChange24h],
+ );
+
+ const handlePressWithTracking = useCallback(() => {
+ trackEvent(
+ createEventBuilder(MetaMetricsEvents.WHATS_HAPPENING_INTERACTED)
+ .addProperties({
+ ...getWhatsHappeningEventProps(item, cardIndex, source),
+ interaction_type: WhatsHappeningInteractionType.RelatedAssetPressed,
+ view: WhatsHappeningView.Carousel,
+ asset_symbol: asset.symbol,
+ perps_market: asset.hlPerpsMarket?.[0],
+ asset_caip19: asset.caip19?.[0],
+ })
+ .build(),
+ );
+ handleTrade();
+ }, [
+ trackEvent,
+ createEventBuilder,
+ item,
+ cardIndex,
+ source,
+ asset.symbol,
+ asset.hlPerpsMarket,
+ asset.caip19,
+ handleTrade,
+ ]);
+
+ const inner = (
+
+
+
+ {displaySymbol}
+
+ {changeText ? (
+
+ {changeText}
+
+ ) : null}
+
+ );
+
+ if (canTrade) {
+ return (
+ tw.style('shrink', pressed && 'opacity-80')}
+ >
+ {inner}
+
+ );
+ }
+
+ return inner;
+};
+
+export default memo(WhatsHappeningAssetPill);
diff --git a/app/components/UI/WhatsHappening/components/WhatsHappeningAssetSlider.test.tsx b/app/components/UI/WhatsHappening/components/WhatsHappeningAssetSlider.test.tsx
new file mode 100644
index 000000000000..b0cc12847fcc
--- /dev/null
+++ b/app/components/UI/WhatsHappening/components/WhatsHappeningAssetSlider.test.tsx
@@ -0,0 +1,167 @@
+import React from 'react';
+import { screen } from '@testing-library/react-native';
+import renderWithProvider from '../../../../util/test/renderWithProvider';
+import WhatsHappeningAssetSlider from './WhatsHappeningAssetSlider';
+
+jest.mock('../../../hooks/useAnalytics/useAnalytics', () => ({
+ useAnalytics: () => ({
+ trackEvent: jest.fn(),
+ createEventBuilder: jest.fn(() => ({
+ addProperties: jest.fn(() => ({ build: jest.fn(() => ({})) })),
+ build: jest.fn(() => ({})),
+ })),
+ }),
+}));
+
+const mockUseWhatsHappeningAssetPrices = jest.fn();
+
+jest.mock(
+ '../../../Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices',
+ () => ({
+ useWhatsHappeningAssetPrices: (assets: unknown) =>
+ mockUseWhatsHappeningAssetPrices(assets),
+ }),
+);
+
+jest.mock(
+ '../../../Views/WhatsHappeningDetailView/hooks/useTradeNavigation',
+ () => ({
+ __esModule: true,
+ default: () => ({
+ handleTrade: jest.fn(),
+ canTrade: true,
+ }),
+ }),
+);
+
+jest.mock(
+ '../../../Views/WhatsHappeningDetailView/components/RelatedAssetAvatar',
+ () => 'RelatedAssetAvatar',
+);
+
+jest.mock(
+ '../../../Views/WhatsHappeningDetailView/utils/getRelatedAssetImageSource',
+ () => ({
+ getRelatedAssetImageSource: jest.fn(() => undefined),
+ }),
+);
+
+jest.mock('@metamask/perps-controller', () => ({
+ getPerpsDisplaySymbol: (symbol: string) => symbol,
+}));
+
+const btcAsset = {
+ sourceAssetId: 'a1',
+ symbol: 'BTC',
+ name: 'Bitcoin',
+ caip19: [],
+ hlPerpsMarket: ['BTC'],
+};
+
+const ethAsset = {
+ sourceAssetId: 'a2',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ caip19: [],
+ hlPerpsMarket: ['ETH'],
+};
+
+const noPerpsAsset = {
+ sourceAssetId: 'a3',
+ symbol: 'FOO',
+ name: 'Foo',
+ caip19: ['eip155:1/erc20:0xfoo'],
+};
+
+const baseItem = {
+ id: 'trend-0',
+ title: 'Test headline',
+ description: 'Test description',
+ date: '2026-01-01T00:00:00.000Z',
+ impact: 'positive' as const,
+ relatedAssets: [btcAsset, ethAsset],
+ articles: [],
+};
+
+const sharedProps = {
+ item: baseItem,
+ cardIndex: 0,
+ source: 'homepage' as const,
+};
+
+describe('WhatsHappeningAssetSlider', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseWhatsHappeningAssetPrices.mockReturnValue({
+ perpsPriceBySymbol: {},
+ });
+ });
+
+ it('renders a pill per perps asset', () => {
+ renderWithProvider(
+ ,
+ );
+ expect(screen.getByText('BTC')).toBeOnTheScreen();
+ expect(screen.getByText('ETH')).toBeOnTheScreen();
+ });
+
+ it('filters out non-perps assets', () => {
+ renderWithProvider(
+ ,
+ );
+ expect(screen.getByText('BTC')).toBeOnTheScreen();
+ expect(screen.queryByText('FOO')).toBeNull();
+ });
+
+ it('calls useWhatsHappeningAssetPrices only with perps assets', () => {
+ renderWithProvider(
+ ,
+ );
+ expect(mockUseWhatsHappeningAssetPrices).toHaveBeenCalledWith([btcAsset]);
+ });
+
+ it('returns null when all assets are non-perps', () => {
+ const { toJSON } = renderWithProvider(
+ ,
+ );
+ expect(toJSON()).toBeNull();
+ });
+
+ it('shows the price change text when perpsPriceBySymbol has data', () => {
+ mockUseWhatsHappeningAssetPrices.mockReturnValue({
+ perpsPriceBySymbol: {
+ BTC: { price: 95000, percentChange24h: 1.23 },
+ },
+ });
+ renderWithProvider(
+ ,
+ );
+ expect(screen.getByText('+1.23%')).toBeOnTheScreen();
+ });
+
+ it('shows negative change in red and hides change text when undefined', () => {
+ mockUseWhatsHappeningAssetPrices.mockReturnValue({
+ perpsPriceBySymbol: {
+ BTC: { price: 95000, percentChange24h: -2.5 },
+ ETH: { price: 3000, percentChange24h: undefined },
+ },
+ });
+ renderWithProvider(
+ ,
+ );
+ expect(screen.getByText('-2.50%')).toBeOnTheScreen();
+ expect(screen.queryByText('undefined')).toBeNull();
+ });
+});
diff --git a/app/components/UI/WhatsHappening/components/WhatsHappeningAssetSlider.tsx b/app/components/UI/WhatsHappening/components/WhatsHappeningAssetSlider.tsx
new file mode 100644
index 000000000000..e7389835f679
--- /dev/null
+++ b/app/components/UI/WhatsHappening/components/WhatsHappeningAssetSlider.tsx
@@ -0,0 +1,57 @@
+import React, { memo, useMemo } from 'react';
+import { ScrollView } from 'react-native';
+import { useTailwind } from '@metamask/design-system-twrnc-preset';
+import type { RelatedAsset } from '@metamask/ai-controllers';
+import { useWhatsHappeningAssetPrices } from '../../../Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices';
+import type { WhatsHappeningItem } from '../types';
+import type { WhatsHappeningSourceValue } from '../constants';
+import WhatsHappeningAssetPill from './WhatsHappeningAssetPill';
+
+export interface WhatsHappeningAssetSliderProps {
+ assets: RelatedAsset[];
+ item: WhatsHappeningItem;
+ cardIndex: number;
+ source: WhatsHappeningSourceValue;
+}
+
+const WhatsHappeningAssetSlider: React.FC = ({
+ assets,
+ item,
+ cardIndex,
+ source,
+}) => {
+ const tw = useTailwind();
+
+ const perpsAssets = useMemo(
+ () => assets.filter((a) => a.hlPerpsMarket?.[0]),
+ [assets],
+ );
+
+ const { perpsPriceBySymbol } = useWhatsHappeningAssetPrices(perpsAssets);
+
+ if (perpsAssets.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {perpsAssets.map((asset) => (
+
+ ))}
+
+ );
+};
+
+export default memo(WhatsHappeningAssetSlider);
diff --git a/app/components/UI/WhatsHappening/components/WhatsHappeningCard.test.tsx b/app/components/UI/WhatsHappening/components/WhatsHappeningCard.test.tsx
index 216cf0c26d69..ddb25009af10 100644
--- a/app/components/UI/WhatsHappening/components/WhatsHappeningCard.test.tsx
+++ b/app/components/UI/WhatsHappening/components/WhatsHappeningCard.test.tsx
@@ -28,6 +28,26 @@ jest.mock('../../../hooks/useAnalytics/useAnalytics', () => ({
}),
}));
+jest.mock(
+ '../../../Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices',
+ () => ({
+ useWhatsHappeningAssetPrices: jest.fn(() => ({
+ perpsPriceBySymbol: {},
+ })),
+ }),
+);
+
+jest.mock(
+ '../../../Views/WhatsHappeningDetailView/hooks/useTradeNavigation',
+ () => ({
+ __esModule: true,
+ default: jest.fn(() => ({
+ handleTrade: jest.fn(),
+ canTrade: true,
+ })),
+ }),
+);
+
jest.mock(
'../../../Views/WhatsHappeningDetailView/components/RelatedAssetAvatar',
() => 'RelatedAssetAvatar',
@@ -124,14 +144,14 @@ describe('WhatsHappeningCard', () => {
expect(screen.queryByText('Neutral')).toBeNull();
});
- it('renders the asset symbol label when there is a single related asset', () => {
+ it('renders the asset symbol on the pill when there is a single related asset', () => {
renderWithProvider(
,
);
expect(screen.getByText('BTC')).toBeOnTheScreen();
});
- it('renders " +N" label when there are multiple related assets', () => {
+ it('renders a pill symbol for each related asset', () => {
const ethAsset = {
sourceAssetId: 'eth-mainnet',
symbol: 'ETH',
@@ -153,10 +173,12 @@ describe('WhatsHappeningCard', () => {
renderWithProvider(
,
);
- expect(screen.getByText('BTC +2')).toBeOnTheScreen();
+ expect(screen.getByText('BTC')).toBeOnTheScreen();
+ expect(screen.getByText('ETH')).toBeOnTheScreen();
+ expect(screen.getByText('SOL')).toBeOnTheScreen();
});
- it('strips xyz: prefix from symbol in the asset label', () => {
+ it('strips xyz: prefix from symbol on the pill', () => {
const brentAsset = {
sourceAssetId: 'brentoil',
symbol: 'xyz:BRENTOIL',
@@ -174,7 +196,7 @@ describe('WhatsHappeningCard', () => {
expect(screen.queryByText('xyz:BRENTOIL')).toBeNull();
});
- it('strips xyz: prefix from symbol in the "+N" label', () => {
+ it('strips xyz: prefix from each pill when multiple assets', () => {
const brentAsset = {
sourceAssetId: 'brentoil',
symbol: 'xyz:BRENTOIL',
@@ -198,8 +220,9 @@ describe('WhatsHappeningCard', () => {
,
);
- expect(screen.getByText('BRENTOIL +1')).toBeOnTheScreen();
- expect(screen.queryByText('xyz:BRENTOIL +1')).toBeNull();
+ expect(screen.getByText('BRENTOIL')).toBeOnTheScreen();
+ expect(screen.getByText('WTI')).toBeOnTheScreen();
+ expect(screen.queryByText('xyz:BRENTOIL')).toBeNull();
});
it('does not alter symbols that have no xyz: prefix', () => {
@@ -209,7 +232,7 @@ describe('WhatsHappeningCard', () => {
expect(screen.getByText('BTC')).toBeOnTheScreen();
});
- it('does not render asset label when relatedAssets is empty', () => {
+ it('does not render asset pills when relatedAssets is empty', () => {
const item = { ...baseItem, relatedAssets: [] };
renderWithProvider(
,
@@ -217,7 +240,7 @@ describe('WhatsHappeningCard', () => {
expect(screen.queryByText('BTC')).toBeNull();
});
- it('renders relative time when date is valid', () => {
+ it('renders relative time at the top when date is valid', () => {
renderWithProvider(
,
);
diff --git a/app/components/UI/WhatsHappening/components/WhatsHappeningCard.tsx b/app/components/UI/WhatsHappening/components/WhatsHappeningCard.tsx
index c645f48827c7..790da12c33a4 100644
--- a/app/components/UI/WhatsHappening/components/WhatsHappeningCard.tsx
+++ b/app/components/UI/WhatsHappening/components/WhatsHappeningCard.tsx
@@ -17,15 +17,13 @@ import {
getImpactBackgroundClass,
getImpactTextColor,
} from '../util/impact';
-import { getPerpsDisplaySymbol } from '@metamask/perps-controller';
-import RelatedAssetAvatar from '../../../Views/WhatsHappeningDetailView/components/RelatedAssetAvatar';
-import { getRelatedAssetImageSource } from '../../../Views/WhatsHappeningDetailView/utils/getRelatedAssetImageSource';
import { MetaMetricsEvents } from '../../../../core/Analytics';
import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
import { useViewportTracking } from '../../MarketInsights/hooks/useViewportTracking';
import { formatRelativeTime } from '../../MarketInsights/utils/marketInsightsFormatting';
import { getWhatsHappeningEventProps } from '../eventProperties';
import type { WhatsHappeningSourceValue } from '../constants';
+import WhatsHappeningAssetSlider from './WhatsHappeningAssetSlider';
interface WhatsHappeningCardProps {
item: WhatsHappeningItem;
@@ -34,8 +32,6 @@ interface WhatsHappeningCardProps {
onPress?: (item: WhatsHappeningItem) => void;
}
-const MAX_VISIBLE_ASSET_ICONS = 3;
-
const WhatsHappeningCard: React.FC = ({
item,
cardIndex,
@@ -65,25 +61,6 @@ const WhatsHappeningCard: React.FC = ({
const { ref: cardRef, onLayout: onVisibilityLayout } =
useViewportTracking(handleVisible);
- const visibleAssets = useMemo(
- () => item.relatedAssets.slice(0, MAX_VISIBLE_ASSET_ICONS),
- [item.relatedAssets],
- );
- const visibleAssetImages = useMemo(
- () => visibleAssets.map(getRelatedAssetImageSource),
- [visibleAssets],
- );
- const firstAsset = item.relatedAssets[0];
- const remainingAssetCount = Math.max(0, item.relatedAssets.length - 1);
- const firstAssetDisplaySymbol = firstAsset
- ? getPerpsDisplaySymbol(firstAsset.symbol)
- : null;
- const assetLabel = firstAssetDisplaySymbol
- ? remainingAssetCount > 0
- ? `${firstAssetDisplaySymbol} +${remainingAssetCount}`
- : firstAssetDisplaySymbol
- : null;
-
return (
= ({
'w-[280px] rounded-2xl bg-background-muted overflow-hidden p-4',
)}
>
-
- {item.impact && (
+
+ {(item.impact || formattedDate) && (
-
- {getImpactLabel(item.impact)}
-
+ {item.impact ? (
+
+
+ {getImpactLabel(item.impact)}
+
+
+ ) : (
+
+ )}
+ {formattedDate ? (
+
+ {formattedDate}
+
+ ) : null}
)}
@@ -126,56 +122,14 @@ const WhatsHappeningCard: React.FC = ({
-
- {assetLabel && (
-
- {visibleAssets.length > 0 && (
-
- {visibleAssets.map((asset, index) => (
- 0 ? '-ml-1' : ''}
- >
-
-
- ))}
-
- )}
-
- {assetLabel}
-
-
- )}
-
- {formattedDate && (
-
- {formattedDate}
-
- )}
-
+ {item.relatedAssets.length > 0 ? (
+
+ ) : null}
);
diff --git a/app/components/UI/WhatsHappening/components/WhatsHappeningCardSkeleton.tsx b/app/components/UI/WhatsHappening/components/WhatsHappeningCardSkeleton.tsx
index 742cb81c9479..4a01a5a0dcbb 100644
--- a/app/components/UI/WhatsHappening/components/WhatsHappeningCardSkeleton.tsx
+++ b/app/components/UI/WhatsHappening/components/WhatsHappeningCardSkeleton.tsx
@@ -17,8 +17,13 @@ const WhatsHappeningCardSkeleton: React.FC = () => {
>
- {/* Category badge */}
-
+ {/* Impact badge (left) + date (right) */}
+
+
+
+
{/* Title */}
{
'w-[75%] h-4 rounded',
]}
/>
- {/* Asset avatar + label (left) and date (right) */}
-
-
-
-
-
-
+ {/* Asset pill slider (compact chips) */}
+
+
+
+
+
diff --git a/app/components/UI/WhatsHappening/components/index.ts b/app/components/UI/WhatsHappening/components/index.ts
index 9886db9f9199..4c9983f478eb 100644
--- a/app/components/UI/WhatsHappening/components/index.ts
+++ b/app/components/UI/WhatsHappening/components/index.ts
@@ -1,2 +1,4 @@
+export { default as WhatsHappeningAssetPill } from './WhatsHappeningAssetPill';
+export { default as WhatsHappeningAssetSlider } from './WhatsHappeningAssetSlider';
export { default as WhatsHappeningCard } from './WhatsHappeningCard';
export { default as WhatsHappeningCardSkeleton } from './WhatsHappeningCardSkeleton';
diff --git a/app/components/UI/WhatsHappening/constants.ts b/app/components/UI/WhatsHappening/constants.ts
index 75234401872f..3e5269433954 100644
--- a/app/components/UI/WhatsHappening/constants.ts
+++ b/app/components/UI/WhatsHappening/constants.ts
@@ -20,4 +20,5 @@ export const WhatsHappeningInteractionType = {
SourceClick: 'source_click',
TradePressed: 'trade_pressed',
Pan: 'pan',
+ RelatedAssetPressed: 'related_asset_pressed',
} as const;
diff --git a/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.tsx b/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.tsx
index 4eee33525007..6da7c4f11073 100644
--- a/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.tsx
+++ b/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.tsx
@@ -86,7 +86,9 @@ const WhatsHappeningExpandedCard: React.FC = ({
[item.date],
);
- const { perpsPriceBySymbol } = useWhatsHappeningAssetPrices(item);
+ const { perpsPriceBySymbol } = useWhatsHappeningAssetPrices(
+ item.relatedAssets,
+ );
const scrollBottomFadeColors = useMemo((): string[] => {
if (isDarkMode) {
diff --git a/app/components/Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices.test.ts b/app/components/Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices.test.ts
index 8894fb4336fd..13d496661a6a 100644
--- a/app/components/Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices.test.ts
+++ b/app/components/Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices.test.ts
@@ -1,6 +1,5 @@
import { renderHook } from '@testing-library/react-native';
import { useWhatsHappeningAssetPrices } from './useWhatsHappeningAssetPrices';
-import type { WhatsHappeningItem } from '../../../UI/WhatsHappening/types';
import type { RelatedAsset } from '@metamask/ai-controllers';
// ── Mocks ──────────────────────────────────────────────────────────────────────
@@ -38,16 +37,6 @@ const assetNoPerps: RelatedAsset = {
caip19: ['eip155:1/erc20:0xfoo'],
};
-const makeItem = (relatedAssets: RelatedAsset[]): WhatsHappeningItem => ({
- id: 'trend-0',
- title: 'Test',
- description: 'Test description',
- date: '2026-01-01T00:00:00.000Z',
- impact: 'positive',
- relatedAssets,
- articles: [],
-});
-
// ── Tests ──────────────────────────────────────────────────────────────────────
describe('useWhatsHappeningAssetPrices', () => {
@@ -58,17 +47,19 @@ describe('useWhatsHappeningAssetPrices', () => {
describe('perps live price subscription', () => {
it('returns empty perpsPriceBySymbol when there are no hlPerpsMarket entries', () => {
- const item = makeItem([assetNoPerps]);
- const { result } = renderHook(() => useWhatsHappeningAssetPrices(item));
+ const { result } = renderHook(() =>
+ useWhatsHappeningAssetPrices([assetNoPerps]),
+ );
expect(result.current.perpsPriceBySymbol).toEqual({});
});
it('passes symbols to usePerpsLivePrices without duplicates', () => {
- const item = makeItem([
- tslaAsset,
- { ...tslaAsset, sourceAssetId: 'tsla2' },
- ]);
- renderHook(() => useWhatsHappeningAssetPrices(item));
+ renderHook(() =>
+ useWhatsHappeningAssetPrices([
+ tslaAsset,
+ { ...tslaAsset, sourceAssetId: 'tsla2' },
+ ]),
+ );
expect(mockUsePerpsLivePrices).toHaveBeenCalledWith({
symbols: ['xyz:TSLA'],
throttleMs: 3000,
@@ -79,8 +70,9 @@ describe('useWhatsHappeningAssetPrices', () => {
mockUsePerpsLivePrices.mockReturnValue({
'xyz:TSLA': { price: '172.50', percentChange24h: '3.45' },
});
- const item = makeItem([tslaAsset]);
- const { result } = renderHook(() => useWhatsHappeningAssetPrices(item));
+ const { result } = renderHook(() =>
+ useWhatsHappeningAssetPrices([tslaAsset]),
+ );
expect(result.current.perpsPriceBySymbol['xyz:TSLA']).toEqual({
price: 172.5,
percentChange24h: 3.45,
@@ -91,8 +83,9 @@ describe('useWhatsHappeningAssetPrices', () => {
mockUsePerpsLivePrices.mockReturnValue({
'xyz:TSLA': { price: '172.50' },
});
- const item = makeItem([tslaAsset]);
- const { result } = renderHook(() => useWhatsHappeningAssetPrices(item));
+ const { result } = renderHook(() =>
+ useWhatsHappeningAssetPrices([tslaAsset]),
+ );
expect(result.current.perpsPriceBySymbol['xyz:TSLA']).toMatchObject({
price: 172.5,
percentChange24h: undefined,
@@ -103,8 +96,9 @@ describe('useWhatsHappeningAssetPrices', () => {
mockUsePerpsLivePrices.mockReturnValue({
BTC: { price: '95000', percentChange24h: '2.5' },
});
- const item = makeItem([btcPerpsAsset]);
- const { result } = renderHook(() => useWhatsHappeningAssetPrices(item));
+ const { result } = renderHook(() =>
+ useWhatsHappeningAssetPrices([btcPerpsAsset]),
+ );
expect(mockUsePerpsLivePrices).toHaveBeenCalledWith({
symbols: ['BTC'],
throttleMs: 3000,
@@ -119,8 +113,7 @@ describe('useWhatsHappeningAssetPrices', () => {
name: 'Ethereum',
hlPerpsMarket: ['ETH'],
};
- const item = makeItem([tslaAsset, ethAsset]);
- renderHook(() => useWhatsHappeningAssetPrices(item));
+ renderHook(() => useWhatsHappeningAssetPrices([tslaAsset, ethAsset]));
expect(mockUsePerpsLivePrices).toHaveBeenCalledWith(
expect.objectContaining({
symbols: expect.arrayContaining(['xyz:TSLA', 'ETH']),
@@ -130,8 +123,7 @@ describe('useWhatsHappeningAssetPrices', () => {
});
it('does not include token-only assets (no hlPerpsMarket) in the symbols list', () => {
- const item = makeItem([assetNoPerps]);
- renderHook(() => useWhatsHappeningAssetPrices(item));
+ renderHook(() => useWhatsHappeningAssetPrices([assetNoPerps]));
expect(mockUsePerpsLivePrices).toHaveBeenCalledWith({
symbols: [],
throttleMs: 3000,
diff --git a/app/components/Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices.ts b/app/components/Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices.ts
index f7b38ae7c171..9416d62e8d71 100644
--- a/app/components/Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices.ts
+++ b/app/components/Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices.ts
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
-import type { WhatsHappeningItem } from '../../../UI/WhatsHappening/types';
+import type { RelatedAsset } from '@metamask/ai-controllers';
import { usePerpsLivePrices } from '../../../UI/Perps/hooks/stream';
export interface PerpsPriceEntry {
@@ -13,21 +13,18 @@ export interface UseWhatsHappeningAssetPricesResult {
}
/**
- * Returns live price data for every perps market referenced by a "What's
- * Happening" card item. Prices come from the WebSocket stream via
+ * Returns live price data for every perps market referenced by the given
+ * related assets. Prices come from the WebSocket stream via
* `usePerpsLivePrices` and are throttled to 3 s to limit re-renders.
*
- * All related assets are expected to have an `hlPerpsMarket` entry; assets
- * without one are ignored.
+ * Assets without an `hlPerpsMarket` entry are ignored.
*/
export function useWhatsHappeningAssetPrices(
- item: WhatsHappeningItem,
+ assets: RelatedAsset[],
): UseWhatsHappeningAssetPricesResult {
const perpsSymbols = useMemo(
- () => [
- ...new Set(item.relatedAssets.flatMap((a) => a.hlPerpsMarket ?? [])),
- ],
- [item.relatedAssets],
+ () => [...new Set(assets.flatMap((a) => a.hlPerpsMarket ?? []))],
+ [assets],
);
const livePerpsPrices = usePerpsLivePrices({
From bb50e5baf63d6b2cd4af2fa747254d530a9ff112 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Mon, 18 May 2026 20:40:57 +0000
Subject: [PATCH 21/66] [skip ci] Bump version number to 5052
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index a87a0d26eb3c..09989ae122f5 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5048
+ versionCode 5052
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index cd4e7ed673b6..48bbed58326e 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5048
+ VERSION_NUMBER: 5052
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5048
+ FLASK_VERSION_NUMBER: 5052
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 382706d507a2..f9aa2abcaded 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5048;
+ CURRENT_PROJECT_VERSION = 5052;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5048;
+ CURRENT_PROJECT_VERSION = 5052;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5048;
+ CURRENT_PROJECT_VERSION = 5052;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5048;
+ CURRENT_PROJECT_VERSION = 5052;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 7b5bdee677b1727fd06b8eecf73ed0c424541f72 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 23:00:54 +0200
Subject: [PATCH 22/66] chore(runway): cherry-pick chore: adds ab test for WH
positioning in explore cp-7.78.0 (#30328)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- chore: adds ab test for WH positioning in explore cp-7.78.0 (#30303)
## **Description**
Adds an AB test for the positioning of the Whats Happening section in
the Explore page.
## **Changelog**
CHANGELOG entry: no-changelog
## **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**
- [ ] 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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Low risk UI-only change gated by a remote A/B test flag; primary risk
is unintended section ordering or missing content in Explore for certain
variants.
>
> **Overview**
> Adds a new remote A/B test
(`socialAiTSA531AbtestWhatsHappeningExplore`) to control whether the
Explore "Now" tab shows *Whats Happening* before or after the *Predict*
carousel.
>
> `NowTab` now reads the variant via `useABTest` and renders the two
intro sections in the chosen order, while analytics are updated to
attach the new assignment to `EXPLORE_INTERACTED` events (registry +
unit test coverage).
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
166fec1a47ed4f179f75a8624eba12ea85416280. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[e3a4312](https://github.com/MetaMask/metamask-mobile/commit/e3a43129c7464d2f5a6d0ea03c306c0a00d78220)
Co-authored-by: João Santos
---
.../Views/TrendingView/abTestConfig.ts | 29 ++++
.../Views/TrendingView/tabs/NowTab.test.tsx | 155 +++++++++++++++++-
.../Views/TrendingView/tabs/NowTab.tsx | 70 +++++---
app/util/analytics/abTestAnalyticsRegistry.ts | 4 +
app/util/analytics/enrichWithABTests.test.ts | 16 ++
tests/feature-flags/feature-flag-registry.ts | 23 +++
6 files changed, 269 insertions(+), 28 deletions(-)
create mode 100644 app/components/Views/TrendingView/abTestConfig.ts
diff --git a/app/components/Views/TrendingView/abTestConfig.ts b/app/components/Views/TrendingView/abTestConfig.ts
new file mode 100644
index 000000000000..05cd1eec1d32
--- /dev/null
+++ b/app/components/Views/TrendingView/abTestConfig.ts
@@ -0,0 +1,29 @@
+import { EVENT_NAME } from '../../../core/Analytics/MetaMetrics.events';
+import type { ABTestAnalyticsMapping } from '../../../util/analytics/abTestAnalytics.types';
+
+export const WHATS_HAPPENING_EXPLORE_AB_KEY =
+ 'socialAiTSA531AbtestWhatsHappeningExplore';
+
+export enum WhatsHappeningExploreVariant {
+ Control = 'control',
+ Treatment = 'treatment',
+}
+
+export const WHATS_HAPPENING_EXPLORE_VARIANTS: Record<
+ WhatsHappeningExploreVariant,
+ { whatsHappeningBeforePredict: boolean }
+> = {
+ [WhatsHappeningExploreVariant.Control]: {
+ whatsHappeningBeforePredict: false,
+ },
+ [WhatsHappeningExploreVariant.Treatment]: {
+ whatsHappeningBeforePredict: true,
+ },
+};
+
+export const WHATS_HAPPENING_EXPLORE_AB_TEST_ANALYTICS_MAPPING: ABTestAnalyticsMapping =
+ {
+ flagKey: WHATS_HAPPENING_EXPLORE_AB_KEY,
+ validVariants: Object.values(WhatsHappeningExploreVariant),
+ eventNames: [EVENT_NAME.EXPLORE_INTERACTED],
+ };
diff --git a/app/components/Views/TrendingView/tabs/NowTab.test.tsx b/app/components/Views/TrendingView/tabs/NowTab.test.tsx
index 419c37148682..fef2ddf490a3 100644
--- a/app/components/Views/TrendingView/tabs/NowTab.test.tsx
+++ b/app/components/Views/TrendingView/tabs/NowTab.test.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
+import { WhatsHappeningExploreVariant } from '../abTestConfig';
const mockNavigate = jest.fn();
@@ -19,6 +20,12 @@ jest.mock('../feeds/tokens/useTokensFeed', () => ({
useTokensFeed: jest.fn(() => ({ data: [], isLoading: false })),
}));
+const mockUseABTest = jest.fn();
+jest.mock('../../../../hooks', () => ({
+ useABTest: (...args: unknown[]) =>
+ Reflect.apply(mockUseABTest, undefined, args),
+}));
+
const mockUsePerpsFeed = jest.fn(() => ({
data: [],
isLoading: false,
@@ -39,10 +46,44 @@ jest.mock('../feeds/perps/perpsNavigation', () => ({
) => mockNavigateToPerpsMarketList(nav, filter, sortOptionId),
}));
+interface MockPredictionMarket {
+ id: string;
+}
+
+const mockUsePredictionsFeed = jest.fn<
+ { data: MockPredictionMarket[]; isLoading: boolean },
+ []
+>(() => ({
+ data: [],
+ isLoading: false,
+}));
jest.mock('../feeds/predictions/usePredictionsFeed', () => ({
- usePredictionsFeed: jest.fn(() => ({ data: [], isLoading: false })),
+ usePredictionsFeed: () => mockUsePredictionsFeed(),
}));
+jest.mock('../feeds/predictions/PredictionRowItem', () => {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const { createElement } = require('react');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const { View } = require('react-native');
+ return {
+ PredictionCarouselRowItem: ({ market }: { market: { id: string } }) =>
+ createElement(View, { testID: `predict-market-row-item-${market.id}` }),
+ };
+});
+
+jest.mock('../components/HorizontalCarousel', () => {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const { createElement } = require('react');
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const { View } = require('react-native');
+ return {
+ __esModule: true,
+ default: ({ idPrefix }: { idPrefix: string }) =>
+ createElement(View, { testID: `${idPrefix}-flash-list` }),
+ };
+});
+
jest.mock('../feeds/stocks/useStocksFeed', () => ({
useStocksFeed: jest.fn(() => ({ data: [], isLoading: false })),
}));
@@ -88,6 +129,9 @@ const defaultTabProps = {
onRefresh: jest.fn(),
};
+const predictSectionTestId = 'section-header-view-all-predictions';
+const whatsHappeningSectionTestId = 'whats-happening-carousel';
+
const renderNowTab = (props = defaultTabProps) =>
render(
@@ -95,6 +139,64 @@ const renderNowTab = (props = defaultTabProps) =>
,
);
+interface RenderNode {
+ props?: {
+ testID?: string;
+ };
+ children?: unknown[] | null;
+}
+
+const isRenderNode = (node: unknown): node is RenderNode =>
+ Boolean(node) && typeof node === 'object';
+
+const collectTestIds = (node: unknown): string[] => {
+ if (Array.isArray(node)) {
+ return node.flatMap(collectTestIds);
+ }
+
+ if (!isRenderNode(node)) {
+ return [];
+ }
+
+ const ownTestIds =
+ typeof node.props?.testID === 'string' ? [node.props.testID] : [];
+ const childTestIds = node.children?.flatMap(collectTestIds) ?? [];
+
+ return [...ownTestIds, ...childTestIds];
+};
+
+const getIntroSectionOrder = (tree: unknown) =>
+ collectTestIds(tree).filter((testId) =>
+ [predictSectionTestId, whatsHappeningSectionTestId].includes(testId),
+ );
+
+const mockControlAbTest = () =>
+ mockUseABTest.mockReturnValue({
+ variant: { whatsHappeningBeforePredict: false },
+ variantName: WhatsHappeningExploreVariant.Control,
+ isActive: true,
+ });
+
+const mockTreatmentAbTest = () =>
+ mockUseABTest.mockReturnValue({
+ variant: { whatsHappeningBeforePredict: true },
+ variantName: WhatsHappeningExploreVariant.Treatment,
+ isActive: true,
+ });
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ mockControlAbTest();
+ mockUsePerpsFeed.mockReturnValue({
+ data: [],
+ isLoading: false,
+ refetch: jest.fn(),
+ defaultSortOptionId: 'priceChange' as const,
+ });
+ mockUsePredictionsFeed.mockReturnValue({ data: [], isLoading: false });
+ mockWhatsHappeningImpl.mockReturnValue(null);
+});
+
describe('NowTab — WhatsHappeningSection integration', () => {
const mockUseSelector = useSelector as jest.MockedFunction<
typeof useSelector
@@ -109,6 +211,7 @@ describe('NowTab — WhatsHappeningSection integration', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseSelector.mockImplementation(mockSelectorBase);
+ mockControlAbTest();
// Default: section mock renders nothing; individual tests override as needed.
mockWhatsHappeningImpl.mockReturnValue(null);
});
@@ -156,6 +259,55 @@ describe('NowTab — WhatsHappeningSection integration', () => {
const [forwardedRef] = mockWhatsHappeningImpl.mock.calls[0];
expect(forwardedRef).not.toBeNull();
});
+
+ it('renders Predict before Whats Happening for the control variant', () => {
+ mockUseSelector.mockImplementation((selector) => {
+ if (selector === selectWhatsHappeningEnabled) return true;
+ if (selector === selectPredictEnabledFlag) return true;
+ return mockSelectorBase(selector);
+ });
+ mockUsePredictionsFeed.mockReturnValue({
+ data: [{ id: 'market-1' }],
+ isLoading: false,
+ });
+ mockWhatsHappeningImpl.mockReturnValue(
+ React.createElement('View', {
+ testID: whatsHappeningSectionTestId,
+ }),
+ );
+
+ const { toJSON } = renderNowTab();
+
+ expect(getIntroSectionOrder(toJSON())).toEqual([
+ predictSectionTestId,
+ whatsHappeningSectionTestId,
+ ]);
+ });
+
+ it('renders Whats Happening before Predict for the treatment variant', () => {
+ mockUseSelector.mockImplementation((selector) => {
+ if (selector === selectWhatsHappeningEnabled) return true;
+ if (selector === selectPredictEnabledFlag) return true;
+ return mockSelectorBase(selector);
+ });
+ mockTreatmentAbTest();
+ mockUsePredictionsFeed.mockReturnValue({
+ data: [{ id: 'market-1' }],
+ isLoading: false,
+ });
+ mockWhatsHappeningImpl.mockReturnValue(
+ React.createElement('View', {
+ testID: whatsHappeningSectionTestId,
+ }),
+ );
+
+ const { toJSON } = renderNowTab();
+
+ expect(getIntroSectionOrder(toJSON())).toEqual([
+ whatsHappeningSectionTestId,
+ predictSectionTestId,
+ ]);
+ });
});
describe('NowTab — Perps Movers "View All" navigation', () => {
@@ -173,6 +325,7 @@ describe('NowTab — Perps Movers "View All" navigation', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseSelector.mockImplementation(mockSelectorBase);
+ mockControlAbTest();
mockWhatsHappeningImpl.mockReturnValue(null);
});
diff --git a/app/components/Views/TrendingView/tabs/NowTab.tsx b/app/components/Views/TrendingView/tabs/NowTab.tsx
index ce71d846a712..eac28db2c0a3 100644
--- a/app/components/Views/TrendingView/tabs/NowTab.tsx
+++ b/app/components/Views/TrendingView/tabs/NowTab.tsx
@@ -39,6 +39,11 @@ import WhatsHappeningSection from '../../../UI/WhatsHappening';
import { WhatsHappeningSource } from '../../../UI/WhatsHappening/constants';
import type { SectionRefreshHandle } from '../../Homepage/types';
import { selectWhatsHappeningEnabled } from '../../../../selectors/featureFlagController/whatsHappening';
+import { useABTest } from '../../../../hooks';
+import {
+ WHATS_HAPPENING_EXPLORE_AB_KEY,
+ WHATS_HAPPENING_EXPLORE_VARIANTS,
+} from '../abTestConfig';
interface PerpsBlockProps {
refresh: TabProps['refresh'];
@@ -102,6 +107,10 @@ const NowTab: React.FC = ({ refresh, refreshing, onRefresh }) => {
const isPerpsEnabled = useSelector(selectPerpsEnabledFlag);
const isPredictEnabled = useSelector(selectPredictEnabledFlag);
const isWhatsHappeningEnabled = useSelector(selectWhatsHappeningEnabled);
+ const { variant: whatsHappeningExploreVariant } = useABTest(
+ WHATS_HAPPENING_EXPLORE_AB_KEY,
+ WHATS_HAPPENING_EXPLORE_VARIANTS,
+ );
const whatsHappeningRef = useRef(null);
@@ -171,39 +180,46 @@ const NowTab: React.FC = ({ refresh, refreshing, onRefresh }) => {
cryptoMovers.isLoading || cryptoMovers.data.length > 0;
const showStocks = stocks.isLoading || stocks.data.length > 0;
+ const whatsHappeningSection = isWhatsHappeningEnabled ? (
+
+
+
+ ) : null;
+
+ const predictionsSection = showPredictions ? (
+
+ navigateToPredictionsList(navigation, 'trending')}
+ testID="section-header-view-all-predictions"
+ tabName="Now"
+ sectionName="predictions_trending"
+ />
+
+ data={predictions.data}
+ isLoading={predictions.isLoading}
+ renderItem={renderPredictionItem}
+ Skeleton={PredictionsSkeleton}
+ idPrefix="predictions"
+ />
+
+ ) : null;
+
+ const orderedIntroSections =
+ whatsHappeningExploreVariant.whatsHappeningBeforePredict
+ ? [whatsHappeningSection, predictionsSection]
+ : [predictionsSection, whatsHappeningSection];
+
return (
- {isWhatsHappeningEnabled && (
-
-
-
- )}
-
- {showPredictions && (
-
- navigateToPredictionsList(navigation, 'trending')}
- testID="section-header-view-all-predictions"
- tabName="Now"
- sectionName="predictions_trending"
- />
-
- data={predictions.data}
- isLoading={predictions.isLoading}
- renderItem={renderPredictionItem}
- Skeleton={PredictionsSkeleton}
- idPrefix="predictions"
- />
-
- )}
+ {orderedIntroSections}
{showCryptoMovers && (
diff --git a/app/util/analytics/abTestAnalyticsRegistry.ts b/app/util/analytics/abTestAnalyticsRegistry.ts
index b54996a86bed..4d661398e28f 100644
--- a/app/util/analytics/abTestAnalyticsRegistry.ts
+++ b/app/util/analytics/abTestAnalyticsRegistry.ts
@@ -8,6 +8,7 @@ import {
WALLET_HOME_POST_ONBOARDING_AB_TEST_ANALYTICS_MAPPING,
} from '../../components/Views/Homepage/abTestConfig';
import { STICKY_FOOTER_SWAP_LABEL_AB_TEST_ANALYTICS_MAPPING } from '../../components/UI/TokenDetails/components/abTestConfig';
+import { WHATS_HAPPENING_EXPLORE_AB_TEST_ANALYTICS_MAPPING } from '../../components/Views/TrendingView/abTestConfig';
export const AB_TEST_ANALYTICS_MAPPINGS: readonly ABTestAnalyticsMapping[] = [
// Card
@@ -22,6 +23,9 @@ export const AB_TEST_ANALYTICS_MAPPINGS: readonly ABTestAnalyticsMapping[] = [
HUB_PAGE_DISCOVERY_TABS_AB_TEST_ANALYTICS_MAPPING,
WALLET_HOME_POST_ONBOARDING_AB_TEST_ANALYTICS_MAPPING,
+ // Explore
+ WHATS_HAPPENING_EXPLORE_AB_TEST_ANALYTICS_MAPPING,
+
// Token Details
STICKY_FOOTER_SWAP_LABEL_AB_TEST_ANALYTICS_MAPPING,
];
diff --git a/app/util/analytics/enrichWithABTests.test.ts b/app/util/analytics/enrichWithABTests.test.ts
index 2c94b5c4778e..f46b7c394ad9 100644
--- a/app/util/analytics/enrichWithABTests.test.ts
+++ b/app/util/analytics/enrichWithABTests.test.ts
@@ -1,4 +1,6 @@
import { AnalyticsEventBuilder } from './AnalyticsEventBuilder';
+import { MetaMetricsEvents } from '../../core/Analytics/MetaMetrics.events';
+import { WHATS_HAPPENING_EXPLORE_AB_KEY } from '../../components/Views/TrendingView/abTestConfig';
import { createActiveABTestAssignment } from './activeABTestAssignments';
import { enrichWithABTests } from './enrichWithABTests';
@@ -144,6 +146,20 @@ describe('enrichWithABTests', () => {
]);
});
+ it('enriches Explore Page Interacted events with Whats Happening Explore assignment', () => {
+ const event = AnalyticsEventBuilder.createEventBuilder(
+ MetaMetricsEvents.EXPLORE_INTERACTED,
+ ).build();
+
+ const result = enrichWithABTests(event, {
+ [WHATS_HAPPENING_EXPLORE_AB_KEY]: { name: 'treatment' },
+ });
+
+ expect(result.properties.active_ab_tests).toEqual([
+ createActiveABTestAssignment(WHATS_HAPPENING_EXPLORE_AB_KEY, 'treatment'),
+ ]);
+ });
+
it('leaves non-A/B properties and sensitive properties unchanged', () => {
const event = AnalyticsEventBuilder.createEventBuilder('Card Button Viewed')
.addProperties({
diff --git a/tests/feature-flags/feature-flag-registry.ts b/tests/feature-flags/feature-flag-registry.ts
index 705824c89fc0..b8b3da8a901d 100644
--- a/tests/feature-flags/feature-flag-registry.ts
+++ b/tests/feature-flags/feature-flag-registry.ts
@@ -126,6 +126,29 @@ export const FEATURE_FLAG_REGISTRY: Record = {
status: FeatureFlagStatus.Active,
},
+ socialAiTSA531AbtestWhatsHappeningExplore: {
+ name: 'socialAiTSA531AbtestWhatsHappeningExplore',
+ type: FeatureFlagType.Remote,
+ inProd: true,
+ productionDefault: [
+ {
+ name: 'control',
+ scope: {
+ type: 'percentage_rollout',
+ value: 0.5,
+ },
+ },
+ {
+ name: 'treatment',
+ scope: {
+ type: 'percentage_rollout',
+ value: 1,
+ },
+ },
+ ],
+ status: FeatureFlagStatus.Active,
+ },
+
assetsAccountApiBalances: {
name: 'assetsAccountApiBalances',
type: FeatureFlagType.Remote,
From 5e26731864b84d6a7ed19d6176858e7a0d37eb8e Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Mon, 18 May 2026 21:03:24 +0000
Subject: [PATCH 23/66] [skip ci] Bump version number to 5053
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 09989ae122f5..74b789b52fd9 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5052
+ versionCode 5053
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 48bbed58326e..1feef28ddd8f 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5052
+ VERSION_NUMBER: 5053
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5052
+ FLASK_VERSION_NUMBER: 5053
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index f9aa2abcaded..de834d610d8c 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5052;
+ CURRENT_PROJECT_VERSION = 5053;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5052;
+ CURRENT_PROJECT_VERSION = 5053;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5052;
+ CURRENT_PROJECT_VERSION = 5053;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5052;
+ CURRENT_PROJECT_VERSION = 5053;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 5319709d963ccc4b6bdd0e3592e34fcac04f515f Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 16:59:39 +0200
Subject: [PATCH 24/66] chore(runway): cherry-pick feat: add AI disclaimer to
whats happening cp-7.78.0 (#30358)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- feat: add AI disclaimer to whats happening cp-7.78.0 (#30352)
## **Description**
Adds a disclaimer to the Market Insights page when the AI button is
clicked.
## **Changelog**
CHANGELOG entry: no-changelog
## **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
- [ ] 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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Low risk UI-only change that adds a new bottom-sheet visibility state
and callback wiring; primary risk is minor regressions in the Whats
Happening detail view interaction flow.
>
> **Overview**
> Adds an **AI disclaimer** flow to the `What's happening` detail
carousel: tapping the **AI pill** on `WhatsHappeningExpandedCard` now
triggers an `onAIDisclaimerPress` callback, which
`WhatsHappeningDetailView` uses to show/hide
`MarketInsightsDisclaimerBottomSheet`.
>
> Updates unit tests to cover the new AI-pill press behavior and bottom
sheet open/close rendering logic.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
026dd93faaa5531b4a9829e4cacaa8f02ff25d32. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[5efac50](https://github.com/MetaMask/metamask-mobile/commit/5efac500a9274c042903b93bbbedc6f88e41ac11)
Co-authored-by: João Santos
---
.../WhatsHappeningDetailView.test.tsx | 90 ++++++++++++++++---
.../WhatsHappeningDetailView.tsx | 17 ++++
.../WhatsHappeningExpandedCard.test.tsx | 19 ++++
.../components/WhatsHappeningExpandedCard.tsx | 54 +++++++----
4 files changed, 148 insertions(+), 32 deletions(-)
diff --git a/app/components/Views/WhatsHappeningDetailView/WhatsHappeningDetailView.test.tsx b/app/components/Views/WhatsHappeningDetailView/WhatsHappeningDetailView.test.tsx
index d095cc1ce093..54d154b18660 100644
--- a/app/components/Views/WhatsHappeningDetailView/WhatsHappeningDetailView.test.tsx
+++ b/app/components/Views/WhatsHappeningDetailView/WhatsHappeningDetailView.test.tsx
@@ -4,6 +4,7 @@ import { screen, fireEvent } from '@testing-library/react-native';
import renderWithProvider from '../../../util/test/renderWithProvider';
import WhatsHappeningExpandedCard from './components/WhatsHappeningExpandedCard';
import WhatsHappeningSourcesBottomSheet from './components/WhatsHappeningSourcesBottomSheet';
+import MarketInsightsDisclaimerBottomSheet from '../../UI/MarketInsights/components/MarketInsightsEntryCard/MarketInsightsDisclaimerBottomSheet';
import WhatsHappeningDetailView, {
CARD_WIDTH,
} from './WhatsHappeningDetailView';
@@ -43,6 +44,14 @@ jest.mock('./components/WhatsHappeningSourcesBottomSheet', () => ({
default: jest.fn(),
}));
+jest.mock(
+ '../../UI/MarketInsights/components/MarketInsightsEntryCard/MarketInsightsDisclaimerBottomSheet',
+ () => ({
+ __esModule: true,
+ default: jest.fn(),
+ }),
+);
+
jest.mock('../../hooks/useAnalytics/useAnalytics', () => ({
useAnalytics: () => ({
trackEvent: mockTrackEvent,
@@ -107,22 +116,30 @@ describe('WhatsHappeningDetailView', () => {
(WhatsHappeningExpandedCard as unknown as jest.Mock).mockImplementation(
({
onSourcesPress,
+ onAIDisclaimerPress,
}: {
onSourcesPress?: (articles: unknown[]) => void;
+ onAIDisclaimerPress?: () => void;
}) => (
-
- onSourcesPress?.([
- {
- title: 'Test',
- source: 'coindesk.com',
- url: 'https://coindesk.com/test',
- date: '2026-03-15T10:00:00.000Z',
- },
- ])
- }
- />
+
+
+ onSourcesPress?.([
+ {
+ title: 'Test',
+ source: 'coindesk.com',
+ url: 'https://coindesk.com/test',
+ date: '2026-03-15T10:00:00.000Z',
+ },
+ ])
+ }
+ />
+
+
),
);
@@ -133,6 +150,14 @@ describe('WhatsHappeningDetailView', () => {
));
+
+ (
+ MarketInsightsDisclaimerBottomSheet as unknown as jest.Mock
+ ).mockImplementation(({ onClose }: { onClose: () => void }) => (
+
+
+
+ ));
});
it('renders the screen title', () => {
@@ -432,6 +457,45 @@ describe('WhatsHappeningDetailView', () => {
expect(screen.queryByTestId('mock-sources-bottom-sheet')).toBeNull();
});
+ it('shows the AI disclaimer bottom sheet when onAIDisclaimerPress is called from a card', () => {
+ mockUseWhatsHappening.mockReturnValue({
+ items: [mockItem],
+ isLoading: false,
+ error: null,
+ refresh: mockRefresh,
+ });
+ renderWithProvider();
+ const carousel = screen.getByTestId('whats-happening-detail-carousel');
+ fireEvent(carousel, 'layout', {
+ nativeEvent: { layout: { height: 600, width: 375, x: 0, y: 0 } },
+ });
+
+ fireEvent.press(screen.getByTestId('mock-ai-disclaimer-button'));
+
+ expect(
+ screen.getByTestId('mock-ai-disclaimer-bottom-sheet'),
+ ).toBeOnTheScreen();
+ });
+
+ it('hides the AI disclaimer bottom sheet when onClose is called', () => {
+ mockUseWhatsHappening.mockReturnValue({
+ items: [mockItem],
+ isLoading: false,
+ error: null,
+ refresh: mockRefresh,
+ });
+ renderWithProvider();
+ const carousel = screen.getByTestId('whats-happening-detail-carousel');
+ fireEvent(carousel, 'layout', {
+ nativeEvent: { layout: { height: 600, width: 375, x: 0, y: 0 } },
+ });
+ fireEvent.press(screen.getByTestId('mock-ai-disclaimer-button'));
+
+ fireEvent.press(screen.getByTestId('mock-ai-disclaimer-close'));
+
+ expect(screen.queryByTestId('mock-ai-disclaimer-bottom-sheet')).toBeNull();
+ });
+
it('updates the active page indicator dot when the carousel is scrolled', () => {
mockUseWhatsHappening.mockReturnValue({
items: [
diff --git a/app/components/Views/WhatsHappeningDetailView/WhatsHappeningDetailView.tsx b/app/components/Views/WhatsHappeningDetailView/WhatsHappeningDetailView.tsx
index 6ab2b967c2b0..012b4b6a1b5f 100644
--- a/app/components/Views/WhatsHappeningDetailView/WhatsHappeningDetailView.tsx
+++ b/app/components/Views/WhatsHappeningDetailView/WhatsHappeningDetailView.tsx
@@ -36,6 +36,7 @@ import { getWhatsHappeningEventProps } from '../../UI/WhatsHappening/eventProper
import ErrorState from '../Homepage/components/ErrorState/ErrorState';
import WhatsHappeningExpandedCard from './components/WhatsHappeningExpandedCard';
import WhatsHappeningSourcesBottomSheet from './components/WhatsHappeningSourcesBottomSheet';
+import MarketInsightsDisclaimerBottomSheet from '../../UI/MarketInsights/components/MarketInsightsEntryCard/MarketInsightsDisclaimerBottomSheet';
import PageIndicator from './components/PageIndicator';
import { PerpsStreamProvider } from '../../UI/Perps/providers/PerpsStreamManager';
import { MetaMetricsEvents } from '../../../core/Analytics';
@@ -80,6 +81,7 @@ const WhatsHappeningDetailView = () => {
item: WhatsHappeningItem;
cardIndex: number;
} | null>(null);
+ const [isAIDisclaimerVisible, setIsAIDisclaimerVisible] = useState(false);
const scrollViewRef = useRef(null);
const hasScrolledToInitial = useRef(false);
@@ -101,6 +103,15 @@ const WhatsHappeningDetailView = () => {
const handleSourcesClose = useCallback(() => {
setSourcesContext(null);
}, []);
+
+ const handleAIDisclaimerPress = useCallback(() => {
+ setIsAIDisclaimerVisible(true);
+ }, []);
+
+ const handleAIDisclaimerClose = useCallback(() => {
+ setIsAIDisclaimerVisible(false);
+ }, []);
+
const hasTrackedOpenedRef = useRef(false);
const hasTrackedViewRef = useRef(false);
const previousIndexRef = useRef(initialIndex);
@@ -307,6 +318,7 @@ const WhatsHappeningDetailView = () => {
onSourcesPress={(articles) =>
handleSourcesPress(articles, item, index)
}
+ onAIDisclaimerPress={handleAIDisclaimerPress}
/>
))}
@@ -325,6 +337,11 @@ const WhatsHappeningDetailView = () => {
source={source}
/>
)}
+ {isAIDisclaimerVisible && (
+
+ )}
);
};
diff --git a/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.test.tsx b/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.test.tsx
index a76a514e327c..cf269fbfb75f 100644
--- a/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.test.tsx
+++ b/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.test.tsx
@@ -130,6 +130,25 @@ describe('WhatsHappeningExpandedCard', () => {
expect(screen.getByText('Bullish')).toBeOnTheScreen();
});
+ it('calls onAIDisclaimerPress when the AI pill is pressed', () => {
+ const onAIDisclaimerPress = jest.fn();
+
+ renderWithProvider(
+ ,
+ );
+
+ fireEvent.press(screen.getByTestId('whats-happening-ai-disclaimer-button'));
+
+ expect(onAIDisclaimerPress).toHaveBeenCalledTimes(1);
+ });
+
it('renders Neutral badge when impact is explicitly neutral', () => {
const item = { ...baseItem, impact: 'neutral' as const };
renderWithProvider(
diff --git a/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.tsx b/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.tsx
index 6da7c4f11073..9e4d6bf4ef5c 100644
--- a/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.tsx
+++ b/app/components/Views/WhatsHappeningDetailView/components/WhatsHappeningExpandedCard.tsx
@@ -47,6 +47,7 @@ interface WhatsHappeningExpandedCardProps {
* than the card's positioning context.
*/
onSourcesPress?: (articles: Article[]) => void;
+ onAIDisclaimerPress?: () => void;
}
const WhatsHappeningExpandedCard: React.FC = ({
@@ -56,6 +57,7 @@ const WhatsHappeningExpandedCard: React.FC = ({
cardHeight,
source,
onSourcesPress,
+ onAIDisclaimerPress,
}) => {
const tw = useTailwind();
const { themeAppearance, colors } = useTheme();
@@ -133,26 +135,40 @@ const WhatsHappeningExpandedCard: React.FC = ({
gap={2}
twClassName="flex-wrap"
>
- {/* AI pill */}
-
-
-
- {strings('whats_happening.ai')}
-
-
+ {({ pressed }) => (
+
+
+
+ {strings('whats_happening.ai')}
+
+
+ )}
+
Date: Tue, 19 May 2026 15:02:03 +0000
Subject: [PATCH 25/66] [skip ci] Bump version number to 5071
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 74b789b52fd9..614ffaef31d1 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5053
+ versionCode 5071
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 1feef28ddd8f..0322df5d5481 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5053
+ VERSION_NUMBER: 5071
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5053
+ FLASK_VERSION_NUMBER: 5071
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index de834d610d8c..971ecdce1386 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5053;
+ CURRENT_PROJECT_VERSION = 5071;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5053;
+ CURRENT_PROJECT_VERSION = 5071;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5053;
+ CURRENT_PROJECT_VERSION = 5071;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5053;
+ CURRENT_PROJECT_VERSION = 5071;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From d2df18df49092dce603043cd8f51131a0ef0adb8 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Tue, 19 May 2026 18:45:04 +0000
Subject: [PATCH 26/66] [skip ci] Bump version number to 5073
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 614ffaef31d1..3b2e97843a56 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5071
+ versionCode 5073
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 0322df5d5481..2bce03c57696 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5071
+ VERSION_NUMBER: 5073
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5071
+ FLASK_VERSION_NUMBER: 5073
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 971ecdce1386..a2a876bd0b2e 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5071;
+ CURRENT_PROJECT_VERSION = 5073;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5071;
+ CURRENT_PROJECT_VERSION = 5073;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5071;
+ CURRENT_PROJECT_VERSION = 5073;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5071;
+ CURRENT_PROJECT_VERSION = 5073;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From f53b2ba2221aadb464bc56f8be758f95ae29d680 Mon Sep 17 00:00:00 2001
From: chloeYue <105063779+chloeYue@users.noreply.github.com>
Date: Tue, 19 May 2026 22:20:58 +0200
Subject: [PATCH 27/66] chore(release): release-changelog/7.78.0 (#30210)
This PR updates the change log for 7.78.0 (Hotfix - no test plan
generated.)
CHANGELOG entry: null
---
> [!NOTE]
> **Low Risk**
> Low risk: documentation-only change updating release notes and compare
links; no runtime code changes.
>
> **Overview**
> **Release documentation update.** Adds a new `7.78.0` section to
`CHANGELOG.md` with the curated *Added/Changed/Fixed* entries for the
release.
>
> Also updates the bottom compare links so `[Unreleased]` now compares
from `v7.78.0` and adds the `7.78.0` GitHub comparison reference.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
424fefa10a9828ba62181168762c1322bbb06688. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
Co-authored-by: Cursor
---
CHANGELOG.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 49 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db599c880eea..44bc700de1d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [7.78.0]
+
+### Added
+
+- Added a Braze-driven promotional banner to the wallet home screen. (#29301)
+- Added a Money Account onboarding flow. (#30137)
+- Added a postonboarding checklist on the wallet home shown when the balance is empty. (#28851)
+- Added an optional onboarding interest questionnaire after metrics opt-in for eligible users. (#30056)
+- Added a "Paid by MetaMask" treatment on the mUSD conversion confirmation screen when MetaMask fully sponsors the network, provider, and gas fees. (#30120)
+- Added the Money Account withdrawal flow. (#29862)
+- Added mUSD support on Monad. (#29897)
+- Added Batch Sell token selection for selling up to five same-network tokens. (#29690)
+- Added a sort control to a trader's Open and Closed positions on the Top Traders profile screen. (#30027)
+- Added a World Cup promotional banner to the Predict feed. (#30070)
+- Added websocket streaming integration for OHLCV data. (#29739)
+- Added handling for on-ramp provider return deeplinks so users land directly on their order details after completing or cancelling a purchase with an external provider. (#29858)
+- Added an AI disclaimer to the What's Happening section. (#30352)
+- Added price-change pills on related assets. (#30259)
+- Showed the bonus benefits menu for users with the VIP feature enabled. (#29888)
+- Displayed the total benefits count on the Rewards benefits preview header. (#30063)
+
+### Changed
+
+- Updated the primary CTA on the Money Account onboarding stepper to read "Add funds". (#29909)
+- Updated Predict buy previews to include market fees in totals and balance checks. (#29881)
+- Updated Bridge navigation to use the native stack with in-screen headers for Bridge, token selection, and quote selection. (#29829)
+- Updated the mUSD bonus calculator in Rewards with a fresh design. (#29758)
+- Updated the Rewards "theMiracle" logo to be theme-aware. (#30213)
+- Aligned previously base-enabled custom network logos (Stable, Flow, XDC, Fraxtal, Hemi, Plasma, Lukso, Rootstock, MSU, Lens, Plume) to a square format consistent with Popular networks. (#29943)
+- Aligned carousel card heights for accessibility. (#30201)
+- Improved the empty DeFi state navigation to point to Trending v2. (#29927)
+- Improved retry behavior when QR hardware wallet signing scans fail. (#29741)
+- Removed gas alerts from the confirmation modal in gasless flows and updated the 10 MON minimum-reserve alert copy. (#29835)
+
+### Fixed
+
+- Fixed a regression where gas estimate alerts had stopped showing in confirmations. (#30266)
+- Fixed underline positioning in the React Native Scrollable Tab View. (#30133)
+- Fixed a bug where failed builder fee approval was permanently cached, causing subsequent Perps orders to fail. (#30095)
+- Fixed a bug that could repeatedly prompt hardware wallet users while Perps was idle. (#30114)
+- Fixed iOS header inset for Perps order screens. (#30143)
+- Fixed a bug where a trader's positions could appear stale on the Top Traders profile and position screens, and added pull-to-refresh on both screens. (#30039)
+- Fixed limit order margin calculation to use the limit price instead of the market price, preventing "insufficient margin" errors. (#29800)
+- Fixed a bug that could leave the swap quote area blank during slippage refresh. (#29975)
+- Fixed the Account List opening too quickly. (#29859)
+- Fixed an issue where EIP-7702 authorization signatures with leading zero bytes in `r` or `s` could be rejected by relays and public RPCs. (#29717)
+
## [7.77.1]
### Added
@@ -11504,7 +11551,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#957](https://github.com/MetaMask/metamask-mobile/pull/957): fix timeouts (#957)
- [#954](https://github.com/MetaMask/metamask-mobile/pull/954): Bugfix: onboarding navigation (#954)
-[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.1...HEAD
+[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.78.0...HEAD
+[7.78.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.1...v7.78.0
[7.77.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.0...v7.77.1
[7.77.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.3...v7.77.0
[7.76.3]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.0...v7.76.3
From c67bb33e72bf5d138739c29aba2592f8f37fcf32 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Tue, 19 May 2026 20:23:26 +0000
Subject: [PATCH 28/66] [skip ci] Bump version number to 5074
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 3b2e97843a56..dead934b882e 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5073
+ versionCode 5074
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 2bce03c57696..0395e541dbde 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5073
+ VERSION_NUMBER: 5074
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5073
+ FLASK_VERSION_NUMBER: 5074
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index a2a876bd0b2e..d06f84e6f20d 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5073;
+ CURRENT_PROJECT_VERSION = 5074;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5073;
+ CURRENT_PROJECT_VERSION = 5074;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5073;
+ CURRENT_PROJECT_VERSION = 5074;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5073;
+ CURRENT_PROJECT_VERSION = 5074;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From fcaa21563a3ff28f3848bc714d56076fa6a10360 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Wed, 20 May 2026 13:32:17 +0200
Subject: [PATCH 29/66] chore(runway): cherry-pick chore: revert
react-native-mmkv to v3.x cp-7.78.0 (#30430)
- chore: revert react-native-mmkv to v3.x cp-7.78.0 (#30391)
Revert the MMKV major bump that landed in #29195 (RN 0.81.5 upgrade).
The v3 -> v4 jump was not required by RN 0.81 / React 19 / Expo SDK 54
and shipped a latent crash in migration 106 (PPOM cleanup) because v4
makes `MMKV` a type-only export while migration 106 was still calling
`new MMKV(...)` against `require('react-native-mmkv').MMKV`.
CHANGELOG entry: null
Changes:
- package.json: `react-native-mmkv` ^4.1.2 -> ^3.2.0 (resolves to 3.3.3)
`react-native-nitro-modules` left pinned at 0.35.5 since
`react-native-vision-camera` and other packages still need the new Nitro
version.
- app/store/storage-wrapper.ts: `createMMKV()` -> `new MMKV()`,
`.remove(key)` -> `.delete(key)`, drop the `as MMKV` cast,
`EventEmitter2` named import restored.
- app/store/migrations/049.ts: factory -> constructor.
- app/store/migrations/106.ts: drop the `require` + `// eslint-disable`
workaround, use a plain `import { MMKV }`. Fixes the silent migration
failure on devices that had PPOM storage.
- app/store/migrations/136.ts: factory -> constructor, `.remove()` ->
`.delete()` (this migration was added in #29542 after the bump landed,
so it needed forward-translation to v3 rather than a true revert).
- app/store/migrations/106.test.ts, 136.test.ts: revert mock factory
exports from `createMMKV` to `MMKV`.
- app/util/notifications/settings/storage/index.ts: all three callsites
swapped from `createMMKV(...)` to `new MMKV(...)`.
- app/util/test/testSetup.js, testSetupView.js: drop `createMMKV` from
the module-level mock factory (v3 has no such export, so leaving it in
would mask real production bugs in tests).
- ios/Podfile.lock: regenerated. NitroMmkv pod + MMKVCore artifact
removed; react-native-mmkv v3 pod added.
Verified post-revert: no `createMMKV` references left in the codebase,
no `import type { MMKV }` left, no v4-style `.remove(key)` on MMKV
instances, lints clean across all touched files. `notificationStorage`
test still uses `import { MMKV }` for a type annotation, which works
since v3 exports `MMKV` as a class (value + type).
TODO before merge: run the unit-test suite and an iOS/Android smoke
build, plus the in-place upgrade canary scenario from #29195 to confirm
v3 binaries can still read MMKV files that were written by v4 in any
prior dev/internal build (production users are still on v3).
## **Description**
## **Changelog**
CHANGELOG entry:
## **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**
- [ ] 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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Medium Risk**
> Reverts a major storage dependency version and updates MMKV
instantiation/deletion APIs across migrations and wrappers, which can
affect persistence and data cleanup behavior on upgrade. Risk is
mitigated by targeted code changes and updated Jest mocks/tests, but
should be validated with mobile smoke tests and migration paths.
>
> **Overview**
> Reverts `react-native-mmkv` from v4 to v3 (updates `package.json`,
`yarn.lock`, and iOS pods) and removes the Nitro-related MMKV pods from
`Podfile.lock`.
>
> Updates all touched call sites to match the v3 API: replaces
`createMMKV(...)` with `new MMKV(...)`, switches key deletion from
`.remove(...)` to `.delete(...)`, and simplifies migration `106` to use
a standard `import { MMKV }` (eliminating the prior `require`
workaround).
>
> Adjusts Jest mocks and migration tests (`106`, `136`) to mock the
`MMKV` constructor-only export expected in v3 and to assert
`.delete(...)` behavior.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
042b30e8eb29a6a2eda60483e2de08d830934a3d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---------
Co-authored-by: Cursor
[1d3e8b0](https://github.com/MetaMask/metamask-mobile/commit/1d3e8b0dfb0f79142c945916e7b0218b906510a1)
Co-authored-by: Andre Pimenta
Co-authored-by: Cursor
---
app/store/migrations/049.ts | 4 +-
app/store/migrations/106.test.ts | 7 +-
app/store/migrations/106.ts | 3 +-
app/store/migrations/136.test.ts | 30 ++++----
app/store/migrations/136.ts | 8 +--
app/store/storage-wrapper.ts | 8 +--
.../notifications/settings/storage/index.ts | 8 +--
app/util/test/testSetup.js | 1 -
app/util/test/testSetupView.js | 1 -
ios/Podfile.lock | 70 +++++++++----------
package.json | 2 +-
yarn.lock | 11 ++-
12 files changed, 70 insertions(+), 83 deletions(-)
diff --git a/app/store/migrations/049.ts b/app/store/migrations/049.ts
index 55085eed5156..4c910b5b81bc 100644
--- a/app/store/migrations/049.ts
+++ b/app/store/migrations/049.ts
@@ -1,8 +1,8 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { captureException } from '@sentry/react-native';
-import { createMMKV } from 'react-native-mmkv';
+import { MMKV } from 'react-native-mmkv';
-export const storage = createMMKV();
+export const storage = new MMKV();
export default async function migrate(state: unknown) {
const keys = await AsyncStorage.getAllKeys();
diff --git a/app/store/migrations/106.test.ts b/app/store/migrations/106.test.ts
index 56db252e39de..f1cdd62f44e5 100644
--- a/app/store/migrations/106.test.ts
+++ b/app/store/migrations/106.test.ts
@@ -1,20 +1,17 @@
+import { MMKV } from 'react-native-mmkv';
import migrate from './106';
jest.mock('react-native-mmkv', () => ({
MMKV: jest.fn(),
}));
-// `MMKV` is a type-only export in react-native-mmkv v3, so we resolve the
-// jest mock at runtime instead of importing the value.
-const MMKV = jest.requireMock('react-native-mmkv').MMKV as jest.Mock;
-
describe('Migration #106', () => {
const mockMMKVInstance = {
getAllKeys: jest.fn(),
clearAll: jest.fn(),
};
- MMKV.mockImplementation(() => mockMMKVInstance);
+ (MMKV as jest.Mock).mockImplementation(() => mockMMKVInstance);
beforeEach(() => {
jest.clearAllMocks();
diff --git a/app/store/migrations/106.ts b/app/store/migrations/106.ts
index 960a67fc9cbb..a738c80032de 100644
--- a/app/store/migrations/106.ts
+++ b/app/store/migrations/106.ts
@@ -1,6 +1,5 @@
import { captureException } from '@sentry/react-native';
-// eslint-disable-next-line @typescript-eslint/no-require-imports, import-x/no-commonjs
-const { MMKV } = require('react-native-mmkv');
+import { MMKV } from 'react-native-mmkv';
import { ensureValidState } from './util';
const migrationVersion = 106;
diff --git a/app/store/migrations/136.test.ts b/app/store/migrations/136.test.ts
index 74667a63218b..e809cffde9ba 100644
--- a/app/store/migrations/136.test.ts
+++ b/app/store/migrations/136.test.ts
@@ -1,10 +1,10 @@
-import { createMMKV } from 'react-native-mmkv';
+import { MMKV } from 'react-native-mmkv';
import migrate from './136';
import { ensureValidState } from './util';
jest.mock('react-native-mmkv', () => ({
- createMMKV: jest.fn(),
+ MMKV: jest.fn(),
}));
jest.mock('./util', () => ({
@@ -12,15 +12,15 @@ jest.mock('./util', () => ({
}));
describe('migration 136', () => {
- const mockRemove = jest.fn();
+ const mockDelete = jest.fn();
const mockGetString = jest.fn();
const mockMMKVInstance = {
getString: mockGetString,
- remove: mockRemove,
+ delete: mockDelete,
};
- (createMMKV as jest.Mock).mockImplementation(() => mockMMKVInstance);
+ (MMKV as jest.Mock).mockImplementation(() => mockMMKVInstance);
const baseState = {
engine: { backgroundState: {} },
@@ -37,7 +37,7 @@ describe('migration 136', () => {
const state = { ...baseState };
expect(migrate(state)).toBe(state);
- expect(createMMKV).not.toHaveBeenCalled();
+ expect(MMKV).not.toHaveBeenCalled();
});
it('does nothing when legacy storage has no key', () => {
@@ -46,11 +46,11 @@ describe('migration 136', () => {
const state = { ...baseState };
const result = migrate(state);
- expect(createMMKV).toHaveBeenCalledWith({
+ expect(MMKV).toHaveBeenCalledWith({
id: 'redux-persist-attribution',
});
expect(mockGetString).toHaveBeenCalledWith('persist:attribution');
- expect(mockRemove).not.toHaveBeenCalled();
+ expect(mockDelete).not.toHaveBeenCalled();
expect(result).toEqual(baseState);
});
@@ -80,7 +80,7 @@ describe('migration 136', () => {
},
},
});
- expect(mockRemove).toHaveBeenCalledWith('persist:attribution');
+ expect(mockDelete).toHaveBeenCalledWith('persist:attribution');
});
it('does not overwrite when current attribution is already set', () => {
@@ -101,7 +101,7 @@ describe('migration 136', () => {
const result = migrate(state);
expect(result).toEqual(state);
- expect(mockRemove).toHaveBeenCalledWith('persist:attribution');
+ expect(mockDelete).toHaveBeenCalledWith('persist:attribution');
});
it('removes invalid legacy payload without merging', () => {
@@ -110,7 +110,7 @@ describe('migration 136', () => {
const state = { ...baseState };
migrate(state);
- expect(mockRemove).toHaveBeenCalledWith('persist:attribution');
+ expect(mockDelete).toHaveBeenCalledWith('persist:attribution');
});
it('handles MMKV errors without throwing', () => {
@@ -129,7 +129,7 @@ describe('migration 136', () => {
const state = { ...baseState };
migrate(state);
- expect(mockRemove).toHaveBeenCalledWith('persist:attribution');
+ expect(mockDelete).toHaveBeenCalledWith('persist:attribution');
});
it('removes key when attribution is null without merging root state', () => {
@@ -141,7 +141,7 @@ describe('migration 136', () => {
migrate(state);
expect(state.attribution?.attribution).toBeNull();
- expect(mockRemove).toHaveBeenCalledWith('persist:attribution');
+ expect(mockDelete).toHaveBeenCalledWith('persist:attribution');
});
it('removes key when attribution object lacks capturedAt', () => {
@@ -152,7 +152,7 @@ describe('migration 136', () => {
const state = { ...baseState };
migrate(state);
- expect(mockRemove).toHaveBeenCalledWith('persist:attribution');
+ expect(mockDelete).toHaveBeenCalledWith('persist:attribution');
expect(state).toEqual(baseState);
});
@@ -168,6 +168,6 @@ describe('migration 136', () => {
migrate(state);
- expect(mockRemove).toHaveBeenCalledWith('persist:attribution');
+ expect(mockDelete).toHaveBeenCalledWith('persist:attribution');
});
});
diff --git a/app/store/migrations/136.ts b/app/store/migrations/136.ts
index c404362900ea..5eee570153c9 100644
--- a/app/store/migrations/136.ts
+++ b/app/store/migrations/136.ts
@@ -1,6 +1,6 @@
import { captureException } from '@sentry/react-native';
import { hasProperty, isObject } from '@metamask/utils';
-import { createMMKV } from 'react-native-mmkv';
+import { MMKV } from 'react-native-mmkv';
import type {
AttributionRecord,
AttributionState,
@@ -56,7 +56,7 @@ const migration = (state: unknown): unknown => {
: null;
try {
- const legacy = createMMKV({ id: LEGACY_ATTRIBUTION_MMKV_ID });
+ const legacy = new MMKV({ id: LEGACY_ATTRIBUTION_MMKV_ID });
const raw = legacy.getString(LEGACY_PERSIST_KEY);
if (!raw) {
@@ -66,7 +66,7 @@ const migration = (state: unknown): unknown => {
const fromLegacy = parseLegacyAttributionState(raw);
if (!fromLegacy) {
- legacy.remove(LEGACY_PERSIST_KEY);
+ legacy.delete(LEGACY_PERSIST_KEY);
return state;
}
@@ -74,7 +74,7 @@ const migration = (state: unknown): unknown => {
root.attribution = fromLegacy;
}
- legacy.remove(LEGACY_PERSIST_KEY);
+ legacy.delete(LEGACY_PERSIST_KEY);
} catch (error) {
captureException(
new Error(
diff --git a/app/store/storage-wrapper.ts b/app/store/storage-wrapper.ts
index 7eab29f20730..07db315be375 100644
--- a/app/store/storage-wrapper.ts
+++ b/app/store/storage-wrapper.ts
@@ -1,8 +1,8 @@
import ReadOnlyNetworkStore from '../util/test/network-store';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { isE2E } from '../util/test/utils';
-import { createMMKV, type MMKV } from 'react-native-mmkv';
-import EventEmitter2 from 'eventemitter2';
+import { MMKV } from 'react-native-mmkv';
+import { EventEmitter2 } from 'eventemitter2';
/**
* Wrapper class for MMKV.
@@ -39,7 +39,7 @@ class StorageWrapper extends EventEmitter2 {
* The underlying storage implementation.
* Use `ReadOnlyNetworkStore` in test mode otherwise use `AsyncStorage`.
*/
- this.storage = isE2E ? ReadOnlyNetworkStore : createMMKV();
+ this.storage = isE2E ? ReadOnlyNetworkStore : new MMKV();
}
/**
@@ -135,7 +135,7 @@ class StorageWrapper extends EventEmitter2 {
*/
async removeItem(key: string) {
try {
- return await (this.storage as MMKV).remove(key);
+ return await this.storage.delete(key);
} catch (error) {
if (isE2E) {
// Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails
diff --git a/app/util/notifications/settings/storage/index.ts b/app/util/notifications/settings/storage/index.ts
index 9651c5248c0c..fa812775ecd5 100644
--- a/app/util/notifications/settings/storage/index.ts
+++ b/app/util/notifications/settings/storage/index.ts
@@ -1,7 +1,7 @@
-import { createMMKV } from 'react-native-mmkv';
+import { MMKV } from 'react-native-mmkv';
import { STORAGE_TYPES, STORAGE_IDS, mapStorageTypeToIds } from './constants';
-export const notificationStorage = createMMKV({
+export const notificationStorage = new MMKV({
id: STORAGE_IDS.NOTIFICATIONS,
});
@@ -44,11 +44,11 @@ export class mmStorage {
static clearAllStorages() {
Object.keys(STORAGE_IDS).forEach((id) => {
- const storage = createMMKV({ id });
+ const storage = new MMKV({ id });
storage.clearAll();
});
- const defaultStorage = createMMKV();
+ const defaultStorage = new MMKV();
defaultStorage.clearAll();
}
}
diff --git a/app/util/test/testSetup.js b/app/util/test/testSetup.js
index e1a18785090e..787acc381032 100644
--- a/app/util/test/testSetup.js
+++ b/app/util/test/testSetup.js
@@ -551,7 +551,6 @@ jest.mock('react-native-mmkv', () => {
return {
MMKV,
- createMMKV: () => createInMemoryMMKV(),
};
});
diff --git a/app/util/test/testSetupView.js b/app/util/test/testSetupView.js
index 2a9a1c996649..b84b4be64093 100644
--- a/app/util/test/testSetupView.js
+++ b/app/util/test/testSetupView.js
@@ -48,7 +48,6 @@ jest.mock('react-native-mmkv', () => {
return {
MMKV,
- createMMKV: () => createInMemoryMMKV(),
};
});
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 3a7fd04819ee..e2def2310afd 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -556,7 +556,6 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - MMKVCore (2.4.0)
- MultiplatformBleAdapter (0.2.0)
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
@@ -593,37 +592,6 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - NitroMmkv (4.3.1):
- - boost
- - DoubleConversion
- - fast_float
- - fmt
- - glog
- - hermes-engine
- - MMKVCore (= 2.4.0)
- - NitroModules
- - RCT-Folly
- - RCT-Folly/Fabric
- - RCTRequired
- - RCTTypeSafety
- - React-callinvoker
- - React-Core
- - React-debug
- - React-Fabric
- - React-featureflags
- - React-graphics
- - React-ImageManager
- - React-jsi
- - React-NativeModulesApple
- - React-RCTFabric
- - React-renderercss
- - React-rendererdebug
- - React-utils
- - ReactCodegen
- - ReactCommon/turbomodule/bridging
- - ReactCommon/turbomodule/core
- - SocketRocket
- - Yoga
- NitroModules (0.35.5):
- boost
- DoubleConversion
@@ -2591,6 +2559,34 @@ PODS:
- Yoga
- react-native-launch-arguments (4.0.1):
- React
+ - react-native-mmkv (3.3.3):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
- react-native-netinfo (11.4.1):
- React-Core
- react-native-pager-view (6.9.1):
@@ -4167,7 +4163,6 @@ DEPENDENCIES:
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- lottie-react-native (from `../node_modules/lottie-react-native`)
- "NativeUtils (from `../node_modules/@metamask/native-utils`)"
- - NitroMmkv (from `../node_modules/react-native-mmkv`)
- NitroModules (from `../node_modules/react-native-nitro-modules`)
- OpenSSL-Universal
- Permission-BluetoothPeripheral (from `../node_modules/react-native-permissions/ios/BluetoothPeripheral`)
@@ -4219,6 +4214,7 @@ DEPENDENCIES:
- react-native-in-app-review (from `../node_modules/react-native-in-app-review`)
- react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`)
- react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`)
+ - react-native-mmkv (from `../node_modules/react-native-mmkv`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
- react-native-performance (from `../node_modules/react-native-performance`)
@@ -4320,7 +4316,6 @@ SPEC REPOS:
- libdav1d
- libwebp
- lottie-ios
- - MMKVCore
- MultiplatformBleAdapter
- nanopb
- OpenSSL-Universal
@@ -4418,8 +4413,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/lottie-react-native"
NativeUtils:
:path: "../node_modules/@metamask/native-utils"
- NitroMmkv:
- :path: "../node_modules/react-native-mmkv"
NitroModules:
:path: "../node_modules/react-native-nitro-modules"
Permission-BluetoothPeripheral:
@@ -4518,6 +4511,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-keyboard-controller"
react-native-launch-arguments:
:path: "../node_modules/react-native-launch-arguments"
+ react-native-mmkv:
+ :path: "../node_modules/react-native-mmkv"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-pager-view:
@@ -4732,11 +4727,9 @@ SPEC CHECKSUMS:
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3
lottie-react-native: 983fd0489530e8d40f173de7f04e2f88b9317a15
- MMKVCore: 3d16ce9f7d411e135020915fde98a056859a1efa
MultiplatformBleAdapter: b1fddd0d499b96b607e00f0faa8e60648343dc1d
nanopb: 438bc412db1928dac798aa6fd75726007be04262
NativeUtils: 1856148c08a042bbbca306acbe2771db8a9be99b
- NitroMmkv: c68bb2c29c154913707bc6e66791b2b30dd723c1
NitroModules: ea5dc6c43666f4a75e71e372eaca6d3605856e51
OpenSSL-Universal: 9110d21982bb7e8b22a962b6db56a8aa805afde7
Permission-BluetoothPeripheral: 34ab829f159c6cf400c57bac05f5ba1b0af7a86e
@@ -4789,6 +4782,7 @@ SPEC CHECKSUMS:
react-native-in-app-review: b3d1eed3d1596ebf6539804778272c4c65e4a400
react-native-keyboard-controller: 686d5aa0d02bfe7f83d733b98dd17de76463b01d
react-native-launch-arguments: 7eb321ed3f3ef19b3ec4a2eca71c4f9baee76b41
+ react-native-mmkv: ac7507625cd74bac0eb5333604a7cd7b08fe9e3e
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-pager-view: a0516effb17ca5120ac2113bfd21b91130ad5748
react-native-performance: b48d1aba7501a54af5e1a7efb0dab1ab25dc734f
diff --git a/package.json b/package.json
index 2d1c08419dc1..983144156cec 100644
--- a/package.json
+++ b/package.json
@@ -498,7 +498,7 @@
"react-native-level-fs": "3.0.1",
"react-native-linear-gradient": "^2.8.3",
"react-native-material-textfield": "0.16.1",
- "react-native-mmkv": "^4.1.2",
+ "react-native-mmkv": "^3.2.0",
"react-native-modal": "^14.0.0-rc.1",
"react-native-nitro-modules": "0.35.5",
"react-native-os": "patch:react-native-os@npm%3A1.2.6#~/.yarn/patches/react-native-os-npm-1.2.6-65c52ed3dc.patch",
diff --git a/yarn.lock b/yarn.lock
index 11c9ccab7261..7b1475fc469b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -35697,7 +35697,7 @@ __metadata:
react-native-level-fs: "npm:3.0.1"
react-native-linear-gradient: "npm:^2.8.3"
react-native-material-textfield: "npm:0.16.1"
- react-native-mmkv: "npm:^4.1.2"
+ react-native-mmkv: "npm:^3.2.0"
react-native-modal: "npm:^14.0.0-rc.1"
react-native-nitro-modules: "npm:0.35.5"
react-native-os: "patch:react-native-os@npm%3A1.2.6#~/.yarn/patches/react-native-os-npm-1.2.6-65c52ed3dc.patch"
@@ -40424,14 +40424,13 @@ __metadata:
languageName: node
linkType: hard
-"react-native-mmkv@npm:^4.1.2":
- version: 4.3.1
- resolution: "react-native-mmkv@npm:4.3.1"
+"react-native-mmkv@npm:^3.2.0":
+ version: 3.3.3
+ resolution: "react-native-mmkv@npm:3.3.3"
peerDependencies:
react: "*"
react-native: "*"
- react-native-nitro-modules: "*"
- checksum: 10/31608a0ee621a633b46d332ea24ae5697144ed305072b7a10722af3275bc9d5ba1db78eab6d6b64bd562c2d1655a7c227f6295b2f9b520a38460e004369523a8
+ checksum: 10/61c3d61db0bf8c311c99ae9ac6cf93071a2cf8771fc0e92af828938b50be7631834d608126182d13aa97ed059e202ffc31dbfe2d84cc760a21e65893dc6a7866
languageName: node
linkType: hard
From f9bcde94ffba1133f52d8c729f733f2211614c55 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Wed, 20 May 2026 11:34:07 +0000
Subject: [PATCH 30/66] [skip ci] Bump version number to 5093
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index dead934b882e..0489ed6a6289 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5074
+ versionCode 5093
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 0395e541dbde..ce9af6613345 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5074
+ VERSION_NUMBER: 5093
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5074
+ FLASK_VERSION_NUMBER: 5093
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index d06f84e6f20d..8ec218f80338 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5074;
+ CURRENT_PROJECT_VERSION = 5093;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5074;
+ CURRENT_PROJECT_VERSION = 5093;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5074;
+ CURRENT_PROJECT_VERSION = 5093;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5074;
+ CURRENT_PROJECT_VERSION = 5093;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 184d15d369b14e4c550d19d9e72024be48cafab1 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Wed, 20 May 2026 15:09:14 +0200
Subject: [PATCH 31/66] chore(runway): cherry-pick feat(Rewards): Use correct
source tokens for Ondo swaps when coming from Rewards (#30436)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- feat(Rewards): Use correct source tokens for Ondo swaps when coming
from Rewards (#30419)
## **Description**
This change pre-fills the correct source token when attempting to swap
into Ondo assets from the Rewards campaign.
## **Changelog**
CHANGELOG entry: made it easier to get started swapping Ondo GM assets
from Rewards
## **Related issues**
Fixes: n/a
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Medium Risk**
> Changes the token preselection logic passed into `goToSwaps` for
Rewards-driven Ondo swaps, which can impact swap routing and user
balances if chain/token mapping is wrong. Scope is contained to the Ondo
RWA selector and covered by updated unit tests.
>
> **Overview**
> Improves Rewards → Ondo *open position* flow by choosing an
appropriate prefilled swap source token based on the selected
destination asset’s chain: **prefer USDY on Ethereum when the user holds
a balance**, otherwise fall back to a per-chain default (`USDC` on
`eip155:1`, `USDT` on `eip155:56`).
>
> Updates the selector and after-hours confirmation paths to pass this
computed source into `goToSwaps`, and revises tests/mocks to validate
chain-aware defaults and remove the previous “undefined source”
expectations.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b1019e7acc6eaf6d41f8c02345f1ea99ec6dc5fe. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[d719f63](https://github.com/MetaMask/metamask-mobile/commit/d719f634d5143b2afeb7961ba9c1da3580ee5d18)
Co-authored-by: Christian Montoya
---
.../OndoCampaignRwaSelectorView.test.tsx | 67 +++++++++----------
.../Views/OndoCampaignRwaSelectorView.tsx | 52 +++++++++++---
2 files changed, 75 insertions(+), 44 deletions(-)
diff --git a/app/components/UI/Rewards/Views/OndoCampaignRwaSelectorView.test.tsx b/app/components/UI/Rewards/Views/OndoCampaignRwaSelectorView.test.tsx
index 6722d0745985..94cda6f8f94a 100644
--- a/app/components/UI/Rewards/Views/OndoCampaignRwaSelectorView.test.tsx
+++ b/app/components/UI/Rewards/Views/OndoCampaignRwaSelectorView.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { render, fireEvent, act } from '@testing-library/react-native';
+import { render, fireEvent } from '@testing-library/react-native';
import { useSelector } from 'react-redux';
import OndoCampaignRwaSelectorView from './OndoCampaignRwaSelectorView';
import type { TrendingAsset } from '@metamask/assets-controllers';
@@ -142,12 +142,19 @@ jest.mock('../../../../core/Analytics', () => ({
jest.mock('../utils/formatUtils', () => ({
...jest.requireActual('../utils/formatUtils'),
- parseCaip19: jest.fn(() => ({
- namespace: 'eip155',
- chainId: '1',
- assetReference: '0xabc',
- })),
- caipChainIdToHex: jest.fn(() => '0x1'),
+ parseCaip19: jest.fn((assetId: string) => {
+ const [chainId, assetPath] = assetId.split('/');
+ const [namespace, chainReference] = chainId.split(':');
+ const [, assetReference] = assetPath.split(':');
+ return {
+ namespace,
+ chainId: chainReference,
+ assetReference,
+ };
+ }),
+ caipChainIdToHex: jest.fn((caipChainId: string) =>
+ caipChainId === 'eip155:56' ? '0x38' : '0x1',
+ ),
}));
jest.mock(
@@ -470,22 +477,22 @@ describe('OndoCampaignRwaSelectorView', () => {
});
});
- describe('open_position mode — USDY source preselection', () => {
- const USDY_HEX_ADDRESS = '0xabc';
+ describe('open_position mode — source token preselection', () => {
+ const USDY_ADDRESS = '0x96f6ef951840721adbf46ac996b59e0235cb985c';
const ACCOUNT_ADDRESS = '0xaccount1';
beforeEach(() => {
mockRouteParams = { mode: 'open_position', campaignId: 'campaign-1' };
});
- it('passes USDY as source token when user holds a non-zero USDY balance', () => {
+ it('prefers USDY as the source token when user holds a non-zero USDY balance', () => {
mockUseRwaTokens.mockReturnValue({
- data: [buildToken('AAPL')],
+ data: [buildToken('AAPL', 'eip155:1/erc20:0xaapl')],
isLoading: false,
});
mockActiveGroupAccounts = [{ address: ACCOUNT_ADDRESS }];
mockAllTokenBalances = {
- [ACCOUNT_ADDRESS]: { '0x1': { [USDY_HEX_ADDRESS]: '0x64' } },
+ [ACCOUNT_ADDRESS]: { '0x1': { [USDY_ADDRESS]: '0x64' } },
};
const { getByTestId } = render();
@@ -498,40 +505,39 @@ describe('OndoCampaignRwaSelectorView', () => {
expect(destArg?.symbol).toBe('AAPL');
});
- it('passes undefined as source token when active group accounts are empty', () => {
+ it('falls back to USDC on mainnet as the source token for mainnet assets', () => {
mockUseRwaTokens.mockReturnValue({
- data: [buildToken('AAPL')],
+ data: [buildToken('AAPL', 'eip155:1/erc20:0xaapl')],
isLoading: false,
});
- mockActiveGroupAccounts = [];
const { getByTestId } = render();
fireEvent.press(getByTestId('token-row-AAPL'));
expect(mockGoToSwaps).toHaveBeenCalledTimes(1);
- const [srcArg] = mockGoToSwaps.mock.calls[0];
- expect(srcArg).toBeUndefined();
+ const [srcArg, destArg] = mockGoToSwaps.mock.calls[0];
+ expect(srcArg?.symbol).toBe('USDC');
+ expect(srcArg?.chainId).toBe('0x1');
+ expect(destArg?.symbol).toBe('AAPL');
});
- it('passes undefined as source token when USDY balance is zero', () => {
+ it('passes USDT on BNB Chain as the source token for BNB Chain assets', () => {
mockUseRwaTokens.mockReturnValue({
- data: [buildToken('AAPL')],
+ data: [buildToken('AAPL', 'eip155:56/erc20:0xaapl')],
isLoading: false,
});
- mockActiveGroupAccounts = [{ address: ACCOUNT_ADDRESS }];
- mockAllTokenBalances = {
- [ACCOUNT_ADDRESS]: { '0x1': { [USDY_HEX_ADDRESS]: '0x0' } },
- };
const { getByTestId } = render();
fireEvent.press(getByTestId('token-row-AAPL'));
expect(mockGoToSwaps).toHaveBeenCalledTimes(1);
- const [srcArg] = mockGoToSwaps.mock.calls[0];
- expect(srcArg).toBeUndefined();
+ const [srcArg, destArg] = mockGoToSwaps.mock.calls[0];
+ expect(srcArg?.symbol).toBe('USDT');
+ expect(srcArg?.chainId).toBe('0x38');
+ expect(destArg?.chainId).toBe('eip155:56');
});
- it('does not preset USDY as source in swap mode even when user holds balance', () => {
+ it('does not preset an open-position source in swap mode', () => {
mockRouteParams = {
mode: 'swap',
campaignId: 'campaign-1',
@@ -543,10 +549,6 @@ describe('OndoCampaignRwaSelectorView', () => {
data: [buildToken('AAPL')],
isLoading: false,
});
- mockActiveGroupAccounts = [{ address: ACCOUNT_ADDRESS }];
- mockAllTokenBalances = {
- [ACCOUNT_ADDRESS]: { '0x1': { [USDY_HEX_ADDRESS]: '0x64' } },
- };
const { getByTestId } = render();
fireEvent.press(getByTestId('token-row-AAPL'));
@@ -580,7 +582,7 @@ describe('OndoCampaignRwaSelectorView', () => {
const { getByTestId } = render();
fireEvent.press(getByTestId('token-row-AAPL'));
expect(mockGoToSwaps).toHaveBeenCalledWith(
- undefined,
+ expect.objectContaining({ symbol: 'USDC' }),
expect.objectContaining({ name: 'Apple (Ondo Tokenized)' }),
);
});
@@ -709,6 +711,3 @@ describe('OndoCampaignRwaSelectorView', () => {
});
});
});
-
-// keep the act import used in other test files from triggering "unused import" lint
-void act;
diff --git a/app/components/UI/Rewards/Views/OndoCampaignRwaSelectorView.tsx b/app/components/UI/Rewards/Views/OndoCampaignRwaSelectorView.tsx
index 97ee6326fb70..9aed7107db0c 100644
--- a/app/components/UI/Rewards/Views/OndoCampaignRwaSelectorView.tsx
+++ b/app/components/UI/Rewards/Views/OndoCampaignRwaSelectorView.tsx
@@ -62,13 +62,35 @@ import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
import { MetaMetricsEvents } from '../../../../core/Analytics';
import useTrackRewardsPageView from '../hooks/useTrackRewardsPageView';
-// USDY (Ondo USD Yield) on Ethereum mainnet — used to preset the source token
-// for open_position mode. This is the only network where USDY is supported in
-// the RWA campaign feature.
+// USDY (Ondo USD Yield) on Ethereum mainnet — preferred source token for
+// open_position mode when the active account group holds a balance.
const USDY_CAIP19 =
'eip155:1/erc20:0x96f6ef951840721adbf46ac996b59e0235cb985c' as const;
const USDY_DECIMALS = 18;
+const ONDO_OPEN_POSITION_SOURCE_TOKENS: Partial<
+ Record
+> = {
+ 'eip155:1': {
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ symbol: 'USDC',
+ name: 'USD Coin',
+ decimals: 6,
+ chainId: '0x1',
+ image:
+ 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.png',
+ },
+ 'eip155:56': {
+ address: '0x55d398326f99059ff775485246999027b3197955',
+ symbol: 'USDT',
+ name: 'Tether USD',
+ decimals: 18,
+ chainId: '0x38',
+ image:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/56/0x55d398326f99059ff775485246999027b3197955.png',
+ },
+};
+
// ParamListBase requires an index signature, which interfaces don't support
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type OndoCampaignRwaSelectorRouteParams = {
@@ -213,10 +235,6 @@ const OndoCampaignRwaSelectorView: React.FC = () => {
);
const allTokenBalances = useSelector(selectAllTokenBalances);
- // In open_position mode, preset USDY as the source if the user holds a balance.
- // Uses a hardcoded CAIP-19 so the preset is independent of the search state —
- // rwaTokens is filtered by searchQuery and may not contain USDY when a user
- // searches for another token (e.g. "AAPL").
const ondoUsdSrcToken = useMemo((): BridgeToken | undefined => {
if (mode !== 'open_position' || !activeGroupAccounts.length)
return undefined;
@@ -275,15 +293,21 @@ const OndoCampaignRwaSelectorView: React.FC = () => {
(asset: TrendingAsset) => {
const parsed = parseCaip19(asset.assetId);
if (!parsed) return;
+ const destChainId =
+ `${parsed.namespace}:${parsed.chainId}` as CaipChainId;
const destToken: BridgeToken = {
address: parsed.assetReference,
symbol: asset.symbol,
name: asset.name,
decimals: asset.decimals,
- chainId: `${parsed.namespace}:${parsed.chainId}` as CaipChainId,
+ chainId: destChainId,
image: getTrendingTokenImageUrl(asset.assetId),
rwaData: asset.rwaData as BridgeToken['rwaData'],
};
+ const openPositionSourceToken =
+ mode === 'open_position'
+ ? (ondoUsdSrcToken ?? ONDO_OPEN_POSITION_SOURCE_TOKENS[destChainId])
+ : undefined;
if (!isTokenTradingOpen(destToken)) {
const rawNextOpen = destToken.rwaData?.market?.nextOpen;
@@ -303,9 +327,10 @@ const OndoCampaignRwaSelectorView: React.FC = () => {
})
.build(),
);
- goToSwaps(ondoUsdSrcToken, destToken);
+ goToSwaps(openPositionSourceToken, destToken);
},
[
+ mode,
goToSwaps,
isTokenTradingOpen,
trackEvent,
@@ -454,6 +479,13 @@ const OndoCampaignRwaSelectorView: React.FC = () => {
setIsAfterHoursSheetOpen(false);
setAfterHoursNextOpen(null);
if (afterHoursPendingToken) {
+ const openPositionSourceToken =
+ mode === 'open_position'
+ ? (ondoUsdSrcToken ??
+ ONDO_OPEN_POSITION_SOURCE_TOKENS[
+ afterHoursPendingToken.chainId as CaipChainId
+ ])
+ : undefined;
trackEvent(
createEventBuilder(
MetaMetricsEvents.REWARDS_PAGE_BUTTON_CLICKED,
@@ -463,7 +495,7 @@ const OndoCampaignRwaSelectorView: React.FC = () => {
})
.build(),
);
- goToSwaps(ondoUsdSrcToken, afterHoursPendingToken);
+ goToSwaps(openPositionSourceToken, afterHoursPendingToken);
}
setAfterHoursPendingToken(null);
}}
From e4ea25eaae46fbea0a75ddb26fd776f0232bde60 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Wed, 20 May 2026 13:11:42 +0000
Subject: [PATCH 32/66] [skip ci] Bump version number to 5094
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 0489ed6a6289..22ba8de6fccc 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5093
+ versionCode 5094
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index ce9af6613345..e8c514a25b56 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5093
+ VERSION_NUMBER: 5094
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5093
+ FLASK_VERSION_NUMBER: 5094
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 8ec218f80338..ce8de241a0ee 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5093;
+ CURRENT_PROJECT_VERSION = 5094;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5093;
+ CURRENT_PROJECT_VERSION = 5094;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5093;
+ CURRENT_PROJECT_VERSION = 5094;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5093;
+ CURRENT_PROJECT_VERSION = 5094;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From dc8b7c9d3fbe5f95c689b89e853fa75d1f36e48b Mon Sep 17 00:00:00 2001
From: chloeYue <105063779+chloeYue@users.noreply.github.com>
Date: Wed, 20 May 2026 18:37:21 +0200
Subject: [PATCH 33/66] chore: sync stable into release/7.78.0 (post 7.77.2)
(#30459)
## **Description**
This sync brings `stable` back into `release/7.78.0`
## **Changelog**
CHANGELOG entry: null
---
> [!NOTE]
> **Low Risk**
> Low risk documentation-only change updating release notes and version
compare links; no runtime code is modified.
>
> **Overview**
> Adds a new `7.77.2` **Fixed** entry to the `CHANGELOG.md` for a
Hyperliquid perps `deposit-and-order` Relay deposit-flow routing bugfix.
>
> Updates the changelog compare links so `7.78.0` now compares from
`v7.77.2`, and introduces the `7.77.2` compare link.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
7998b5875523102a97f8d4488d9b785818e1f9f4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---------
Co-authored-by: metamaskbot
Co-authored-by: runway-github[bot] <73448015+runway-github[bot]@users.noreply.github.com>
Co-authored-by: Matthew Walsh
Co-authored-by: Cursor
---
CHANGELOG.md | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44bc700de1d1..b8523f0f1116 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -54,6 +54,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the Account List opening too quickly. (#29859)
- Fixed an issue where EIP-7702 authorization signatures with leading zero bytes in `r` or `s` could be rejected by relays and public RPCs. (#29717)
+## [7.77.2]
+
+### Fixed
+
+- Fixed Hyperliquid perps `deposit-and-order` transactions not routing through the correct Relay deposit flow. (#30407)
+
## [7.77.1]
### Added
@@ -11552,7 +11558,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#954](https://github.com/MetaMask/metamask-mobile/pull/954): Bugfix: onboarding navigation (#954)
[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.78.0...HEAD
-[7.78.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.1...v7.78.0
+[7.78.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.2...v7.78.0
+[7.77.2]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.1...v7.77.2
[7.77.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.0...v7.77.1
[7.77.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.3...v7.77.0
[7.76.3]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.0...v7.76.3
From b6bad22d2c7772f04925f878c73bf46553fe05ba Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Wed, 20 May 2026 18:54:30 +0200
Subject: [PATCH 34/66] chore(runway): cherry-pick chore: bump
@metamask/transaction-pay-controller from ^22.5.0 to ^22.6.1 (#30447)
- chore: bump @metamask/transaction-pay-controller from ^22.5.0 to
^22.6.1 (#30407)
## **Description**
Bump `@metamask/transaction-pay-controller` from `^22.5.0` to `^22.6.1`.
## **Changelog**
CHANGELOG entry: null
## **Related issues**
## **Manual testing steps**
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- [ ] I've tested with a power user scenario
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
## **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]
> **Medium Risk**
> Updates a core payments/transaction dependency and refreshes several
related MetaMask controller packages, which could subtly affect
transaction/bridge behavior at runtime despite being a version bump
only.
>
> **Overview**
> Bumps **`@metamask/transaction-pay-controller`** from `^22.5.0` to
`^22.6.1` in `package.json` and updates `yarn.lock` accordingly.
>
> The lockfile refresh also pulls newer related controller versions
(notably `@metamask/bridge-controller`,
`@metamask/bridge-status-controller`, `@metamask/gas-fee-controller`,
and `@metamask/transaction-controller`), aligning transitive
dependencies with the updated pay controller.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3b7622df9d9f41221bc170b52b04bbfec285ce5b. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[72e0e00](https://github.com/MetaMask/metamask-mobile/commit/72e0e00248af23ab627059bb74cb4182332847f4)
Co-authored-by: Matthew Walsh
---
package.json | 2 +-
yarn.lock | 161 ++++++++++++++++++++++++++++++++++++++-------------
2 files changed, 123 insertions(+), 40 deletions(-)
diff --git a/package.json b/package.json
index 983144156cec..47e823c77f46 100644
--- a/package.json
+++ b/package.json
@@ -349,7 +349,7 @@
"@metamask/superstruct": "^3.2.1",
"@metamask/swappable-obj-proxy": "^2.1.0",
"@metamask/transaction-controller": "^65.4.0",
- "@metamask/transaction-pay-controller": "^22.5.0",
+ "@metamask/transaction-pay-controller": "^22.6.1",
"@metamask/tron-wallet-snap": "^1.25.3",
"@metamask/utils": "^11.11.0",
"@myx-trade/sdk": "^0.1.265",
diff --git a/yarn.lock b/yarn.lock
index 7b1475fc469b..9dc6718399ad 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7997,6 +7997,39 @@ __metadata:
languageName: node
linkType: hard
+"@metamask/bridge-controller@npm:^73.0.0":
+ version: 73.0.0
+ resolution: "@metamask/bridge-controller@npm:73.0.0"
+ dependencies:
+ "@ethersproject/address": "npm:^5.7.0"
+ "@ethersproject/bignumber": "npm:^5.7.0"
+ "@ethersproject/constants": "npm:^5.7.0"
+ "@ethersproject/contracts": "npm:^5.7.0"
+ "@ethersproject/providers": "npm:^5.7.0"
+ "@metamask/accounts-controller": "npm:^38.1.1"
+ "@metamask/assets-controller": "npm:^7.1.2"
+ "@metamask/assets-controllers": "npm:^108.1.0"
+ "@metamask/base-controller": "npm:^9.1.0"
+ "@metamask/controller-utils": "npm:^12.1.0"
+ "@metamask/gas-fee-controller": "npm:^26.2.2"
+ "@metamask/keyring-api": "npm:^23.1.0"
+ "@metamask/messenger": "npm:^1.2.0"
+ "@metamask/metamask-eth-abis": "npm:^3.1.1"
+ "@metamask/multichain-network-controller": "npm:^3.1.2"
+ "@metamask/network-controller": "npm:^32.0.0"
+ "@metamask/polling-controller": "npm:^16.0.6"
+ "@metamask/profile-sync-controller": "npm:^28.1.0"
+ "@metamask/remote-feature-flag-controller": "npm:^4.2.1"
+ "@metamask/snaps-controllers": "npm:^19.0.0"
+ "@metamask/transaction-controller": "npm:^66.0.0"
+ "@metamask/utils": "npm:^11.9.0"
+ bignumber.js: "npm:^9.1.2"
+ reselect: "npm:^5.1.1"
+ uuid: "npm:^8.3.2"
+ checksum: 10/4123f2eafe3e2bcdaad8d7b1e98d27378c55d17fd003ea2157243af346943a0c7659037a0e8f1b1468e0b411ac0c0552953c8ffb61d66d999b1aab08cf1e2def
+ languageName: node
+ linkType: hard
+
"@metamask/bridge-status-controller@npm:71.1.0":
version: 71.1.0
resolution: "@metamask/bridge-status-controller@npm:71.1.0"
@@ -8021,27 +8054,27 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/bridge-status-controller@npm:^71.1.4":
- version: 71.1.4
- resolution: "@metamask/bridge-status-controller@npm:71.1.4"
+"@metamask/bridge-status-controller@npm:^71.2.0":
+ version: 71.2.0
+ resolution: "@metamask/bridge-status-controller@npm:71.2.0"
dependencies:
"@metamask/accounts-controller": "npm:^38.1.1"
"@metamask/base-controller": "npm:^9.1.0"
"@metamask/bridge-controller": "npm:^72.0.4"
"@metamask/controller-utils": "npm:^12.1.0"
- "@metamask/gas-fee-controller": "npm:^26.2.1"
+ "@metamask/gas-fee-controller": "npm:^26.2.2"
"@metamask/keyring-controller": "npm:^25.5.0"
"@metamask/messenger": "npm:^1.2.0"
"@metamask/network-controller": "npm:^32.0.0"
- "@metamask/polling-controller": "npm:^16.0.5"
- "@metamask/profile-sync-controller": "npm:^28.0.2"
+ "@metamask/polling-controller": "npm:^16.0.6"
+ "@metamask/profile-sync-controller": "npm:^28.1.0"
"@metamask/snaps-controllers": "npm:^19.0.0"
"@metamask/superstruct": "npm:^3.1.0"
- "@metamask/transaction-controller": "npm:^65.3.0"
+ "@metamask/transaction-controller": "npm:^66.0.0"
"@metamask/utils": "npm:^11.9.0"
bignumber.js: "npm:^9.1.2"
uuid: "npm:^8.3.2"
- checksum: 10/b07f01cded7f1748e2e08ff2d89941f9a40c6a387d2cd4e214ea9858f3ca6ef1b8a1ce3a7a56b7cfb7200232904ed3b7a6dddecd085cb45bbe2425647d8b331b
+ checksum: 10/6a2587741016e95c6a3d9e4c046046a4100757faf47085c50b8328460940ceb7dc341492a3e604c75705f00967441723ff1e120f64024b5b4cf0e860f5f74652
languageName: node
linkType: hard
@@ -8789,17 +8822,17 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/gas-fee-controller@npm:^26.0.3, @metamask/gas-fee-controller@npm:^26.1.0, @metamask/gas-fee-controller@npm:^26.1.1, @metamask/gas-fee-controller@npm:^26.2.1":
- version: 26.2.1
- resolution: "@metamask/gas-fee-controller@npm:26.2.1"
+"@metamask/gas-fee-controller@npm:^26.0.3, @metamask/gas-fee-controller@npm:^26.1.0, @metamask/gas-fee-controller@npm:^26.1.1, @metamask/gas-fee-controller@npm:^26.2.1, @metamask/gas-fee-controller@npm:^26.2.2":
+ version: 26.2.2
+ resolution: "@metamask/gas-fee-controller@npm:26.2.2"
dependencies:
"@metamask/base-controller": "npm:^9.1.0"
- "@metamask/controller-utils": "npm:^12.0.0"
+ "@metamask/controller-utils": "npm:^12.1.0"
"@metamask/eth-query": "npm:^4.0.0"
"@metamask/ethjs-unit": "npm:^0.3.0"
"@metamask/messenger": "npm:^1.2.0"
- "@metamask/network-controller": "npm:^31.0.0"
- "@metamask/polling-controller": "npm:^16.0.5"
+ "@metamask/network-controller": "npm:^32.0.0"
+ "@metamask/polling-controller": "npm:^16.0.6"
"@metamask/utils": "npm:^11.9.0"
"@types/bn.js": "npm:^5.1.5"
"@types/uuid": "npm:^8.3.0"
@@ -8807,7 +8840,7 @@ __metadata:
uuid: "npm:^8.3.2"
peerDependencies:
"@babel/runtime": ^7.0.0
- checksum: 10/aa7fe8b2112baef79219d72c8c7ef020846ce1c1f94cb17c55497dcb539d473a1f054365068f05a6afa51a71ca4e810e104f7c0e7260b8c99d856dc59dac2475
+ checksum: 10/deba95b314bd7fe9908743f3905873bde204d0e26c3863201b884aff59f5dd974919f7c0b024143de53faf846e6cece0e99b4a065c8322721c375d6c11181be1
languageName: node
linkType: hard
@@ -9294,22 +9327,22 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/multichain-network-controller@npm:^3.1.0, @metamask/multichain-network-controller@npm:^3.1.1":
- version: 3.1.1
- resolution: "@metamask/multichain-network-controller@npm:3.1.1"
+"@metamask/multichain-network-controller@npm:^3.1.0, @metamask/multichain-network-controller@npm:^3.1.1, @metamask/multichain-network-controller@npm:^3.1.2":
+ version: 3.1.2
+ resolution: "@metamask/multichain-network-controller@npm:3.1.2"
dependencies:
- "@metamask/accounts-controller": "npm:^38.1.0"
+ "@metamask/accounts-controller": "npm:^38.1.1"
"@metamask/base-controller": "npm:^9.1.0"
- "@metamask/controller-utils": "npm:^12.0.0"
+ "@metamask/controller-utils": "npm:^12.1.0"
"@metamask/keyring-api": "npm:^23.1.0"
"@metamask/keyring-internal-api": "npm:^11.0.1"
"@metamask/messenger": "npm:^1.2.0"
- "@metamask/network-controller": "npm:^31.0.0"
+ "@metamask/network-controller": "npm:^32.0.0"
"@metamask/superstruct": "npm:^3.1.0"
"@metamask/utils": "npm:^11.9.0"
"@solana/addresses": "npm:^2.0.0"
lodash: "npm:^4.17.21"
- checksum: 10/1c17020e9c2d6c8b50f182900730e7c59b23f6ad0e467be1082242ba089b5bf931371bc966f5965fcc611330da1724ddb92062c87a145d3b69a99e7458fe1088
+ checksum: 10/8c9e97d4be3367da76f17c44a80a25e54ec48fb1fcc9c086ae05e500a19524813b5ceec105ce2b4565fa24a8e6a1dba5f305341202ea8afa10def5d8d16106da
languageName: node
linkType: hard
@@ -9492,18 +9525,18 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/polling-controller@npm:^16.0.0, @metamask/polling-controller@npm:^16.0.3, @metamask/polling-controller@npm:^16.0.4, @metamask/polling-controller@npm:^16.0.5":
- version: 16.0.5
- resolution: "@metamask/polling-controller@npm:16.0.5"
+"@metamask/polling-controller@npm:^16.0.0, @metamask/polling-controller@npm:^16.0.3, @metamask/polling-controller@npm:^16.0.4, @metamask/polling-controller@npm:^16.0.5, @metamask/polling-controller@npm:^16.0.6":
+ version: 16.0.6
+ resolution: "@metamask/polling-controller@npm:16.0.6"
dependencies:
"@metamask/base-controller": "npm:^9.1.0"
- "@metamask/controller-utils": "npm:^12.0.0"
- "@metamask/network-controller": "npm:^31.0.0"
+ "@metamask/controller-utils": "npm:^12.1.0"
+ "@metamask/network-controller": "npm:^32.0.0"
"@metamask/utils": "npm:^11.9.0"
"@types/uuid": "npm:^8.3.0"
fast-json-stable-stringify: "npm:^2.1.0"
uuid: "npm:^8.3.2"
- checksum: 10/0fa32076672860e5bc27ac2720223b0f4199aaae9c952c5ec74b53ad49248705cc63a9b07dd25219512c348a85f87d04038f94a45eb2d2219af14d941f327275
+ checksum: 10/12990b447a5188072e5614b17ae6baa1f30911878f995e566387f53e4309d8729a6768b61d1e042462b9d65541c3f0d7caf1df3aeb8a41756bbef7bafd1b8dab
languageName: node
linkType: hard
@@ -9620,7 +9653,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/ramps-controller@npm:^13.2.0, @metamask/ramps-controller@npm:^13.3.1":
+"@metamask/ramps-controller@npm:^13.2.0":
version: 13.3.1
resolution: "@metamask/ramps-controller@npm:13.3.1"
dependencies:
@@ -9631,6 +9664,18 @@ __metadata:
languageName: node
linkType: hard
+"@metamask/ramps-controller@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "@metamask/ramps-controller@npm:14.0.0"
+ dependencies:
+ "@metamask/base-controller": "npm:^9.1.0"
+ "@metamask/controller-utils": "npm:^12.1.0"
+ "@metamask/messenger": "npm:^1.2.0"
+ "@metamask/profile-sync-controller": "npm:^28.1.0"
+ checksum: 10/b00541d2be062af2170a70004ee4b19b850abe271f77077057d26e9c019191202474ebedb15d0a3182ffd2957a1ae3e19ea103d080360e69c94d36e39f326866
+ languageName: node
+ linkType: hard
+
"@metamask/react-data-query@npm:^0.2.0":
version: 0.2.0
resolution: "@metamask/react-data-query@npm:0.2.0"
@@ -10427,9 +10472,47 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/transaction-pay-controller@npm:^22.5.0":
- version: 22.5.0
- resolution: "@metamask/transaction-pay-controller@npm:22.5.0"
+"@metamask/transaction-controller@npm:^66.0.0":
+ version: 66.0.0
+ resolution: "@metamask/transaction-controller@npm:66.0.0"
+ dependencies:
+ "@ethereumjs/common": "npm:^4.4.0"
+ "@ethereumjs/tx": "npm:^5.4.0"
+ "@ethereumjs/util": "npm:^9.1.0"
+ "@ethersproject/abi": "npm:^5.7.0"
+ "@ethersproject/contracts": "npm:^5.7.0"
+ "@ethersproject/providers": "npm:^5.7.0"
+ "@ethersproject/wallet": "npm:^5.7.0"
+ "@metamask/accounts-controller": "npm:^38.1.1"
+ "@metamask/approval-controller": "npm:^9.0.1"
+ "@metamask/base-controller": "npm:^9.1.0"
+ "@metamask/controller-utils": "npm:^12.1.0"
+ "@metamask/core-backend": "npm:^6.3.0"
+ "@metamask/gas-fee-controller": "npm:^26.2.2"
+ "@metamask/messenger": "npm:^1.2.0"
+ "@metamask/metamask-eth-abis": "npm:^3.1.1"
+ "@metamask/network-controller": "npm:^32.0.0"
+ "@metamask/nonce-tracker": "npm:^6.0.0"
+ "@metamask/remote-feature-flag-controller": "npm:^4.2.1"
+ "@metamask/rpc-errors": "npm:^7.0.2"
+ "@metamask/utils": "npm:^11.9.0"
+ async-mutex: "npm:^0.5.0"
+ bignumber.js: "npm:^9.1.2"
+ bn.js: "npm:^5.2.1"
+ eth-method-registry: "npm:^4.0.0"
+ fast-json-patch: "npm:^3.1.1"
+ lodash: "npm:^4.17.21"
+ uuid: "npm:^8.3.2"
+ peerDependencies:
+ "@babel/runtime": ^7.0.0
+ "@metamask/eth-block-tracker": ">=9"
+ checksum: 10/3b8a6606dd4b5005818764eb193dd4cf9f77c5fb7b1295cb5f049413adedced4f22d3fba7653bc23519195946d8f111775993ae12d3965cbef5cca12fad2e97d
+ languageName: node
+ linkType: hard
+
+"@metamask/transaction-pay-controller@npm:^22.6.1":
+ version: 22.6.2
+ resolution: "@metamask/transaction-pay-controller@npm:22.6.2"
dependencies:
"@ethersproject/abi": "npm:^5.7.0"
"@ethersproject/contracts": "npm:^5.7.0"
@@ -10437,22 +10520,22 @@ __metadata:
"@metamask/assets-controller": "npm:^7.1.2"
"@metamask/assets-controllers": "npm:^108.1.0"
"@metamask/base-controller": "npm:^9.1.0"
- "@metamask/bridge-controller": "npm:^72.0.4"
- "@metamask/bridge-status-controller": "npm:^71.1.4"
+ "@metamask/bridge-controller": "npm:^73.0.0"
+ "@metamask/bridge-status-controller": "npm:^71.2.0"
"@metamask/controller-utils": "npm:^12.1.0"
- "@metamask/gas-fee-controller": "npm:^26.2.1"
+ "@metamask/gas-fee-controller": "npm:^26.2.2"
"@metamask/messenger": "npm:^1.2.0"
"@metamask/metamask-eth-abis": "npm:^3.1.1"
"@metamask/network-controller": "npm:^32.0.0"
- "@metamask/ramps-controller": "npm:^13.3.1"
+ "@metamask/ramps-controller": "npm:^14.0.0"
"@metamask/remote-feature-flag-controller": "npm:^4.2.1"
- "@metamask/transaction-controller": "npm:^65.4.0"
+ "@metamask/transaction-controller": "npm:^66.0.0"
"@metamask/utils": "npm:^11.9.0"
bignumber.js: "npm:^9.1.2"
bn.js: "npm:^5.2.1"
immer: "npm:^9.0.6"
lodash: "npm:^4.17.21"
- checksum: 10/611103e0f4a8c2783f8196302830d9befee8973f64e0fdb050f658de3519c6dc01c1bf104788b2d726e8f22e22ed529ac19b7b3e9d53aea945e6676d71bef7e2
+ checksum: 10/70da941dbdaa3e46fad13a5ed9daa4228b68e30823c41599d3e030361fdd1826673f78eea731ca0dccea6a8174d841241bb235889523335b5ec0af00ae75c05e
languageName: node
linkType: hard
@@ -35442,7 +35525,7 @@ __metadata:
"@metamask/test-dapp-multichain": "npm:^0.17.1"
"@metamask/test-dapp-solana": "npm:^0.3.0"
"@metamask/transaction-controller": "npm:^65.4.0"
- "@metamask/transaction-pay-controller": "npm:^22.5.0"
+ "@metamask/transaction-pay-controller": "npm:^22.6.1"
"@metamask/tron-wallet-snap": "npm:^1.25.3"
"@metamask/utils": "npm:^11.11.0"
"@myx-trade/sdk": "npm:^0.1.265"
From b698181b937f32967a2dc54257c284fae5f4f26c Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Wed, 20 May 2026 20:26:06 +0200
Subject: [PATCH 35/66] chore(runway): cherry-pick fix: cp-7.78.0 add
`isAtomicBatchSupported` action to `TransactionPayController` init messenger
(#30455)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- fix: cp-7.78.0 add `isAtomicBatchSupported` action to
`TransactionPayController` init messenger (#30425)
## **Description**
Add `TransactionControllerIsAtomicBatchSupportedAction` to the
`TransactionPayControllerInit` messenger. This enables the init
messenger to delegate the `TransactionController:isAtomicBatchSupported`
action, which is needed during controller initialization to check atomic
batch support.
Companion to MetaMask/metamask-extension#42809.
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Fixes: #42781
## **Manual testing steps**
1. Build the app with `yarn start:ios` or `yarn start:android`
2. Verify the app loads without errors
3. Verify transaction pay functionality works as expected
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [x] 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-extension/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]
> **Low Risk**
> Low risk: adds a single delegated messenger action used during
initialization, with no changes to transaction execution or signing
behavior.
>
> **Overview**
> Allows `TransactionPayController` initialization to query atomic batch
capability by **adding delegation for**
`TransactionController:isAtomicBatchSupported`.
>
> This extends `TransactionPayControllerInit` messenger types/imports to
include `TransactionControllerIsAtomicBatchSupportedAction` and
delegates the new action via `rootMessenger.delegate`.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b81e10b7d15eaac8642fb3e9c3595dee5bdf8467. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[2b27a4f](https://github.com/MetaMask/metamask-mobile/commit/2b27a4f46ff4bf2f77c96a382a69f2219e232f53)
Co-authored-by: Ömer Göktuğ Poyraz
---
.../transaction-pay-controller-messenger.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts b/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts
index 52ad9f30c6d0..3ddb443b2fef 100644
--- a/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts
+++ b/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts
@@ -11,6 +11,7 @@ import {
KeyringControllerSignPersonalMessageAction,
KeyringControllerSignTypedMessageAction,
} from '@metamask/keyring-controller';
+import { TransactionControllerIsAtomicBatchSupportedAction } from '@metamask/transaction-controller';
export function getTransactionPayControllerMessenger(
rootMessenger: RootMessenger,
@@ -65,7 +66,8 @@ type InitMessengerActions =
| DelegationControllerSignDelegationAction
| KeyringControllerSignEip7702AuthorizationAction
| KeyringControllerSignPersonalMessageAction
- | KeyringControllerSignTypedMessageAction;
+ | KeyringControllerSignTypedMessageAction
+ | TransactionControllerIsAtomicBatchSupportedAction;
type InitMessengerEvents = never;
export type TransactionPayControllerInitMessenger = ReturnType<
@@ -91,6 +93,7 @@ export function getTransactionPayControllerInitMessenger(
'KeyringController:signEip7702Authorization',
'KeyringController:signPersonalMessage',
'KeyringController:signTypedMessage',
+ 'TransactionController:isAtomicBatchSupported',
],
events: [],
messenger,
From e5dc36415f3120a8c4f2eb828702cd06c0fa07a3 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Wed, 20 May 2026 18:28:01 +0000
Subject: [PATCH 36/66] [skip ci] Bump version number to 5110
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 22ba8de6fccc..b9fe3bfb40bd 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5094
+ versionCode 5110
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index e8c514a25b56..963fea5fee7e 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5094
+ VERSION_NUMBER: 5110
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5094
+ FLASK_VERSION_NUMBER: 5110
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index ce8de241a0ee..e3578a649f3e 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5094;
+ CURRENT_PROJECT_VERSION = 5110;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5094;
+ CURRENT_PROJECT_VERSION = 5110;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5094;
+ CURRENT_PROJECT_VERSION = 5110;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5094;
+ CURRENT_PROJECT_VERSION = 5110;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 84e699109fc0ddadd57f0302cb88c754b16f9876 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Wed, 20 May 2026 21:55:33 +0200
Subject: [PATCH 37/66] chore(runway): cherry-pick fix: handle Perps withdraw
batch initialization errors cp-7.78.0 (#30469)
- fix: handle Perps withdraw batch initialization errors (#30299)
## **Description**
Fixes a Perps Withdraw loading state where users could get stuck on the
confirmation skeleton if transaction batch initialization failed before
an approval request was created.
The withdraw flow intentionally navigates to Confirmations early and
shows a skeleton loader while the transaction batch is created. This PR
adds the same failure escape hatch used by similar mobile flows: if
`addTransactionBatch` fails, the app navigates back from the
confirmation loader and shows the existing withdrawal failed toast.
## **Changelog**
CHANGELOG entry: Fixed a bug that caused Perps Withdraw to stay stuck on
a loading screen when withdrawal initialization failed
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/CONF-1392
## **Manual testing steps**
```gherkin
Feature: Perps Withdraw error handling
Scenario: user sees an error instead of a stuck loading screen
Given the user is on the Perps home screen
And Perps withdraw to any token is enabled
And withdrawal transaction batch initialization fails
When user taps Withdraw
Then the confirmation skeleton is dismissed
And the user is returned to the previous Perps screen
And a withdrawal failed toast is shown
```
## **Screenshots/Recordings**
### **Before**
Users could remain stuck on the confirmation skeleton loader.
### **After**
The app navigates back and shows the withdrawal failed toast.
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- [ ] I've tested with a power user scenario
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
## **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]
> **Medium Risk**
> Changes Perps withdraw confirmation flow to handle
`addTransactionBatch` failures by navigating back and showing a retry
action, which affects user navigation and error handling paths. Risk is
moderate due to potential edge cases around retry loops and
toast/navigation state.
>
> **Overview**
> Prevents Perps withdraw from getting stuck on the confirmation
skeleton by catching `addTransactionBatch` failures in
`usePerpsWithdrawConfirmation`, navigating back, and showing a new
**retryable** `withdrawalStartFailed` toast that re-attempts
initialization.
>
> Adds the `withdrawalStartFailed(onRetry)` toast option (styled as an
error with a "Try again" link) plus new i18n strings, and
updates/extends unit tests and mocks to cover the failure + retry
behavior and error normalization.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
a7ae72a160a1fe12af64ab72c3f81ce09322b7f0. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---------
Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com>
[b5b803d](https://github.com/MetaMask/metamask-mobile/commit/b5b803d7146c496c631850ad9cc4d7d60e406eaf)
Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com>
Co-authored-by: Daniel <80175477+dan437@users.noreply.github.com>
---
.../Perps/hooks/usePerpsDepositStatus.test.ts | 10 +++
.../UI/Perps/hooks/usePerpsToasts.test.tsx | 59 +++++++++++-
.../UI/Perps/hooks/usePerpsToasts.tsx | 16 ++++
.../usePerpsWithdrawConfirmation.test.ts | 90 ++++++++++++++++++-
.../hooks/usePerpsWithdrawConfirmation.ts | 75 +++++++++++-----
.../hooks/usePerpsWithdrawStatus.test.ts | 10 +++
.../confirmations-developer-options.test.tsx | 8 +-
locales/languages/en.json | 4 +-
8 files changed, 245 insertions(+), 27 deletions(-)
diff --git a/app/components/UI/Perps/hooks/usePerpsDepositStatus.test.ts b/app/components/UI/Perps/hooks/usePerpsDepositStatus.test.ts
index b90a4c841ca0..11ee73fc6dd1 100644
--- a/app/components/UI/Perps/hooks/usePerpsDepositStatus.test.ts
+++ b/app/components/UI/Perps/hooks/usePerpsDepositStatus.test.ts
@@ -198,6 +198,16 @@ describe('usePerpsDepositStatus', () => {
],
hapticsType: NotificationMoment.Error,
})),
+ withdrawalStartFailed: jest.fn(() => ({
+ variant: ToastVariants.Icon,
+ iconName: IconName.Warning,
+ hasNoTimeout: false,
+ labelOptions: [
+ { label: 'Something went wrong', isBold: true },
+ { label: 'Your withdrawal was not started' },
+ ],
+ hapticsType: NotificationMoment.Error,
+ })),
},
},
// Add minimal stubs for other required properties
diff --git a/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx b/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx
index e2ca0840402b..9ff77c33e48d 100644
--- a/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx
+++ b/app/components/UI/Perps/hooks/usePerpsToasts.test.tsx
@@ -10,6 +10,7 @@ import {
import { IconName } from '../../../../component-library/components/Icons/Icon';
import { ButtonVariants } from '../../../../component-library/components/Buttons/Button';
import Routes from '../../../../constants/navigation/Routes';
+import { mockTheme } from '../../../../util/theme';
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(),
@@ -22,9 +23,12 @@ jest.mock('@react-navigation/native', () => ({
jest.mock('../../../../util/haptics');
jest.mock('../../../../util/theme', () => {
- const { mockTheme } = jest.requireActual('../../../../util/theme');
+ const { mockTheme: actualMockTheme } = jest.requireActual(
+ '../../../../util/theme',
+ );
return {
- useAppThemeFromContext: jest.fn(() => mockTheme),
+ mockTheme: actualMockTheme,
+ useAppThemeFromContext: jest.fn(() => actualMockTheme),
};
});
@@ -111,6 +115,33 @@ describe('usePerpsToasts', () => {
});
expect(playNotification).toHaveBeenCalledWith(NotificationMoment.Success);
});
+
+ it('passes retryable withdrawal start failed toast options to toastRef', () => {
+ const onRetry = jest.fn();
+ const { result } = renderHook(() => usePerpsToasts());
+ const config =
+ result.current.PerpsToastOptions.accountManagement.withdrawal.withdrawalStartFailed(
+ onRetry,
+ );
+
+ act(() => {
+ result.current.showToast(config);
+ });
+
+ expect(mockShowToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variant: ToastVariants.Icon,
+ iconName: IconName.Error,
+ iconColor: mockTheme.colors.error.default,
+ backgroundColor: mockTheme.colors.accent04.normal,
+ linkButtonOptions: {
+ label: 'Try again',
+ onPress: onRetry,
+ },
+ }),
+ );
+ expect(playNotification).toHaveBeenCalledWith(NotificationMoment.Error);
+ });
});
describe('PerpsToastOptions configurations', () => {
@@ -242,6 +273,30 @@ describe('usePerpsToasts', () => {
isBold: false,
});
});
+
+ it('returns withdrawal start failed configuration with retry action', () => {
+ const onRetry = jest.fn();
+ const { result } = renderHook(() => usePerpsToasts());
+ const config =
+ result.current.PerpsToastOptions.accountManagement.withdrawal.withdrawalStartFailed(
+ onRetry,
+ );
+
+ expect(config.labelOptions).toEqual([
+ { label: 'Something went wrong', isBold: true },
+ { label: '\n', isBold: false },
+ { label: 'Your withdrawal wasn’t started.', isBold: false },
+ ]);
+ expect(config.linkButtonOptions).toMatchObject({
+ label: 'Try again',
+ onPress: onRetry,
+ });
+ expect(config).toMatchObject({
+ iconName: IconName.Error,
+ iconColor: mockTheme.colors.error.default,
+ backgroundColor: mockTheme.colors.accent04.normal,
+ });
+ });
});
describe('orderManagement.market', () => {
diff --git a/app/components/UI/Perps/hooks/usePerpsToasts.tsx b/app/components/UI/Perps/hooks/usePerpsToasts.tsx
index 733c1e7635e2..9f30df53e003 100644
--- a/app/components/UI/Perps/hooks/usePerpsToasts.tsx
+++ b/app/components/UI/Perps/hooks/usePerpsToasts.tsx
@@ -66,6 +66,7 @@ export interface PerpsToastOptionsConfig {
assetSymbol: string,
) => PerpsToastOptions;
withdrawalFailed: (error?: string) => PerpsToastOptions;
+ withdrawalStartFailed: (onRetry: () => void) => PerpsToastOptions;
};
};
orderManagement: {
@@ -495,6 +496,20 @@ const usePerpsToasts = (): {
}),
),
}),
+ withdrawalStartFailed: (onRetry: () => void) => ({
+ ...perpsBaseToastOptions.error,
+ iconName: IconName.Error,
+ iconColor: theme.colors.error.default,
+ backgroundColor: theme.colors.accent04.normal,
+ labelOptions: getPerpsToastLabels(
+ strings('perps.withdrawal.toast_error_title'),
+ strings('perps.withdrawal.toast_start_error_description'),
+ ),
+ linkButtonOptions: {
+ label: strings('perps.withdrawal.try_again'),
+ onPress: onRetry,
+ },
+ }),
},
},
// Intentional duplication of some options between market and limit to avoid coupling.
@@ -997,6 +1012,7 @@ const usePerpsToasts = (): {
perpsBaseToastOptions.success,
perpsBaseToastOptions.warning,
perpsToastButtonOptions,
+ theme.colors.accent04.normal,
theme.colors.background.muted,
theme.colors.error.default,
theme.colors.error.muted,
diff --git a/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.test.ts b/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.test.ts
index 7e2607ff20e4..d15f5762a5b1 100644
--- a/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.test.ts
+++ b/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.test.ts
@@ -1,7 +1,8 @@
import { ORIGIN_METAMASK } from '@metamask/controller-utils';
+import { useNavigation } from '@react-navigation/native';
import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller';
import { Hex } from '@metamask/utils';
-import { renderHook, act } from '@testing-library/react-native';
+import { renderHook, act, waitFor } from '@testing-library/react-native';
import { useSelector } from 'react-redux';
import { selectSelectedInternalAccountAddress } from '../../../../selectors/accountsController';
import { selectDefaultEndpointByChainId } from '../../../../selectors/networkController';
@@ -12,6 +13,11 @@ import { ConfirmationLoader } from '../../../Views/confirmations/components/conf
import { ARBITRUM_USDC } from '../../../Views/confirmations/constants/perps';
import Routes from '../../../../constants/navigation/Routes';
import { usePerpsWithdrawConfirmation } from './usePerpsWithdrawConfirmation';
+import usePerpsToasts from './usePerpsToasts';
+
+jest.mock('@react-navigation/native', () => ({
+ useNavigation: jest.fn(),
+}));
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
@@ -40,10 +46,20 @@ jest.mock('../../../Views/confirmations/hooks/useConfirmNavigation', () => ({
useConfirmNavigation: jest.fn(),
}));
+jest.mock('./usePerpsToasts', () => jest.fn());
+
const MOCK_ACCOUNT = '0x1234567890123456789012345678901234567890' as Hex;
const MOCK_TRANSFER_DATA = '0xabcdef' as Hex;
const MOCK_NETWORK_CLIENT_ID = 'arbitrum-mainnet';
const mockNavigateToConfirmation = jest.fn();
+const mockGoBack = jest.fn();
+const mockShowToast = jest.fn();
+const mockWithdrawalStartFailedToast = { labelOptions: [{ label: 'failed' }] };
+let retryWithdraw: (() => void) | undefined;
+const mockWithdrawalStartFailed = jest.fn((onRetry: () => void) => {
+ retryWithdraw = onRetry;
+ return mockWithdrawalStartFailedToast;
+});
describe('usePerpsWithdrawConfirmation', () => {
const mockAddTransactionBatch = jest.mocked(addTransactionBatch);
@@ -55,9 +71,12 @@ describe('usePerpsWithdrawConfirmation', () => {
selectDefaultEndpointByChainId,
);
const mockUseConfirmNavigation = jest.mocked(useConfirmNavigation);
+ const mockUseNavigation = jest.mocked(useNavigation);
+ const mockUsePerpsToasts = jest.mocked(usePerpsToasts);
beforeEach(() => {
jest.clearAllMocks();
+ retryWithdraw = undefined;
mockSelectSelectedInternalAccountAddress.mockReturnValue(MOCK_ACCOUNT);
mockSelectDefaultEndpointByChainId.mockReturnValue({
@@ -68,6 +87,19 @@ describe('usePerpsWithdrawConfirmation', () => {
mockUseConfirmNavigation.mockReturnValue({
navigateToConfirmation: mockNavigateToConfirmation,
} as never);
+ mockUseNavigation.mockReturnValue({
+ goBack: mockGoBack,
+ } as never);
+ mockUsePerpsToasts.mockReturnValue({
+ showToast: mockShowToast,
+ PerpsToastOptions: {
+ accountManagement: {
+ withdrawal: {
+ withdrawalStartFailed: mockWithdrawalStartFailed,
+ },
+ },
+ },
+ } as never);
(useSelector as jest.Mock).mockImplementation(((
selector: (state: object) => unknown,
@@ -120,7 +152,7 @@ describe('usePerpsWithdrawConfirmation', () => {
});
});
- it('propagates error when addTransactionBatch fails', async () => {
+ it('navigates back and shows a retryable error toast when addTransactionBatch fails', async () => {
const error = new Error('batch failed');
mockAddTransactionBatch.mockRejectedValueOnce(error);
@@ -131,5 +163,59 @@ describe('usePerpsWithdrawConfirmation', () => {
await result.current.withdrawWithConfirmation();
}),
).rejects.toThrow('batch failed');
+
+ expect(mockGoBack).toHaveBeenCalledTimes(1);
+ expect(mockWithdrawalStartFailed).toHaveBeenCalledWith(
+ expect.any(Function),
+ );
+ expect(mockShowToast).toHaveBeenCalledWith(mockWithdrawalStartFailedToast);
+
+ act(() => {
+ retryWithdraw?.();
+ });
+
+ expect(mockNavigateToConfirmation).toHaveBeenCalledTimes(2);
+ expect(mockAddTransactionBatch).toHaveBeenCalledTimes(2);
+ });
+
+ it('normalizes non-Error addTransactionBatch failures before rethrowing', async () => {
+ mockAddTransactionBatch.mockRejectedValueOnce('batch failed');
+
+ const { result } = renderHook(() => usePerpsWithdrawConfirmation());
+
+ await expect(
+ act(async () => {
+ await result.current.withdrawWithConfirmation();
+ }),
+ ).rejects.toThrow('batch failed');
+
+ expect(mockGoBack).toHaveBeenCalledTimes(1);
+ expect(mockShowToast).toHaveBeenCalledWith(mockWithdrawalStartFailedToast);
+ });
+
+ it('swallows retry failures after showing another retryable error toast', async () => {
+ mockAddTransactionBatch
+ .mockRejectedValueOnce(new Error('batch failed'))
+ .mockRejectedValueOnce(new Error('retry failed'));
+
+ const { result } = renderHook(() => usePerpsWithdrawConfirmation());
+
+ await expect(
+ act(async () => {
+ await result.current.withdrawWithConfirmation();
+ }),
+ ).rejects.toThrow('batch failed');
+
+ act(() => {
+ retryWithdraw?.();
+ });
+
+ await waitFor(() => {
+ expect(mockGoBack).toHaveBeenCalledTimes(2);
+ });
+
+ expect(mockNavigateToConfirmation).toHaveBeenCalledTimes(2);
+ expect(mockShowToast).toHaveBeenCalledTimes(2);
+ expect(mockWithdrawalStartFailed).toHaveBeenCalledTimes(2);
});
});
diff --git a/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.ts b/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.ts
index 0da7893c1269..4f746df4beb9 100644
--- a/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.ts
+++ b/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.ts
@@ -1,4 +1,5 @@
import { useCallback } from 'react';
+import { useNavigation } from '@react-navigation/native';
import { Hex } from '@metamask/utils';
import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller';
import { ORIGIN_METAMASK } from '@metamask/controller-utils';
@@ -12,6 +13,8 @@ import { ConfirmationLoader } from '../../../Views/confirmations/components/conf
import { ARBITRUM_USDC } from '../../../Views/confirmations/constants/perps';
import { RootState } from '../../../../reducers';
import Routes from '../../../../constants/navigation/Routes';
+import { ensureError } from '../../../../util/errorUtils';
+import usePerpsToasts from './usePerpsToasts';
/**
* Hook that triggers the Perps "withdraw to any token" confirmation flow.
@@ -23,6 +26,8 @@ import Routes from '../../../../constants/navigation/Routes';
export function usePerpsWithdrawConfirmation() {
const selectedAccount = useSelector(selectSelectedInternalAccountAddress);
const { navigateToConfirmation } = useConfirmNavigation();
+ const navigation = useNavigation();
+ const { showToast, PerpsToastOptions } = usePerpsToasts();
const { networkClientId } =
useSelector((state: RootState) =>
@@ -34,29 +39,57 @@ export function usePerpsWithdrawConfirmation() {
amount: '0x0',
}) as Hex;
- const withdrawWithConfirmation = useCallback(async () => {
- navigateToConfirmation({
- loader: ConfirmationLoader.CustomAmount,
- stack: Routes.PERPS.ROOT,
- });
+ const withdrawWithConfirmation = useCallback(
+ async function runWithdrawWithConfirmation() {
+ navigateToConfirmation({
+ loader: ConfirmationLoader.CustomAmount,
+ stack: Routes.PERPS.ROOT,
+ });
- await addTransactionBatch({
- from: selectedAccount as Hex,
- origin: ORIGIN_METAMASK,
+ try {
+ await addTransactionBatch({
+ from: selectedAccount as Hex,
+ origin: ORIGIN_METAMASK,
+ networkClientId,
+ disableHook: true,
+ disableSequential: true,
+ transactions: [
+ {
+ params: {
+ to: ARBITRUM_USDC.address,
+ data: transferData,
+ },
+ type: TransactionType.perpsWithdraw,
+ },
+ ],
+ });
+ } catch (error) {
+ const errorObj = ensureError(
+ error,
+ 'usePerpsWithdrawConfirmation.withdrawWithConfirmation',
+ );
+
+ navigation.goBack();
+ showToast(
+ PerpsToastOptions.accountManagement.withdrawal.withdrawalStartFailed(
+ () => {
+ runWithdrawWithConfirmation().catch(() => undefined);
+ },
+ ),
+ );
+ throw errorObj;
+ }
+ },
+ [
+ navigateToConfirmation,
+ navigation,
networkClientId,
- disableHook: true,
- disableSequential: true,
- transactions: [
- {
- params: {
- to: ARBITRUM_USDC.address,
- data: transferData,
- },
- type: TransactionType.perpsWithdraw,
- },
- ],
- });
- }, [navigateToConfirmation, networkClientId, selectedAccount, transferData]);
+ PerpsToastOptions.accountManagement.withdrawal,
+ selectedAccount,
+ showToast,
+ transferData,
+ ],
+ );
return { withdrawWithConfirmation };
}
diff --git a/app/components/UI/Perps/hooks/usePerpsWithdrawStatus.test.ts b/app/components/UI/Perps/hooks/usePerpsWithdrawStatus.test.ts
index 8040f4d970ce..f3669b6b5106 100644
--- a/app/components/UI/Perps/hooks/usePerpsWithdrawStatus.test.ts
+++ b/app/components/UI/Perps/hooks/usePerpsWithdrawStatus.test.ts
@@ -83,6 +83,16 @@ describe('usePerpsWithdrawStatus', () => {
labelOptions: [{ label: 'Withdrawal failed', isBold: true }],
hapticsType: NotificationMoment.Error,
})),
+ withdrawalStartFailed: jest.fn(() => ({
+ variant: ToastVariants.Icon,
+ iconName: IconName.Warning,
+ hasNoTimeout: false,
+ labelOptions: [
+ { label: 'Something went wrong', isBold: true },
+ { label: 'Your withdrawal was not started' },
+ ],
+ hapticsType: NotificationMoment.Error,
+ })),
},
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.test.tsx b/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.test.tsx
index 234c79ce4a15..61d5aa95398f 100644
--- a/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.test.tsx
+++ b/app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.test.tsx
@@ -3,7 +3,7 @@ import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller';
import { Hex } from '@metamask/utils';
import { fireEvent, act, render } from '@testing-library/react-native';
import React from 'react';
-import { useTheme } from '@react-navigation/native';
+import { useNavigation, useTheme } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { selectSelectedInternalAccountAddress } from '../../../../../../selectors/accountsController';
import { selectDefaultEndpointByChainId } from '../../../../../../selectors/networkController';
@@ -27,6 +27,7 @@ jest.mock('react-redux', () => ({
jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
+ useNavigation: jest.fn(),
useTheme: jest.fn(),
}));
@@ -71,6 +72,7 @@ const MOCK_POLYGON_USDCE = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' as Hex;
const MOCK_PROXY_ADDRESS = '0x13032833b30f3388208cda38971fdc839936b042' as Hex;
const MOCK_NETWORK_CLIENT_ID = 'arbitrum-mainnet';
const mockNavigateToConfirmation = jest.fn();
+const mockGoBack = jest.fn();
const mockSelectMoneyAccountDepositEnabledFlag = jest.mocked(
selectMoneyAccountDepositEnabledFlag,
);
@@ -80,6 +82,7 @@ const mockSelectMoneyAccountWithdrawEnabledFlag = jest.mocked(
describe('ConfirmationsDeveloperOptions', () => {
const mockUseSelector = jest.mocked(useSelector);
+ const mockUseNavigation = jest.mocked(useNavigation);
const mockUseTheme = jest.mocked(useTheme);
const mockUseStyles = jest.mocked(useStyles);
const mockSelectSelectedInternalAccountAddress = jest.mocked(
@@ -98,6 +101,9 @@ describe('ConfirmationsDeveloperOptions', () => {
mockUseTheme.mockReturnValue({
colors: {},
} as never);
+ mockUseNavigation.mockReturnValue({
+ goBack: mockGoBack,
+ } as never);
mockUseStyles.mockReturnValue({
styles: {
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 99a4494c2bae..7dcb4bb2f6ad 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -1288,7 +1288,9 @@
"toast_completed_subtitle": "{{amount}} USDC moved to your wallet",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} moved to your wallet",
"toast_error_title": "Something went wrong",
- "toast_error_description": "Failed to proceed with withdrawal"
+ "toast_error_description": "Failed to proceed with withdrawal",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Try again"
},
"quote": {
"network_fee": "Network fee",
From 4b2f4512dfea5a45c3860748b41b4a8bff2613d5 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Wed, 20 May 2026 19:57:23 +0000
Subject: [PATCH 38/66] [skip ci] Bump version number to 5111
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index b9fe3bfb40bd..544d09041009 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5110
+ versionCode 5111
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 963fea5fee7e..0b4f73179d8f 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5110
+ VERSION_NUMBER: 5111
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5110
+ FLASK_VERSION_NUMBER: 5111
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index e3578a649f3e..9535c4c67829 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5110;
+ CURRENT_PROJECT_VERSION = 5111;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5110;
+ CURRENT_PROJECT_VERSION = 5111;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5110;
+ CURRENT_PROJECT_VERSION = 5111;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5110;
+ CURRENT_PROJECT_VERSION = 5111;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 7e00e329412e5ff0e044532edfa2fbe902e1779e Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Wed, 20 May 2026 22:08:09 -0400
Subject: [PATCH 39/66] chore(runway): cherry-pick feat(predict): Bottom Sheet
Keyboard Fix cp-7.78.0 (#30488)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- feat(predict): Bottom Sheet Keyboard Fix cp-7.78.0 (#30483)
## **Description**
The Predict Buy bottom-sheet introduced in
[#28779](https://github.com/MetaMask/metamask-mobile/pull/28779) opened
with the custom keypad already rendered behind the rest of the sheet
content. The root cause was a single `isInputFocused` boolean inside
`usePredictBuyInputState` that was hard-coded to `true` on first render
and overloaded across **five** unrelated behaviours:
1. Mounting `PredictKeypad`.
2. Highlighting the amount-display "active" state.
3. Hiding `PredictBuyBottomContent` while the keypad is up.
4. Disabling `PredictFeeSummary`.
5. Deferring the mm_pay relay-config side effects (`updatePendingAmount`
/ `setPayToken`) inside `PredictPayWithAnyTokenInfo`.
Because the only "off switch" was `setIsInputFocused(false)` (driven
from a `Done` button that doesn't exist in sheet mode) the previous PR
worked around the issue with three `isSheetMode ? false :
isInputFocused` ternaries — Bugbot flagged this on #28779. The
workarounds also produced a confusing situation where bottom content and
keypad rendered simultaneously on first sheet open.
This PR separates the conflated meanings, parameterises the initial
state, and removes the workarounds. No behaviour change for the legacy
full-screen flow.
### What changed
**`usePredictBuyInputState`
([hooks/usePredictBuyInputState.ts](app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyInputState.ts))**
- Accepts an options object: `{ initialKeypadOpen = true }`.
- Renamed state + setter: `isInputFocused` → `isKeypadOpen`,
`setIsInputFocused` → `setIsKeypadOpen`.
- Default value preserves legacy behaviour; sheet mode opts out.
**`PredictBuyWithAnyToken`
([PredictBuyWithAnyToken.tsx](app/components/UI/Predict/views/PredictBuyWithAnyToken/PredictBuyWithAnyToken.tsx))**
- Calls the hook as `usePredictBuyInputState({ initialKeypadOpen:
!isSheetMode })` so the sheet opens with the keypad collapsed.
- Removed the three `isSheetMode ? false : isInputFocused` patches.
- Renders `PredictBuyBottomContent` at the call site via `{(isSheetMode
|| !isKeypadOpen) && }` instead of
relying on the component to bail internally. Keeps the legacy "hide
while typing" behaviour while letting the sheet show the bottom content
(and Confirm) at all times.
- Passes the new self-documenting `shouldDeferRelaySetup={!isSheetMode
&& isKeypadOpen}` prop to `PredictPayWithAnyTokenInfo`. Same effective
value as before, but the prop name now describes its actual purpose.
**`PredictPayWithAnyTokenInfo`
([components/PredictPayWithAnyTokenInfo](app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictPayWithAnyTokenInfo/PredictPayWithAnyTokenInfo.tsx))**
- Renamed prop: `isInputFocused` → `shouldDeferRelaySetup`. This is the
genuine separation: the prop is **not** about UI keypad state, it's
about pausing `updatePendingAmount` / `setPayToken` calls. Legacy mode
still defers until the user taps Done; sheet mode never defers (relay
must update on every keystroke since there's no Done and the user can
tap Confirm with the keypad still open — preventing underfunded
deposits).
- Added a JSDoc comment explaining the contract.
**`PredictBuyBottomContent`
([components/PredictBuyBottomContent](app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyBottomContent/PredictBuyBottomContent.tsx))**
- Removed the `isInputFocused` prop and the `if (isInputFocused) return
null` early return. Component is now purely structural; visibility is
the parent's responsibility.
**`PredictKeypad` and `PredictBuyAmountSection`**
- Mechanical prop renames: `isInputFocused` → `isKeypadOpen`,
`setIsInputFocused` → `setIsKeypadOpen`.
**Legacy `PredictBuyPreview`
([views/PredictBuyPreview](app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.tsx))**
- Local `useState(true)` renamed to `isKeypadOpen` for consistency with
the rest of the tree. No behaviour change (still initialises `true`,
still gates `renderBottomContent`).
**Tests**
- All prop/state references renamed.
- New coverage on `usePredictBuyInputState` for `initialKeypadOpen:
false` and `initialKeypadOpen: true`.
- New assertions in `PredictBuyWithAnyToken.test.tsx` that the hook is
called with `initialKeypadOpen: false` in sheet mode and
`initialKeypadOpen: true` in non-sheet mode.
- `PredictBuyBottomContent.test.tsx`: removed the now-irrelevant
`isInputFocused is true / false` describe blocks since visibility is
caller-controlled.
- `PredictPayWithAnyTokenInfo.test.tsx`: updated 43 prop references and
renamed two test descriptions to reflect the new "relay deferral"
intent.
- `PredictBuyPreview.test.tsx`: updated `renderBottomContent` describe
block descriptions.
### Net behavioural effect (sheet mode)
- Sheet opens → keypad hidden, amount display shows `$0` (inactive),
quick amounts + pay-with row + fee summary + Confirm button visible
(Confirm disabled until amount > 0).
- Tap amount display → keypad opens at the bottom of the sheet, stacked
**below** the Confirm button (does not overlap).
- Tap a quick amount → value set, keypad closes.
- Tap Confirm with keypad still open → places order (relay setup is
up-to-date because `shouldDeferRelaySetup` is `false` in sheet mode).
- Auto-blur on banner display still works.
Legacy full-screen flow: unchanged.
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Refs: PRED-707 (follow-up to
[#28779](https://github.com/MetaMask/metamask-mobile/pull/28779))
## **Manual testing steps**
```gherkin
Feature: Predict buy bottom-sheet keypad initial state
Scenario: Sheet opens with keypad collapsed (flag ON)
Given the predictBottomSheet feature flag is enabled
And the user is on a prediction market details page
When user taps a "Yes" or "No" outcome button
Then a bottom sheet opens with the buy preview content
And the custom numeric keypad is NOT visible
And the amount display shows "$0"
And the quick amount buttons ($20 / $50 / $100 / $250) are visible
And the Pay with row, fee summary and Confirm button are visible
And the Confirm button is disabled
Scenario: Tapping the amount opens the keypad
Given the buy bottom sheet is open with no amount entered
And the keypad is hidden
When user taps the amount display
Then the custom keypad becomes visible at the bottom of the sheet
And the keypad does NOT cover the Confirm button
And the amount display shows the active highlight
Scenario: Tapping a quick amount sets value and closes the keypad
Given the keypad is open
And the user has typed "$73"
When user taps the "$50" quick amount button
Then the amount becomes "$50"
And the keypad closes
And haptic feedback fires (Light impact)
And the Confirm button is enabled
Scenario: Confirming with keypad still open
Given the keypad is open
And the user has typed "$25"
When user taps the Confirm button
Then the order is placed successfully
And the relay was configured for $25 (no underfunded deposit)
And the sheet closes
Scenario: Banner display auto-blurs the keypad in sheet mode
Given the buy bottom sheet is open
And the keypad is open
When an order_failed or price_changed banner appears
Then the keypad auto-closes
And the banner + Retry CTA are visible without needing to tap Done
Scenario: Legacy full-screen flow is unchanged (flag OFF)
Given the predictBottomSheet feature flag is disabled
And the user navigates to the BuyPreview screen
When the screen mounts
Then the keypad is open by default (matching previous behaviour)
And tapping Done closes the keypad and reveals the bottom content
And the relay is configured once on Done (deferred during typing)
```
## **Screenshots/Recordings**
### **Before**
Sheet opens with the keypad rendered behind the bottom content; bottom
content and keypad are both visible simultaneously on first paint. (See
screenshot in PR comments — keypad sits below `Confirm` even though the
user has not yet tapped the amount display.)
### **After**
Sheet opens with the keypad hidden; bottom content (quick amounts, pay
with row, fee summary, Confirm) is the only thing visible. Tapping the
amount display reveals the keypad; tapping a quick amount or Confirm
collapses it.
https://github.com/user-attachments/assets/af89b78a-5103-48eb-b2c9-346ea64cd463
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
> N/A — pure refactor of UI state semantics, no new code paths.
## **Pre-merge reviewer checklist**
- [x] 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]
> **Medium Risk**
> Changes how the Predict buy keypad state is tracked and used to gate
bottom content and mm_pay relay setup; mistakes could surface as
incorrect UI visibility or misconfigured deposit/payment amounts during
checkout. Scope is mostly contained to Predict buy views and covered by
updated tests.
>
> **Overview**
> Fixes the Predict buy bottom-sheet keypad initial state by splitting
the previously overloaded `isInputFocused` flag into a dedicated
`isKeypadOpen` state, including a new `initialKeypadOpen` option in
`usePredictBuyInputState` so sheet mode can start closed.
>
> Updates
`PredictBuyWithAnyToken`/`PredictKeypad`/`PredictBuyAmountSection` to
use `isKeypadOpen` for keypad mounting and active styling, moves
bottom-content visibility control to the parent (removing
`PredictBuyBottomContent`’s internal early-return), and introduces
`shouldDeferRelaySetup` (replacing `isInputFocused`) to explicitly gate
`PredictPayWithAnyTokenInfo`’s relay-configuration side effects.
>
> Refactors legacy `PredictBuyPreview` to the same `isKeypadOpen` naming
and updates/adds tests to assert the new initialization, visibility
behavior, and relay deferral propagation.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
98d3d4dd1fb15c581bbb92472bf951513f646b60. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[f2b07b6](https://github.com/MetaMask/metamask-mobile/commit/f2b07b68c53d132a1cd38c1a32d57813e82f0fc4)
Co-authored-by: Aslau Mario-Daniel
---
.../PredictKeypad/PredictKeypad.test.tsx | 44 +++---
.../PredictKeypad/PredictKeypad.tsx | 28 ++--
.../PredictBuyPreview.test.tsx | 4 +-
.../PredictBuyPreview/PredictBuyPreview.tsx | 10 +-
.../PredictBuyWithAnyToken.test.tsx | 134 ++++++++++++++---
.../PredictBuyWithAnyToken.tsx | 138 +++++++++---------
.../PredictBuyAmountSection.test.tsx | 38 ++---
.../PredictBuyAmountSection.tsx | 6 +-
.../PredictBuyBottomContent.test.tsx | 43 +-----
.../PredictBuyBottomContent.tsx | 7 -
.../PredictPayWithAnyTokenInfo.test.tsx | 90 ++++++------
.../PredictPayWithAnyTokenInfo.tsx | 20 ++-
.../hooks/usePredictBuyInputState.test.ts | 28 +++-
.../hooks/usePredictBuyInputState.ts | 22 ++-
14 files changed, 349 insertions(+), 263 deletions(-)
diff --git a/app/components/UI/Predict/components/PredictKeypad/PredictKeypad.test.tsx b/app/components/UI/Predict/components/PredictKeypad/PredictKeypad.test.tsx
index e16c12dff40b..8ee4c9b1af02 100644
--- a/app/components/UI/Predict/components/PredictKeypad/PredictKeypad.test.tsx
+++ b/app/components/UI/Predict/components/PredictKeypad/PredictKeypad.test.tsx
@@ -18,12 +18,12 @@ describe('PredictKeypad', () => {
mockOnChange = null;
});
const defaultProps = {
- isInputFocused: true,
+ isKeypadOpen: true,
currentValue: 1,
currentValueUSDString: '1.00',
setCurrentValue: jest.fn(),
setCurrentValueUSDString: jest.fn(),
- setIsInputFocused: jest.fn(),
+ setIsKeypadOpen: jest.fn(),
};
beforeEach(() => {
@@ -35,32 +35,26 @@ describe('PredictKeypad', () => {
});
describe('Rendering', () => {
- it('renders keypad when input is focused', () => {
- // Arrange
- const props = { ...defaultProps, isInputFocused: true };
+ it('renders keypad when keypad is open', () => {
+ const props = { ...defaultProps, isKeypadOpen: true };
- // Act
const { getByText } = render();
- // Assert
- expect(getByText('$20')).toBeTruthy();
- expect(getByText('$50')).toBeTruthy();
- expect(getByText('$100')).toBeTruthy();
- expect(getByText('Done')).toBeTruthy();
+ expect(getByText('$20')).toBeOnTheScreen();
+ expect(getByText('$50')).toBeOnTheScreen();
+ expect(getByText('$100')).toBeOnTheScreen();
+ expect(getByText('Done')).toBeOnTheScreen();
});
- it('does not render keypad when input is not focused', () => {
- // Arrange
- const props = { ...defaultProps, isInputFocused: false };
+ it('does not render keypad when keypad is closed', () => {
+ const props = { ...defaultProps, isKeypadOpen: false };
- // Act
const { queryByText } = render();
- // Assert
- expect(queryByText('$20')).toBeNull();
- expect(queryByText('$50')).toBeNull();
- expect(queryByText('$100')).toBeNull();
- expect(queryByText('Done')).toBeNull();
+ expect(queryByText('$20')).not.toBeOnTheScreen();
+ expect(queryByText('$50')).not.toBeOnTheScreen();
+ expect(queryByText('$100')).not.toBeOnTheScreen();
+ expect(queryByText('Done')).not.toBeOnTheScreen();
});
});
@@ -117,7 +111,7 @@ describe('PredictKeypad', () => {
fireEvent.press(getByText('Done'));
// Assert
- expect(props.setIsInputFocused).toHaveBeenCalledWith(false);
+ expect(props.setIsKeypadOpen).toHaveBeenCalledWith(false);
});
it('exposes handleAmountPress handler through ref', () => {
@@ -130,7 +124,7 @@ describe('PredictKeypad', () => {
ref.current?.handleAmountPress();
// Assert
- expect(props.setIsInputFocused).toHaveBeenCalledWith(true);
+ expect(props.setIsKeypadOpen).toHaveBeenCalledWith(true);
});
it('exposes handleKeypadAmountPress handler through ref', () => {
@@ -157,7 +151,7 @@ describe('PredictKeypad', () => {
ref.current?.handleDonePress();
// Assert
- expect(props.setIsInputFocused).toHaveBeenCalledWith(false);
+ expect(props.setIsKeypadOpen).toHaveBeenCalledWith(false);
});
});
@@ -184,7 +178,7 @@ describe('PredictKeypad', () => {
expect(props.setCurrentValueUSDString).toHaveBeenCalledWith('25');
expect(props.setCurrentValue).toHaveBeenCalledWith(25);
- expect(props.setIsInputFocused).toHaveBeenCalledWith(false);
+ expect(props.setIsKeypadOpen).toHaveBeenCalledWith(false);
});
it('handles empty string after removing decimal point', () => {
@@ -212,7 +206,7 @@ describe('PredictKeypad', () => {
ref.current?.handleDonePress();
expect(props.setCurrentValueUSDString).not.toHaveBeenCalled();
- expect(props.setIsInputFocused).toHaveBeenCalledWith(false);
+ expect(props.setIsKeypadOpen).toHaveBeenCalledWith(false);
});
});
diff --git a/app/components/UI/Predict/components/PredictKeypad/PredictKeypad.tsx b/app/components/UI/Predict/components/PredictKeypad/PredictKeypad.tsx
index f86e9b450e71..be44445c5e6b 100644
--- a/app/components/UI/Predict/components/PredictKeypad/PredictKeypad.tsx
+++ b/app/components/UI/Predict/components/PredictKeypad/PredictKeypad.tsx
@@ -8,12 +8,12 @@ import Button, {
import Keypad from '../../../../Base/Keypad';
interface PredictKeypadProps {
- isInputFocused: boolean;
+ isKeypadOpen: boolean;
currentValue: number;
currentValueUSDString: string;
setCurrentValue: (value: number) => void;
setCurrentValueUSDString: (value: string) => void;
- setIsInputFocused: (focused: boolean) => void;
+ setIsKeypadOpen: (open: boolean) => void;
hideHeader?: boolean;
}
@@ -26,12 +26,12 @@ export interface PredictKeypadHandles {
const PredictKeypad = forwardRef(
(
{
- isInputFocused,
+ isKeypadOpen,
currentValue,
currentValueUSDString,
setCurrentValue,
setCurrentValueUSDString,
- setIsInputFocused,
+ setIsKeypadOpen,
hideHeader = false,
},
ref,
@@ -39,8 +39,8 @@ const PredictKeypad = forwardRef(
const tw = useTailwind();
const handleAmountPress = useCallback(() => {
- setIsInputFocused(true);
- }, [setIsInputFocused]);
+ setIsKeypadOpen(true);
+ }, [setIsKeypadOpen]);
const handleKeypadAmountPress = useCallback(
(amount: number) => {
@@ -58,9 +58,9 @@ const PredictKeypad = forwardRef(
setCurrentValueUSDString(cleanedValue);
setCurrentValue(parseFloat(cleanedValue) || 0);
}
- setIsInputFocused(false);
+ setIsKeypadOpen(false);
}, [
- setIsInputFocused,
+ setIsKeypadOpen,
currentValueUSDString,
setCurrentValueUSDString,
setCurrentValue,
@@ -93,9 +93,9 @@ const PredictKeypad = forwardRef(
adjustedValue = value.replace('.', '');
}
- // Set focus flag immediately
- if (!isInputFocused) {
- setIsInputFocused(true);
+ // Open the keypad immediately on any keystroke
+ if (!isKeypadOpen) {
+ setIsKeypadOpen(true);
}
// Enforce 9-digit limit (ignoring non-digits). Block the change if exceeded.
@@ -129,14 +129,14 @@ const PredictKeypad = forwardRef(
},
[
currentValue,
- isInputFocused,
+ isKeypadOpen,
setCurrentValue,
setCurrentValueUSDString,
- setIsInputFocused,
+ setIsKeypadOpen,
],
);
- if (!isInputFocused) return null;
+ if (!isKeypadOpen) return null;
return (
diff --git a/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.test.tsx b/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.test.tsx
index 5e752b137150..16e473b99f91 100644
--- a/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.test.tsx
+++ b/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.test.tsx
@@ -2191,7 +2191,7 @@ describe('PredictBuyPreview', () => {
});
describe('renderBottomContent visibility', () => {
- it('returns null when isInputFocused is true', () => {
+ it('returns null when keypad is open', () => {
mockBalance = 1000;
mockBalanceLoading = false;
@@ -2203,7 +2203,7 @@ describe('PredictBuyPreview', () => {
).not.toBeOnTheScreen();
});
- it('renders bottom content when isInputFocused is false', () => {
+ it('renders bottom content when keypad is closed', () => {
mockBalance = 1000;
mockBalanceLoading = false;
diff --git a/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.tsx b/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.tsx
index 19434cf238ed..864ebd5705ac 100644
--- a/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.tsx
+++ b/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.tsx
@@ -158,7 +158,7 @@ const PredictBuyPreview = (props: PredictBuyPreviewProps) => {
const [currentValue, setCurrentValue] = useState(0);
const [currentValueUSDString, setCurrentValueUSDString] = useState('');
- const [isInputFocused, setIsInputFocused] = useState(true);
+ const [isKeypadOpen, setIsKeypadOpen] = useState(true);
const [isUserInputChange, setIsUserInputChange] = useState(false);
const [isFeeBreakdownVisible, setIsFeeBreakdownVisible] = useState(false);
const previousValueRef = useRef(0);
@@ -427,7 +427,7 @@ const PredictBuyPreview = (props: PredictBuyPreviewProps) => {
keypadRef.current?.handleAmountPress()}
- isActive={isInputFocused}
+ isActive={isKeypadOpen}
hasError={isInsufficientBalance}
/>
@@ -545,7 +545,7 @@ const PredictBuyPreview = (props: PredictBuyPreviewProps) => {
};
const renderBottomContent = () => {
- if (isInputFocused) {
+ if (isKeypadOpen) {
return null;
}
@@ -609,12 +609,12 @@ const PredictBuyPreview = (props: PredictBuyPreviewProps) => {
{renderMinimumBetWarning()}
{renderBottomContent()}
{isFeeBreakdownVisible && (
diff --git a/app/components/UI/Predict/views/PredictBuyWithAnyToken/PredictBuyWithAnyToken.test.tsx b/app/components/UI/Predict/views/PredictBuyWithAnyToken/PredictBuyWithAnyToken.test.tsx
index 77fc37099255..bfa38645c8ff 100644
--- a/app/components/UI/Predict/views/PredictBuyWithAnyToken/PredictBuyWithAnyToken.test.tsx
+++ b/app/components/UI/Predict/views/PredictBuyWithAnyToken/PredictBuyWithAnyToken.test.tsx
@@ -14,7 +14,7 @@ const mockResetOrderNotFilled = jest.fn();
const mockClearBuyErrorBanner = jest.fn();
const mockSetCurrentValue = jest.fn();
const mockSetCurrentValueUSDString = jest.fn();
-const mockSetIsInputFocused = jest.fn();
+const mockSetIsKeypadOpen = jest.fn();
const mockSetIsUserInputChange = jest.fn();
const mockSetIsConfirming = jest.fn();
const mockHandleRetryWithBestPrice = jest.fn();
@@ -133,19 +133,22 @@ jest.mock('./hooks/usePredictBuyAvailableBalance', () => ({
}),
}));
+const mockUsePredictBuyInputState = jest.fn((..._args: unknown[]) => ({
+ currentValue: 20,
+ setCurrentValue: mockSetCurrentValue,
+ currentValueUSDString: '$20.00',
+ setCurrentValueUSDString: mockSetCurrentValueUSDString,
+ isKeypadOpen: false,
+ setIsKeypadOpen: mockSetIsKeypadOpen,
+ isUserInputChange: true,
+ setIsUserInputChange: mockSetIsUserInputChange,
+ isConfirming: false,
+ setIsConfirming: mockSetIsConfirming,
+}));
+
jest.mock('./hooks/usePredictBuyInputState', () => ({
- usePredictBuyInputState: () => ({
- currentValue: 20,
- setCurrentValue: mockSetCurrentValue,
- currentValueUSDString: '$20.00',
- setCurrentValueUSDString: mockSetCurrentValueUSDString,
- isInputFocused: false,
- setIsInputFocused: mockSetIsInputFocused,
- isUserInputChange: true,
- setIsUserInputChange: mockSetIsUserInputChange,
- isConfirming: false,
- setIsConfirming: mockSetIsConfirming,
- }),
+ usePredictBuyInputState: (...args: unknown[]) =>
+ mockUsePredictBuyInputState(...args),
}));
jest.mock('./hooks/usePredictBuyInfo', () => ({
@@ -317,15 +320,18 @@ jest.mock('../../components/PredictOrderRetrySheet', () => {
));
});
+const mockPredictPayWithAnyTokenInfo = jest.fn();
+
jest.mock('./components/PredictPayWithAnyTokenInfo', () => {
const { Text } = jest.requireActual('react-native');
- return function MockPredictPayWithAnyTokenInfo({
- currentValue,
- }: {
+ return function MockPredictPayWithAnyTokenInfo(props: {
currentValue: number;
- isInputFocused: boolean;
+ shouldDeferRelaySetup: boolean;
}) {
- return {currentValue};
+ mockPredictPayWithAnyTokenInfo(props);
+ return (
+ {props.currentValue}
+ );
};
});
@@ -544,6 +550,14 @@ describe('PredictBuyWithAnyToken', () => {
).not.toBeOnTheScreen();
});
+ it('initialises usePredictBuyInputState with initialKeypadOpen=false', () => {
+ renderWithProvider();
+
+ expect(mockUsePredictBuyInputState).toHaveBeenCalledWith({
+ initialKeypadOpen: false,
+ });
+ });
+
it('renders PredictQuickAmounts inside bottom content', () => {
renderWithProvider();
@@ -564,12 +578,12 @@ describe('PredictBuyWithAnyToken', () => {
expect(screen.getByTestId('predict-keypad')).toBeOnTheScreen();
});
- it('sets isInputFocused to false when quick amount is tapped', () => {
+ it('closes the keypad when a quick amount is tapped', () => {
renderWithProvider();
fireEvent.press(screen.getByTestId('quick-amount-20'));
- expect(mockSetIsInputFocused).toHaveBeenCalledWith(false);
+ expect(mockSetIsKeypadOpen).toHaveBeenCalledWith(false);
expect(mockSetCurrentValue).toHaveBeenCalledWith(20);
expect(mockSetCurrentValueUSDString).toHaveBeenCalledWith('20');
});
@@ -640,6 +654,14 @@ describe('PredictBuyWithAnyToken', () => {
});
describe('non-sheet mode', () => {
+ it('initialises usePredictBuyInputState with initialKeypadOpen=true', () => {
+ renderWithProvider();
+
+ expect(mockUsePredictBuyInputState).toHaveBeenCalledWith({
+ initialKeypadOpen: true,
+ });
+ });
+
it('does NOT render the banner even if buyErrorBanner is set', () => {
mockBuyErrorBanner = {
variant: 'order_failed',
@@ -670,6 +692,78 @@ describe('PredictBuyWithAnyToken', () => {
});
});
+ describe('shouldDeferRelaySetup propagation', () => {
+ const sheetProps = {
+ mode: 'sheet' as const,
+ market: { id: 'market-1' },
+ outcome: { id: 'outcome-1' },
+ outcomeToken: { id: 'token-1', title: 'Yes', price: 0.62 },
+ entryPoint: 'market_details',
+ onClose: jest.fn(),
+ } as unknown as PredictBuyPreviewProps;
+
+ const mockHookReturnWithKeypadOpen = (isKeypadOpen: boolean) => ({
+ currentValue: 20,
+ setCurrentValue: mockSetCurrentValue,
+ currentValueUSDString: '$20.00',
+ setCurrentValueUSDString: mockSetCurrentValueUSDString,
+ isKeypadOpen,
+ setIsKeypadOpen: mockSetIsKeypadOpen,
+ isUserInputChange: true,
+ setIsUserInputChange: mockSetIsUserInputChange,
+ isConfirming: false,
+ setIsConfirming: mockSetIsConfirming,
+ });
+
+ it('passes shouldDeferRelaySetup=false in sheet mode when keypad is closed', () => {
+ mockUsePredictBuyInputState.mockReturnValueOnce(
+ mockHookReturnWithKeypadOpen(false),
+ );
+
+ renderWithProvider();
+
+ expect(mockPredictPayWithAnyTokenInfo).toHaveBeenLastCalledWith(
+ expect.objectContaining({ shouldDeferRelaySetup: false }),
+ );
+ });
+
+ it('passes shouldDeferRelaySetup=false in sheet mode even when keypad is open', () => {
+ mockUsePredictBuyInputState.mockReturnValueOnce(
+ mockHookReturnWithKeypadOpen(true),
+ );
+
+ renderWithProvider();
+
+ expect(mockPredictPayWithAnyTokenInfo).toHaveBeenLastCalledWith(
+ expect.objectContaining({ shouldDeferRelaySetup: false }),
+ );
+ });
+
+ it('passes shouldDeferRelaySetup=false in non-sheet mode when keypad is closed', () => {
+ mockUsePredictBuyInputState.mockReturnValueOnce(
+ mockHookReturnWithKeypadOpen(false),
+ );
+
+ renderWithProvider();
+
+ expect(mockPredictPayWithAnyTokenInfo).toHaveBeenLastCalledWith(
+ expect.objectContaining({ shouldDeferRelaySetup: false }),
+ );
+ });
+
+ it('passes shouldDeferRelaySetup=true in non-sheet mode when keypad is open', () => {
+ mockUsePredictBuyInputState.mockReturnValueOnce(
+ mockHookReturnWithKeypadOpen(true),
+ );
+
+ renderWithProvider();
+
+ expect(mockPredictPayWithAnyTokenInfo).toHaveBeenLastCalledWith(
+ expect.objectContaining({ shouldDeferRelaySetup: true }),
+ );
+ });
+ });
+
describe('CTA button modes', () => {
const sheetProps = {
mode: 'sheet' as const,
diff --git a/app/components/UI/Predict/views/PredictBuyWithAnyToken/PredictBuyWithAnyToken.tsx b/app/components/UI/Predict/views/PredictBuyWithAnyToken/PredictBuyWithAnyToken.tsx
index 24c7879988d5..0ee65666cd92 100644
--- a/app/components/UI/Predict/views/PredictBuyWithAnyToken/PredictBuyWithAnyToken.tsx
+++ b/app/components/UI/Predict/views/PredictBuyWithAnyToken/PredictBuyWithAnyToken.tsx
@@ -165,21 +165,21 @@ const PredictBuyWithAnyToken = (props: PredictBuyPreviewProps) => {
setCurrentValue,
currentValueUSDString,
setCurrentValueUSDString,
- isInputFocused,
- setIsInputFocused,
+ isKeypadOpen,
+ setIsKeypadOpen,
isUserInputChange,
setIsUserInputChange,
isConfirming,
setIsConfirming,
- } = usePredictBuyInputState();
+ } = usePredictBuyInputState({ initialKeypadOpen: !isSheetMode });
const handleQuickAmount = useCallback(
(amount: number) => {
setCurrentValue(amount);
setCurrentValueUSDString(amount.toString());
- setIsInputFocused(false);
+ setIsKeypadOpen(false);
},
- [setCurrentValue, setCurrentValueUSDString, setIsInputFocused],
+ [setCurrentValue, setCurrentValueUSDString, setIsKeypadOpen],
);
const handleFeesInfoPress = useCallback(() => {
@@ -333,14 +333,14 @@ const PredictBuyWithAnyToken = (props: PredictBuyPreviewProps) => {
previousValueRef.current = currentValue;
}, [currentValue, isUserInputChange, clearBuyErrorBanner]);
- // When the banner appears in sheet mode, blur the amount input so the keypad
- // collapses and the Retry CTA + banner are immediately visible without the
- // user having to dismiss the keyboard.
+ // When the banner appears in sheet mode, close the keypad so the Retry CTA
+ // + banner are immediately visible without the user having to dismiss the
+ // keyboard.
useEffect(() => {
- if (isSheetMode && isBannerActive && isInputFocused) {
- setIsInputFocused(false);
+ if (isSheetMode && isBannerActive && isKeypadOpen) {
+ setIsKeypadOpen(false);
}
- }, [isSheetMode, isBannerActive, isInputFocused, setIsInputFocused]);
+ }, [isSheetMode, isBannerActive, isKeypadOpen, setIsKeypadOpen]);
const handleBuyButtonPress = useCallback(() => {
if (isPaymentSelectorNavigationLocked) {
@@ -432,7 +432,7 @@ const PredictBuyWithAnyToken = (props: PredictBuyPreviewProps) => {
{
{
{!isSheetMode && (
)}
-
- {isSheetMode && (
-
- )}
- {payWithAnyTokenEnabled && isSheetMode && (
-
+ {isSheetMode && (
+
+ )}
+ {payWithAnyTokenEnabled && isSheetMode && (
+
+ )}
+ {/* Always enabled when rendered: in legacy mode the parent only
+ mounts PredictBuyBottomContent while the keypad is closed; in
+ sheet mode the fee summary is always actionable. */}
+
- )}
-
- {isSheetMode && buyErrorBanner && (
-
+ )}
+
- )}
-
-
+
+ )}
{isSheetMode && (
)}
@@ -581,7 +583,7 @@ const PredictBuyWithAnyToken = (props: PredictBuyPreviewProps) => {
);
diff --git a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyAmountSection/PredictBuyAmountSection.test.tsx b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyAmountSection/PredictBuyAmountSection.test.tsx
index b371304903fe..fdadcfe21e02 100644
--- a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyAmountSection/PredictBuyAmountSection.test.tsx
+++ b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyAmountSection/PredictBuyAmountSection.test.tsx
@@ -71,7 +71,7 @@ describe('PredictBuyAmountSection', () => {
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
;
- isInputFocused: boolean;
+ isKeypadOpen: boolean;
isBalanceLoading: boolean;
isBalancePulsing: boolean;
availableBalanceDisplay: string;
@@ -32,7 +32,7 @@ interface PredictBuyAmountSectionProps {
const PredictBuyAmountSection = ({
currentValueUSDString,
keypadRef,
- isInputFocused,
+ isKeypadOpen,
isBalanceLoading,
isBalancePulsing,
availableBalanceDisplay,
@@ -75,7 +75,7 @@ const PredictBuyAmountSection = ({
onPress={() =>
!isPlacingOrder && keypadRef.current?.handleAmountPress()
}
- isActive={isInputFocused && !isPlacingOrder}
+ isActive={isKeypadOpen && !isPlacingOrder}
hasError={false}
/>
diff --git a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyBottomContent/PredictBuyBottomContent.test.tsx b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyBottomContent/PredictBuyBottomContent.test.tsx
index ca84d50fcf92..46266e4e0f14 100644
--- a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyBottomContent/PredictBuyBottomContent.test.tsx
+++ b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyBottomContent/PredictBuyBottomContent.test.tsx
@@ -34,25 +34,10 @@ describe('PredictBuyBottomContent', () => {
jest.clearAllMocks();
});
- describe('when isInputFocused is true', () => {
- it('returns null and does not render anything', () => {
- renderWithProvider(
-
- {mockChildren}
- ,
- );
-
- expect(screen.queryByText(/Disclaimer text/)).not.toBeOnTheScreen();
- expect(screen.queryByTestId('children-content')).not.toBeOnTheScreen();
- });
- });
-
- describe('when isInputFocused is false', () => {
+ describe('rendering', () => {
it('renders children content', () => {
renderWithProvider(
-
- {mockChildren}
- ,
+ {mockChildren},
);
expect(screen.getByTestId('children-content')).toBeOnTheScreen();
@@ -60,9 +45,7 @@ describe('PredictBuyBottomContent', () => {
it('renders disclaimer text', () => {
renderWithProvider(
-
- {mockChildren}
- ,
+ {mockChildren},
);
expect(screen.getByText(/Disclaimer text/)).toBeOnTheScreen();
@@ -70,9 +53,7 @@ describe('PredictBuyBottomContent', () => {
it('renders learn more link', () => {
renderWithProvider(
-
- {mockChildren}
- ,
+ {mockChildren},
);
expect(screen.getByText(/Learn more/)).toBeOnTheScreen();
@@ -80,9 +61,7 @@ describe('PredictBuyBottomContent', () => {
it('opens Polymarket TOS URL when learn more is pressed', () => {
renderWithProvider(
-
- {mockChildren}
- ,
+ {mockChildren},
);
const learnMoreLink = screen.getByText(/Learn more/);
@@ -108,9 +87,7 @@ describe('PredictBuyBottomContent', () => {
);
renderWithProvider(
-
- {multipleChildren}
- ,
+ {multipleChildren},
);
expect(screen.getByTestId('child-1')).toBeOnTheScreen();
@@ -121,9 +98,7 @@ describe('PredictBuyBottomContent', () => {
describe('Linking behavior', () => {
it('calls Linking.openURL with correct URL', () => {
renderWithProvider(
-
- {mockChildren}
- ,
+ {mockChildren},
);
const learnMoreLink = screen.getByText(/Learn more/);
@@ -137,9 +112,7 @@ describe('PredictBuyBottomContent', () => {
it('opens URL only when learn more is pressed', () => {
renderWithProvider(
-
- {mockChildren}
- ,
+ {mockChildren},
);
expect(Linking.openURL).not.toHaveBeenCalled();
diff --git a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyBottomContent/PredictBuyBottomContent.tsx b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyBottomContent/PredictBuyBottomContent.tsx
index 316da14465ca..e507c9de80a7 100644
--- a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyBottomContent/PredictBuyBottomContent.tsx
+++ b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictBuyBottomContent/PredictBuyBottomContent.tsx
@@ -12,23 +12,16 @@ import { Linking } from 'react-native';
import { strings } from '../../../../../../../../locales/i18n';
interface PredictBuyBottomContentProps {
- isInputFocused: boolean;
-
hideBorder?: boolean;
children: React.ReactNode;
}
const PredictBuyBottomContent = ({
- isInputFocused,
hideBorder = false,
children,
}: PredictBuyBottomContentProps) => {
const tw = useTailwind();
- if (isInputFocused) {
- return null;
- }
-
return (
{
,
);
@@ -149,7 +149,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -163,7 +163,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -177,7 +177,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -204,7 +204,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
collector: '0xCollector',
},
})}
- isInputFocused={false}
+ shouldDeferRelaySetup={false}
/>,
);
@@ -229,7 +229,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
collector: '0xCollector',
},
})}
- isInputFocused={false}
+ shouldDeferRelaySetup={false}
/>,
);
@@ -254,7 +254,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
collector: '0xCollector',
},
})}
- isInputFocused={false}
+ shouldDeferRelaySetup={false}
/>,
);
@@ -281,7 +281,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
collector: '0xCollector',
},
})}
- isInputFocused={false}
+ shouldDeferRelaySetup={false}
/>,
);
@@ -299,7 +299,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
preview={createMockPreview({
maxAmountSpent: 99.99,
})}
- isInputFocused={false}
+ shouldDeferRelaySetup={false}
/>,
);
@@ -323,7 +323,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
collector: '0xCollector',
},
})}
- isInputFocused={false}
+ shouldDeferRelaySetup={false}
/>,
);
@@ -333,28 +333,28 @@ describe('PredictPayWithAnyTokenInfo', () => {
});
describe('deposit amount gating', () => {
- it('does not commit deposit amount while input is focused', () => {
+ it('does not commit deposit amount while relay setup is deferred', () => {
mockActiveTransactionMeta = { id: 'tx-1' };
render(
,
);
expect(mockUpdatePendingAmount).not.toHaveBeenCalled();
});
- it('commits deposit amount when input loses focus', () => {
+ it('commits deposit amount when relay setup deferral is released', () => {
mockActiveTransactionMeta = { id: 'tx-1' };
const { rerender } = render(
,
);
@@ -364,7 +364,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -378,7 +378,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -391,7 +391,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -399,7 +399,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -414,7 +414,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -429,7 +429,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -444,7 +444,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -455,7 +455,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -465,7 +465,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -482,7 +482,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -497,7 +497,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -512,7 +512,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -527,7 +527,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -551,7 +551,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
collector: '0xCollector',
},
})}
- isInputFocused={false}
+ shouldDeferRelaySetup={false}
/>,
);
@@ -569,7 +569,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -594,7 +594,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
collector: '0xCollector',
},
})}
- isInputFocused={false}
+ shouldDeferRelaySetup={false}
/>,
);
@@ -610,7 +610,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -626,7 +626,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -642,7 +642,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -658,7 +658,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -674,7 +674,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -696,7 +696,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -711,7 +711,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -729,7 +729,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -754,7 +754,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -779,7 +779,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -798,7 +798,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -813,7 +813,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -830,7 +830,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -847,7 +847,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
@@ -865,7 +865,7 @@ describe('PredictPayWithAnyTokenInfo', () => {
,
);
diff --git a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictPayWithAnyTokenInfo/PredictPayWithAnyTokenInfo.tsx b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictPayWithAnyTokenInfo/PredictPayWithAnyTokenInfo.tsx
index 3a7013abb42c..a43c4af2dbb5 100644
--- a/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictPayWithAnyTokenInfo/PredictPayWithAnyTokenInfo.tsx
+++ b/app/components/UI/Predict/views/PredictBuyWithAnyToken/components/PredictPayWithAnyTokenInfo/PredictPayWithAnyTokenInfo.tsx
@@ -15,13 +15,21 @@ import { getPredictBuyAllInCost } from '../../../../utils/orders';
interface PredictPayWithAnyTokenInfoProps {
currentValue: number;
preview?: OrderPreview | null;
- isInputFocused: boolean;
+ /**
+ * When true, defers the mm_pay relay-config side effects
+ * (`updatePendingAmount` / `setPayToken`). The legacy full-screen flow
+ * sets this while the keypad is open and only releases it on Done so the
+ * relay isn't reconfigured on every keystroke. The bottom-sheet flow
+ * keeps it false because there is no Done affordance and the user can
+ * tap Confirm while the keypad is still open.
+ */
+ shouldDeferRelaySetup: boolean;
}
const PredictPayWithAnyTokenInfo = ({
currentValue,
preview,
- isInputFocused,
+ shouldDeferRelaySetup,
}: PredictPayWithAnyTokenInfoProps) => {
const transactionMeta = useTransactionMetadataRequest();
@@ -33,7 +41,7 @@ const PredictPayWithAnyTokenInfo = ({
);
};
@@ -41,7 +49,7 @@ const PredictPayWithAnyTokenInfo = ({
function PredictPayWithAnyTokenInfoInner({
currentValue,
preview,
- isInputFocused,
+ shouldDeferRelaySetup,
}: PredictPayWithAnyTokenInfoProps) {
const [depositAmount, setDepositAmount] = useState('');
@@ -66,8 +74,8 @@ function PredictPayWithAnyTokenInfoInner({
!isPredictBalanceSelected &&
!!fees &&
currentValue >= MINIMUM_BET &&
- !isInputFocused,
- [isPredictBalanceSelected, fees, currentValue, isInputFocused],
+ !shouldDeferRelaySetup,
+ [isPredictBalanceSelected, fees, currentValue, shouldDeferRelaySetup],
);
const computedDepositAmount = useMemo(() => {
diff --git a/app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyInputState.test.ts b/app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyInputState.test.ts
index 43c1a909ecc6..507043cac8e4 100644
--- a/app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyInputState.test.ts
+++ b/app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyInputState.test.ts
@@ -94,21 +94,37 @@ describe('usePredictBuyInputState', () => {
});
});
- describe('isInputFocused', () => {
- it('initializes to true', () => {
+ describe('isKeypadOpen', () => {
+ it('initializes to true by default', () => {
const { result } = renderHook(() => usePredictBuyInputState());
- expect(result.current.isInputFocused).toBe(true);
+ expect(result.current.isKeypadOpen).toBe(true);
});
- it('updates isInputFocused via setIsInputFocused', () => {
+ it('honours initialKeypadOpen=false from caller options', () => {
+ const { result } = renderHook(() =>
+ usePredictBuyInputState({ initialKeypadOpen: false }),
+ );
+
+ expect(result.current.isKeypadOpen).toBe(false);
+ });
+
+ it('honours initialKeypadOpen=true from caller options', () => {
+ const { result } = renderHook(() =>
+ usePredictBuyInputState({ initialKeypadOpen: true }),
+ );
+
+ expect(result.current.isKeypadOpen).toBe(true);
+ });
+
+ it('updates isKeypadOpen via setIsKeypadOpen', () => {
const { result } = renderHook(() => usePredictBuyInputState());
act(() => {
- result.current.setIsInputFocused(false);
+ result.current.setIsKeypadOpen(false);
});
- expect(result.current.isInputFocused).toBe(false);
+ expect(result.current.isKeypadOpen).toBe(false);
});
});
diff --git a/app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyInputState.ts b/app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyInputState.ts
index a65dd0058226..214b171a7bff 100644
--- a/app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyInputState.ts
+++ b/app/components/UI/Predict/views/PredictBuyWithAnyToken/hooks/usePredictBuyInputState.ts
@@ -1,7 +1,13 @@
import { SetStateAction, useCallback, useRef, useState } from 'react';
import { usePredictActiveOrder } from '../../../hooks/usePredictActiveOrder';
-export const usePredictBuyInputState = () => {
+interface UsePredictBuyInputStateOptions {
+ initialKeypadOpen?: boolean;
+}
+
+export const usePredictBuyInputState = ({
+ initialKeypadOpen = true,
+}: UsePredictBuyInputStateOptions = {}) => {
const { clearOrderError } = usePredictActiveOrder();
const [currentValue, setCurrentValueState] = useState(0);
@@ -13,14 +19,14 @@ export const usePredictBuyInputState = () => {
currentValue ? currentValue.toString() : '',
);
- const [isInputFocused, setIsInputFocusedState] = useState(true);
+ const [isKeypadOpen, setIsKeypadOpenState] = useState(initialKeypadOpen);
const shouldSyncCurrentValueRef = useRef(false);
const shouldClearAmountErrorRef = useRef(false);
- const shouldSyncInputFocusRef = useRef(false);
+ const shouldSyncKeypadOpenRef = useRef(false);
- const setIsInputFocused = useCallback((nextIsInputFocused: boolean) => {
- shouldSyncInputFocusRef.current = true;
- setIsInputFocusedState(nextIsInputFocused);
+ const setIsKeypadOpen = useCallback((nextIsKeypadOpen: boolean) => {
+ shouldSyncKeypadOpenRef.current = true;
+ setIsKeypadOpenState(nextIsKeypadOpen);
}, []);
const [isUserInputChange, setIsUserInputChange] = useState(false);
@@ -56,8 +62,8 @@ export const usePredictBuyInputState = () => {
setCurrentValue,
currentValueUSDString,
setCurrentValueUSDString,
- isInputFocused,
- setIsInputFocused,
+ isKeypadOpen,
+ setIsKeypadOpen,
isUserInputChange,
setIsUserInputChange,
isConfirming,
From 9e04ba8013aa277868db86dc212623460d6c3d6f Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Thu, 21 May 2026 02:09:50 +0000
Subject: [PATCH 40/66] [skip ci] Bump version number to 5114
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 544d09041009..510548a42db3 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5111
+ versionCode 5114
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 0b4f73179d8f..123f2c4e5896 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5111
+ VERSION_NUMBER: 5114
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5111
+ FLASK_VERSION_NUMBER: 5114
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 9535c4c67829..766b53b592d7 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5111;
+ CURRENT_PROJECT_VERSION = 5114;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5111;
+ CURRENT_PROJECT_VERSION = 5114;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5111;
+ CURRENT_PROJECT_VERSION = 5114;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5111;
+ CURRENT_PROJECT_VERSION = 5114;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 5f65e93f97c7226cea6a13f0305bd7dd7d271b3d Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 09:23:00 +0200
Subject: [PATCH 41/66] chore(runway): cherry-pick chore: track x/tweet card
taps in Market Insights cp-7.78.0 (#30465)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- chore: track x/tweet card taps in Market Insights cp-7.78.0 (#30453)
## **Description**
Tracks when a user taps a tweet card in the Market Insights view by
firing the existing `MARKET_INSIGHTS_INTERACTION` event with the
`source_url` set to the tweet's URL.
No segment-schema changes were needed.
## **Changelog**
CHANGELOG entry: null
## **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**
- [ ] 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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Low risk: adds a single analytics call on tweet-card taps and updates
a unit test; behavior change is limited to emitting an event before
opening the URL.
>
> **Overview**
> Market Insights now **tracks tweet-card taps** by firing
`MetaMetricsEvents.MARKET_INSIGHTS_INTERACTION` with `interaction_type:
'source_click'` and the tapped `source_url` before opening the link.
>
> Tests were updated to assert the new analytics event is emitted when a
tweet card is pressed.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
938efad0d2f3ffaf47029d3a43d7414a806b2a9b. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[d7d9e04](https://github.com/MetaMask/metamask-mobile/commit/d7d9e0497412ca8e1a6be772b5908207b0a63984)
---------
Co-authored-by: António Regadas
Co-authored-by: Bigshmow
Co-authored-by: Devin Stewart <49423028+Bigshmow@users.noreply.github.com>
---
.../MarketInsightsView.test.tsx | 11 ++++++++-
.../MarketInsightsView/MarketInsightsView.tsx | 23 +++++++++++--------
2 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.test.tsx b/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.test.tsx
index 2a8d5d84228d..d4e30f90a629 100644
--- a/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.test.tsx
+++ b/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.test.tsx
@@ -480,6 +480,15 @@ describe('MarketInsightsView', () => {
expect(Linking.openURL).toHaveBeenCalledWith(
'https://x.com/user/status/100',
);
+ expect(mockTrackEvent).toHaveBeenCalledWith(
+ expect.objectContaining({
+ category: MetaMetricsEvents.MARKET_INSIGHTS_INTERACTION,
+ properties: expect.objectContaining({
+ interaction_type: 'source_click',
+ source_url: 'https://x.com/user/status/100',
+ }),
+ }),
+ );
expect(getByTestId('token-details-sticky-footer')).toBeOnTheScreen();
@@ -541,7 +550,7 @@ describe('MarketInsightsView', () => {
properties: expect.objectContaining({
asset_symbol: 'eth',
interaction_type: 'source_click',
- source: 'https://www.coindesk.com/article',
+ source_url: 'https://www.coindesk.com/article',
}),
}),
);
diff --git a/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.tsx b/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.tsx
index 758632cf178b..e00b44b1cc0b 100644
--- a/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.tsx
+++ b/app/components/UI/MarketInsights/Views/MarketInsightsView/MarketInsightsView.tsx
@@ -290,12 +290,6 @@ const MarketInsightsView: React.FC = () => {
assetSymbolProperty,
]);
- const handleTweetPress = useCallback((url: string) => {
- if (isSafeUrl(url)) {
- Linking.openURL(url);
- }
- }, []);
-
const closeEligibilityModal = useCallback(() => {
setIsEligibilityModalVisible(false);
}, []);
@@ -410,7 +404,7 @@ const MarketInsightsView: React.FC = () => {
(
interactionType: 'thumbs_up' | 'thumbs_down' | 'source_click',
options?: {
- source?: string;
+ sourceUrl?: string;
feedbackReason?: MarketInsightsFeedbackReason;
feedbackText?: string;
},
@@ -419,7 +413,7 @@ const MarketInsightsView: React.FC = () => {
...assetIdProperty,
...assetSymbolProperty,
interaction_type: interactionType,
- ...(options?.source ? { source: options.source } : {}),
+ ...(options?.sourceUrl ? { source_url: options.sourceUrl } : {}),
...(options?.feedbackReason
? { feedback_reason: options.feedbackReason }
: {}),
@@ -437,6 +431,17 @@ const MarketInsightsView: React.FC = () => {
[trackEvent, createEventBuilder, assetIdProperty, assetSymbolProperty],
);
+ const handleTweetPress = useCallback(
+ (url: string) => {
+ if (!isSafeUrl(url)) {
+ return;
+ }
+ trackMarketInsightsInteraction('source_click', { sourceUrl: url });
+ Linking.openURL(url);
+ },
+ [trackMarketInsightsInteraction],
+ );
+
const showFeedbackSubmittedToast = useCallback(() => {
toastRef?.current?.showToast({
variant: ToastVariants.Icon,
@@ -512,7 +517,7 @@ const MarketInsightsView: React.FC = () => {
if (!isSafeUrl(url)) {
return;
}
- trackMarketInsightsInteraction('source_click', { source: url });
+ trackMarketInsightsInteraction('source_click', { sourceUrl: url });
setSelectedTrend(null);
navigation.navigate(Routes.BROWSER.HOME, {
screen: Routes.BROWSER.VIEW,
From cc2c7d582907a35ba385b942686a57813fdd1e0b Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 10:03:19 +0200
Subject: [PATCH 42/66] chore(runway): cherry-pick fix: football prediction
markets not loading (#30490)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- fix: football prediction markets not loading cp-7.78.0 (#30466)
## **Description**
**Root cause:** The Polymarket API returns `null` for the
`orderPriceMinTickSize` field on some sports markets. Our code was
calling `.toString()` on that value without a null check, which threw a
runtime error. Since the entire batch of events was parsed in a single
`.map()`, one bad event crashed the whole thing and returned an empty
list.
**Fix:**
1. Changed `market.orderPriceMinTickSize.toString()` to
`market.orderPriceMinTickSize?.toString() ?? '0.01'` to safely handle
the null.
2. Updated the TypeScript type to `number | null` to correctly reflect
what the API actually returns.
3. Changed the event parsing loop from `.map()` to `.flatMap()` with a
per-event `try/catch`, so a single malformed event is skipped and logged
instead of taking down the whole batch.
## **Changelog**
CHANGELOG entry: football prediction markets not loading
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3259
## **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**
https://github.com/user-attachments/assets/3fb77be4-1060-45c3-adb2-cb75cd4f41a8
### **After**
https://github.com/user-attachments/assets/464094d7-94ea-4002-8531-498bb6d173da
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Medium Risk**
> Changes market parsing defaults and error handling for Polymarket
events; while localized, it can affect tick-size-dependent pricing
behavior and which events appear if parsing failures occur.
>
> **Overview**
> Fixes Polymarket sports markets failing to load when the API returns a
`null` `orderPriceMinTickSize`.
>
> Updates the Polymarket market type to allow `orderPriceMinTickSize:
number | null`, safely derives `tickSize` with a fallback default, and
makes `parsePolymarketEvents` resilient by parsing each event in a
`try/catch` (logging and skipping only the bad event instead of failing
the whole batch).
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1f8de4a56425ebce37ac2180e881b9f96fe0d61a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---------
Co-authored-by: Luis Taniça
[e9fa454](https://github.com/MetaMask/metamask-mobile/commit/e9fa45487bedc5f00579a7c01011b505b2a5e086)
Co-authored-by: Juanmi <95381763+juanmigdr@users.noreply.github.com>
Co-authored-by: Luis Taniça
---
.../UI/Predict/providers/polymarket/types.ts | 2 +-
.../UI/Predict/providers/polymarket/utils.ts | 65 +++++++++++--------
2 files changed, 39 insertions(+), 28 deletions(-)
diff --git a/app/components/UI/Predict/providers/polymarket/types.ts b/app/components/UI/Predict/providers/polymarket/types.ts
index b2ded81342d8..12e3a17db279 100644
--- a/app/components/UI/Predict/providers/polymarket/types.ts
+++ b/app/components/UI/Predict/providers/polymarket/types.ts
@@ -75,7 +75,7 @@ export interface PolymarketApiMarket {
closed: boolean;
active: boolean;
resolvedBy: string;
- orderPriceMinTickSize: number;
+ orderPriceMinTickSize: number | null;
events?: PolymarketApiEvent[];
umaResolutionStatus: string;
line?: number;
diff --git a/app/components/UI/Predict/providers/polymarket/utils.ts b/app/components/UI/Predict/providers/polymarket/utils.ts
index 35864170d7be..06a76658013d 100644
--- a/app/components/UI/Predict/providers/polymarket/utils.ts
+++ b/app/components/UI/Predict/providers/polymarket/utils.ts
@@ -1006,7 +1006,7 @@ export const parsePolymarketMarket = (
sportsMarketType: market.sportsMarketType,
line: market.line,
negRisk: market.negRisk,
- tickSize: market.orderPriceMinTickSize.toString(),
+ tickSize: market.orderPriceMinTickSize?.toString() ?? '0.01',
resolvedBy: market.resolvedBy,
resolutionStatus: market.umaResolutionStatus,
});
@@ -1036,8 +1036,8 @@ export const parsePolymarketEvents = (
const { category, teamLookup, extendedSportsMarketsLeagues } = options;
const sortBy = options.sortMarketsBy ?? sortMarketsBy;
- const parsedMarkets: PredictMarket[] = events.map(
- (event: PolymarketApiEvent) => {
+ return events.flatMap((event: PolymarketApiEvent) => {
+ try {
const tags = Array.isArray(event.tags) ? event.tags : [];
const eventLeague = getEventLeague(event, extendedSportsMarketsLeagues);
@@ -1091,30 +1091,41 @@ export const parsePolymarketEvents = (
? buildOutcomeGroups(outcomes)
: undefined;
- return {
- id: event.id,
- slug: event.slug,
- providerId: POLYMARKET_PROVIDER_ID,
- title: event.title,
- description,
- image: event.icon,
- status: event.closed
- ? PredictMarketStatus.CLOSED
- : PredictMarketStatus.OPEN,
- recurrence: getRecurrence(event.series),
- endDate: event.endDate,
- category,
- tags: tags.map((t) => t.slug),
- outcomes,
- ...(outcomeGroups && { outcomeGroups }),
- liquidity: event.liquidity,
- volume: event.volume,
- game,
- ...(seriesData && { series: seriesData }),
- };
- },
- );
- return parsedMarkets;
+ return [
+ {
+ id: event.id,
+ slug: event.slug,
+ providerId: POLYMARKET_PROVIDER_ID,
+ title: event.title,
+ description,
+ image: event.icon,
+ status: event.closed
+ ? PredictMarketStatus.CLOSED
+ : PredictMarketStatus.OPEN,
+ recurrence: getRecurrence(event.series),
+ endDate: event.endDate,
+ category,
+ tags: tags.map((t) => t.slug),
+ outcomes,
+ ...(outcomeGroups && { outcomeGroups }),
+ liquidity: event.liquidity,
+ volume: event.volume,
+ game,
+ ...(seriesData && { series: seriesData }),
+ ...(event.parentEventId !== undefined && {
+ parentMarketId: event.parentEventId,
+ }),
+ } as PredictMarket,
+ ];
+ } catch (err) {
+ Logger.error(err instanceof Error ? err : new Error(String(err)), {
+ feature: 'predict',
+ method: 'parsePolymarketEvents',
+ eventId: event.id,
+ });
+ return [];
+ }
+ });
};
/**
From d55933ba2b1b74831020a35eb5f004d0286d94e8 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Thu, 21 May 2026 08:05:10 +0000
Subject: [PATCH 43/66] [skip ci] Bump version number to 5119
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 510548a42db3..bc5769aa7fcc 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5114
+ versionCode 5119
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 123f2c4e5896..3a71a27adb17 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5114
+ VERSION_NUMBER: 5119
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5114
+ FLASK_VERSION_NUMBER: 5119
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 766b53b592d7..1bb343517623 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5114;
+ CURRENT_PROJECT_VERSION = 5119;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5114;
+ CURRENT_PROJECT_VERSION = 5119;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5114;
+ CURRENT_PROJECT_VERSION = 5119;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5114;
+ CURRENT_PROJECT_VERSION = 5119;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From e5b845ae1f7104928f1dfade5c2df4f9eabb51a2 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 11:01:47 +0200
Subject: [PATCH 44/66] chore(runway): cherry-pick fix: handle BTC quote when
BTC quote has missing or non-positive network fee data (#30494)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- fix: handle BTC quote when BTC quote has missing or non-positive
network fee data (#30313)
## **Description**
This pull request improves how the Bridge UI handles and displays cases
where the network fee is unavailable, especially for Bitcoin (BTC)
bridges. It introduces a new hook to detect unavailable network fees,
updates the logic for disabling/enabling the confirm button, and adjusts
messaging and analytics accordingly. The changes also include
comprehensive tests for these scenarios.
**Key changes:**
**1. Handling Network Fee Unavailability in the UI**
- Added the `useIsNetworkFeeUnavailable` hook to detect when the network
fee (especially for BTC) is unavailable and integrated it into both
`BridgeView` and `SwapsConfirmButton` components. This disables the
confirm button and updates the label to "Insufficient funds" when the
fee is unavailable.
**2. Analytics and Event Tracking**
- Updated the `useBridgeQuoteEvents` hook and its consumers to track
when the network fee is unavailable, emitting a new
`network_fee_unavailable` warning instead of the generic insufficient
gas warning.
**3. Gas Fee Calculation Logic**
- Modified `useHasSufficientGas` to use `totalNetworkFee.amount` for BTC
quotes instead of `gasFee.effective.amount`, ensuring correct balance
validation for Bitcoin transactions.
**4. Testing and Coverage**
- Added and updated tests for both the UI and hooks to cover scenarios
where the BTC network fee is unavailable, ensuring proper button states,
messaging, and analytics events. This includes new test cases for
`SwapsConfirmButton` and `useBridgeQuoteEvents`, and additional
BTC-specific tests for `useHasSufficientGas`.
These updates ensure that users are clearly informed when a bridge
transaction cannot proceed due to unavailable network fees, especially
for Bitcoin, and that analytics accurately reflect this state.
## **Changelog**
CHANGELOG entry: fixed BTC quote when BTC quote has missing or
non-positive network fee data
## **Related issues**
Fixes: Bridging BTC is failing at the transaction crafting in certain
cases
## **Manual testing steps**
BTC quote with unavailable fee
1. Open the Bridge/Swap flow.
2. Select BTC as the source token and ETH as the destination token.
3. Enter an amount that can produce a BTC quote.
4. Simulate or reproduce a quote where the BTC network fee is 0 or
unavailable.
5. Confirm the CTA shows Insufficient funds.
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Medium Risk**
> Updates bridge confirmation disablement, labeling, and analytics
around fee/gas validation, which can block submissions and alter event
warnings; impact is mostly limited to BTC and UI state handling but
affects core swap/bridge CTA logic.
>
> **Overview**
> **Improves BTC quote handling when network fee data is
missing/invalid.** Adds `useIsNetworkFeeUnavailable` to detect BTC
quotes with missing, non-finite, or non-positive
`totalNetworkFee.amount`, and uses it to disable submission and show
*Insufficient funds* (instead of *Insufficient gas*) in `BridgeView`,
`SwapsConfirmButton`, and Quick Buy.
>
> **Adjusts fee validation + analytics.** `useHasSufficientGas` now
prefers `totalNetworkFee.amount` for BTC when checking gas sufficiency,
and `useBridgeQuoteEvents` emits a new `network_fee_unavailable` warning
(taking precedence over insufficient gas).
>
> **Expands test coverage.** Adds/updates unit tests to cover BTC
network-fee-unavailable cases across confirm button states, “Get new
quote” enablement, quote warnings, Quick Buy button errors, and BTC gas
validation.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
30e4c630fd67b9987a48ac6f3683e291805a503d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[5042fdb](https://github.com/MetaMask/metamask-mobile/commit/5042fdb184b5024bf8eb0cbb5c10d26e3436c11c)
Co-authored-by: Fred
---
.../UI/Bridge/Views/BridgeView/index.tsx | 9 +-
.../SwapsConfirmButton.test.tsx | 92 ++++++++++++++++-
.../components/SwapsConfirmButton/index.tsx | 17 +++-
.../hooks/useBridgeQuoteEvents/index.ts | 8 +-
.../useBridgeQuoteEvents.test.tsx | 7 ++
.../Bridge/hooks/useHasSufficientGas/index.ts | 6 +-
.../useHasSufficientGas.test.ts | 36 +++++++
.../useIsNetworkFeeUnavailable/index.test.ts | 99 +++++++++++++++++++
.../hooks/useIsNetworkFeeUnavailable/index.ts | 27 +++++
.../useQuickBuyBottomSheet.test.ts | 40 +++++++-
.../useQuickBuyBottomSheet.ts | 32 +++---
11 files changed, 352 insertions(+), 21 deletions(-)
create mode 100644 app/components/UI/Bridge/hooks/useIsNetworkFeeUnavailable/index.test.ts
create mode 100644 app/components/UI/Bridge/hooks/useIsNetworkFeeUnavailable/index.ts
diff --git a/app/components/UI/Bridge/Views/BridgeView/index.tsx b/app/components/UI/Bridge/Views/BridgeView/index.tsx
index c28fd6a2bcdc..2cd3a591daee 100644
--- a/app/components/UI/Bridge/Views/BridgeView/index.tsx
+++ b/app/components/UI/Bridge/Views/BridgeView/index.tsx
@@ -118,6 +118,7 @@ import {
ButtonSize,
ButtonVariants,
} from '../../../../../component-library/components/Buttons/Button/Button.types.ts';
+import { useIsNetworkFeeUnavailable } from '../../hooks/useIsNetworkFeeUnavailable/index.ts';
const SCROLL_NEAR_BOTTOM_PX = 160;
@@ -271,7 +272,9 @@ const BridgeViewContent = ({ latestSourceBalance }: BridgeViewContentProps) => {
// Destination address is only needed for EVM <> Non-EVM bridges, or Non-EVM <> Non-EVM bridges (when different)
(!hasDestinationPicker || (hasDestinationPicker && Boolean(destAddress)));
+ const isNetworkFeeUnavailable = useIsNetworkFeeUnavailable(activeQuote);
const hasSufficientGas = useHasSufficientGas({ quote: activeQuote });
+ const hasInsufficientGas = !isNetworkFeeUnavailable && !hasSufficientGas;
const hasInsufficientBalance = useIsInsufficientBalance({
amount: sourceAmount,
token: sourceToken,
@@ -310,18 +313,20 @@ const BridgeViewContent = ({ latestSourceBalance }: BridgeViewContentProps) => {
(isLoading && !activeQuote) ||
hasInsufficientBalance ||
hasInsufficientNativeReserveError ||
+ isNetworkFeeUnavailable ||
isSubmittingTx ||
(isHardwareAddress && isSolanaSourced) ||
!!blockaidError ||
- !hasSufficientGas ||
+ hasInsufficientGas ||
!walletAddress;
useBridgeQuoteEvents({
hasInsufficientBalance,
hasInsufficientNativeReserveError,
hasNoQuotesAvailable: isNoQuotesAvailable,
- hasInsufficientGas: !hasSufficientGas,
+ hasInsufficientGas,
hasTxAlert: Boolean(blockaidError),
+ isNetworkFeeUnavailable,
isSubmitDisabled,
isPriceImpactWarningVisible: shouldShowPriceImpactWarning,
});
diff --git a/app/components/UI/Bridge/components/SwapsConfirmButton/SwapsConfirmButton.test.tsx b/app/components/UI/Bridge/components/SwapsConfirmButton/SwapsConfirmButton.test.tsx
index d1d8eaca7ce1..a793603eaf5d 100644
--- a/app/components/UI/Bridge/components/SwapsConfirmButton/SwapsConfirmButton.test.tsx
+++ b/app/components/UI/Bridge/components/SwapsConfirmButton/SwapsConfirmButton.test.tsx
@@ -21,7 +21,10 @@ import { MOCK_ENTROPY_SOURCE as mockEntropySource } from '../../../../../util/te
import { BigNumber } from 'ethers';
import Engine from '../../../../../core/Engine';
import { setSourceAmount } from '../../../../../core/redux/slices/bridge';
-import { MetaMetricsSwapsEventSource } from '@metamask/bridge-controller';
+import {
+ ChainId,
+ MetaMetricsSwapsEventSource,
+} from '@metamask/bridge-controller';
import { PriceImpactModalType } from '../PriceImpactModal/constants';
import { TokenWarningModalMode } from '../TokenWarningModal/constants';
import { SecurityDataType } from '../../types';
@@ -133,6 +136,18 @@ const mockActiveQuote = {
},
};
+const mockBtcQuoteWithUnavailableNetworkFee = {
+ ...mockActiveQuote,
+ quote: {
+ ...mockActiveQuote.quote,
+ srcChainId: ChainId.BTC,
+ },
+ totalNetworkFee: {
+ ...mockActiveQuote.totalNetworkFee,
+ amount: '0',
+ },
+};
+
// Mock useBridgeQuoteData
jest.mock('../../hooks/useBridgeQuoteData', () => ({
useBridgeQuoteData: jest
@@ -337,6 +352,30 @@ describe('SwapsConfirmButton', () => {
expect(getByText(strings('bridge.insufficient_gas'))).toBeTruthy();
});
+ it('displays "Insufficient funds" when BTC network fee is unavailable', () => {
+ jest
+ .mocked(useBridgeQuoteData as unknown as jest.Mock)
+ .mockImplementation(() => ({
+ ...mockUseBridgeQuoteData,
+ activeQuote: mockBtcQuoteWithUnavailableNetworkFee,
+ }));
+
+ const { getByText } = renderWithProvider(
+ ,
+ {
+ state: mockState,
+ },
+ );
+
+ expect(getByText(strings('bridge.insufficient_funds'))).toBeTruthy();
+ expect(useHasSufficientGas).toHaveBeenCalledWith({
+ quote: mockBtcQuoteWithUnavailableNetworkFee,
+ });
+ });
+
it('hides label behind loading indicator when submitting', () => {
const submittingState = {
...mockState,
@@ -773,6 +812,30 @@ describe('SwapsConfirmButton', () => {
// Label visible because not loading/submitting
expect(getByText(strings('bridge.insufficient_gas'))).toBeTruthy();
});
+
+ it('does not show loading when disabled due to unavailable BTC network fee', () => {
+ jest
+ .mocked(useBridgeQuoteData as unknown as jest.Mock)
+ .mockImplementation(() => ({
+ ...mockUseBridgeQuoteData,
+ activeQuote: mockBtcQuoteWithUnavailableNetworkFee,
+ }));
+
+ const { getByText, getByTestId, queryByTestId } = renderWithProvider(
+ ,
+ {
+ state: mockState,
+ },
+ );
+
+ const button = getByTestId(BridgeViewSelectorsIDs.CONFIRM_BUTTON);
+ expect(button.props.accessibilityState?.disabled).toBe(true);
+ expect(queryByTestId('spinner-container')).toBeNull();
+ expect(getByText(strings('bridge.insufficient_funds'))).toBeTruthy();
+ });
});
describe('Stale Quote (isQuoteStale)', () => {
@@ -868,6 +931,33 @@ describe('SwapsConfirmButton', () => {
).toBeTruthy();
});
+ it('keeps "Get new quote" enabled when BTC network fee is unavailable', () => {
+ jest
+ .mocked(useBridgeQuoteData as unknown as jest.Mock)
+ .mockImplementation(() => ({
+ ...mockUseBridgeQuoteData,
+ activeQuote: mockBtcQuoteWithUnavailableNetworkFee,
+ needsNewQuote: true,
+ isLoading: false,
+ }));
+
+ const { getByText, getByTestId } = renderWithProvider(
+ ,
+ {
+ state: mockState,
+ },
+ );
+
+ const button = getByTestId(BridgeViewSelectorsIDs.CONFIRM_BUTTON);
+ expect(button.props.accessibilityState?.disabled).toBe(false);
+ expect(
+ getByText(strings('quote_expired_modal.get_new_quote')),
+ ).toBeTruthy();
+ });
+
it('button is not disabled when quote is expired', () => {
jest
.mocked(useBridgeQuoteData as unknown as jest.Mock)
diff --git a/app/components/UI/Bridge/components/SwapsConfirmButton/index.tsx b/app/components/UI/Bridge/components/SwapsConfirmButton/index.tsx
index 337f24c09a46..55b1b130ec75 100644
--- a/app/components/UI/Bridge/components/SwapsConfirmButton/index.tsx
+++ b/app/components/UI/Bridge/components/SwapsConfirmButton/index.tsx
@@ -40,6 +40,7 @@ import type { TokenWarningModalParams } from '../TokenWarningModal';
import { TokenWarningModalMode } from '../TokenWarningModal/constants';
import type { TransactionActiveAbTestEntry } from '../../../../../util/transactions/transaction-active-ab-test-attribution-registry';
import { useInsufficientNativeReserveError } from '../../hooks/useInsufficientNativeReserveError';
+import { useIsNetworkFeeUnavailable } from '../../hooks/useIsNetworkFeeUnavailable';
interface Props {
latestSourceBalance: ReturnType;
@@ -105,7 +106,9 @@ export const SwapsConfirmButton = ({
transactionActiveAbTests,
});
+ const isNetworkFeeUnavailable = useIsNetworkFeeUnavailable(activeQuote);
const hasSufficientGas = useHasSufficientGas({ quote: activeQuote });
+ const hasInsufficientGas = !isNetworkFeeUnavailable && !hasSufficientGas;
// Check both the display amount and the atomic amount are non-zero.
// An amount like 0.000000001 BTC (8 decimals) is non-zero as a number but
@@ -166,10 +169,11 @@ export const SwapsConfirmButton = ({
(isLoading && !activeQuote) ||
hasInsufficientBalance ||
hasInsufficientNativeReserveError ||
+ isNetworkFeeUnavailable ||
isSubmittingTx ||
(isHardwareAddress && isSolanaSourced) ||
hasError ||
- !hasSufficientGas ||
+ hasInsufficientGas ||
!walletAddress;
const handleContinue = async () => {
@@ -248,9 +252,13 @@ export const SwapsConfirmButton = ({
return strings('bridge.confirm_swap');
}
- if (hasInsufficientBalance || hasInsufficientNativeReserveError)
+ if (
+ hasInsufficientBalance ||
+ hasInsufficientNativeReserveError ||
+ isNetworkFeeUnavailable
+ )
return strings('bridge.insufficient_funds');
- if (!hasSufficientGas) return strings('bridge.insufficient_gas');
+ if (hasInsufficientGas) return strings('bridge.insufficient_gas');
if (isSubmittingTx) return strings('bridge.submitting_transaction');
return strings('bridge.confirm_swap');
@@ -260,7 +268,8 @@ export const SwapsConfirmButton = ({
sourceAmount,
hasInsufficientBalance,
hasInsufficientNativeReserveError,
- hasSufficientGas,
+ isNetworkFeeUnavailable,
+ hasInsufficientGas,
isSubmittingTx,
needsNewQuote,
]);
diff --git a/app/components/UI/Bridge/hooks/useBridgeQuoteEvents/index.ts b/app/components/UI/Bridge/hooks/useBridgeQuoteEvents/index.ts
index 2ce324d172bc..7074e50e4a04 100644
--- a/app/components/UI/Bridge/hooks/useBridgeQuoteEvents/index.ts
+++ b/app/components/UI/Bridge/hooks/useBridgeQuoteEvents/index.ts
@@ -22,6 +22,7 @@ export const useBridgeQuoteEvents = ({
hasInsufficientNativeReserveError,
hasNoQuotesAvailable,
hasInsufficientGas,
+ isNetworkFeeUnavailable,
hasTxAlert,
isSubmitDisabled,
isPriceImpactWarningVisible,
@@ -30,6 +31,7 @@ export const useBridgeQuoteEvents = ({
hasInsufficientNativeReserveError: boolean;
hasNoQuotesAvailable: boolean;
hasInsufficientGas: boolean;
+ isNetworkFeeUnavailable: boolean;
hasTxAlert: boolean;
isSubmitDisabled: boolean;
isPriceImpactWarningVisible: boolean;
@@ -47,8 +49,11 @@ export const useBridgeQuoteEvents = ({
const latestWarnings: QuoteWarning[] = [];
hasNoQuotesAvailable && latestWarnings.push('no_quotes');
- hasInsufficientGas &&
+ if (isNetworkFeeUnavailable) {
+ latestWarnings.push('network_fee_unavailable' as QuoteWarning);
+ } else if (hasInsufficientGas) {
latestWarnings.push('insufficient_gas_for_selected_quote');
+ }
hasInsufficientBalance && latestWarnings.push('insufficient_balance');
hasInsufficientNativeReserveError &&
// @ts-expect-error - 'insufficient_native_reserve' is a valid QuoteWarning
@@ -60,6 +65,7 @@ export const useBridgeQuoteEvents = ({
}, [
hasNoQuotesAvailable,
hasInsufficientGas,
+ isNetworkFeeUnavailable,
hasInsufficientBalance,
hasInsufficientNativeReserveError,
hasTxAlert,
diff --git a/app/components/UI/Bridge/hooks/useBridgeQuoteEvents/useBridgeQuoteEvents.test.tsx b/app/components/UI/Bridge/hooks/useBridgeQuoteEvents/useBridgeQuoteEvents.test.tsx
index 43c622b8985f..e2f2b5df1331 100644
--- a/app/components/UI/Bridge/hooks/useBridgeQuoteEvents/useBridgeQuoteEvents.test.tsx
+++ b/app/components/UI/Bridge/hooks/useBridgeQuoteEvents/useBridgeQuoteEvents.test.tsx
@@ -42,6 +42,7 @@ describe('useBridgeQuoteEvents', () => {
hasNoQuotesAvailable: false,
hasInsufficientBalance: false,
hasInsufficientGas: false,
+ isNetworkFeeUnavailable: false,
hasTxAlert: false,
isSubmitDisabled: false,
isPriceImpactWarningVisible: false,
@@ -58,6 +59,11 @@ describe('useBridgeQuoteEvents', () => {
it.each([
[{ hasNoQuotesAvailable: true }, ['no_quotes']],
[{ hasInsufficientGas: true }, ['insufficient_gas_for_selected_quote']],
+ [{ isNetworkFeeUnavailable: true }, ['network_fee_unavailable']],
+ [
+ { hasInsufficientGas: true, isNetworkFeeUnavailable: true },
+ ['network_fee_unavailable'],
+ ],
[{ hasInsufficientBalance: true }, ['insufficient_balance']],
[{ hasTxAlert: true }, ['tx_alert']],
[{ isPriceImpactWarningVisible: true }, ['price_impact']],
@@ -84,6 +90,7 @@ describe('useBridgeQuoteEvents', () => {
hasNoQuotesAvailable: false,
hasInsufficientBalance: false,
hasInsufficientGas: false,
+ isNetworkFeeUnavailable: false,
hasTxAlert: false,
isSubmitDisabled: false,
isPriceImpactWarningVisible: false,
diff --git a/app/components/UI/Bridge/hooks/useHasSufficientGas/index.ts b/app/components/UI/Bridge/hooks/useHasSufficientGas/index.ts
index 850da58d48dc..4ffbbe679e1d 100644
--- a/app/components/UI/Bridge/hooks/useHasSufficientGas/index.ts
+++ b/app/components/UI/Bridge/hooks/useHasSufficientGas/index.ts
@@ -1,6 +1,7 @@
import {
formatChainIdToCaip,
formatChainIdToHex,
+ isBitcoinChainId,
isNonEvmChainId,
} from '@metamask/bridge-controller';
import { useLatestBalance } from '../useLatestBalance';
@@ -50,7 +51,10 @@ export const useHasSufficientGas = ({ quote }: Props): boolean | null => {
}
// quote.gasFee.effective.amount might be in scientific notation (e.g. 9.200359292e-8), so we need to handle that
- const gasAmount = quote?.gasFee?.effective?.amount;
+ const gasAmount =
+ sourceChainId && isBitcoinChainId(sourceChainId)
+ ? (quote?.totalNetworkFee?.amount ?? quote?.gasFee?.effective?.amount)
+ : quote?.gasFee?.effective?.amount;
const effectiveGasFee =
isNumberValue(gasAmount) && gasAmount != null
? new BigNumber(gasAmount).toFixed()
diff --git a/app/components/UI/Bridge/hooks/useHasSufficientGas/useHasSufficientGas.test.ts b/app/components/UI/Bridge/hooks/useHasSufficientGas/useHasSufficientGas.test.ts
index a58c3e1f8800..4b433d0250e5 100644
--- a/app/components/UI/Bridge/hooks/useHasSufficientGas/useHasSufficientGas.test.ts
+++ b/app/components/UI/Bridge/hooks/useHasSufficientGas/useHasSufficientGas.test.ts
@@ -2,6 +2,7 @@ import { renderHookWithProvider } from '../../../../../util/test/renderWithProvi
import { useHasSufficientGas } from './index';
import { useLatestBalance } from '../useLatestBalance';
import { useBridgeQuoteData } from '../useBridgeQuoteData';
+import { ChainId } from '@metamask/bridge-controller';
import { BigNumber } from 'ethers';
// Mock dependencies
@@ -250,6 +251,9 @@ describe('useHasSufficientGas', () => {
gasFee: {
effective: { amount: '0.001' }, // 0.001 SOL
},
+ totalNetworkFee: {
+ amount: '0.02',
+ },
} as unknown as ReturnType['activeQuote'];
// User has 0.01 SOL
@@ -277,6 +281,9 @@ describe('useHasSufficientGas', () => {
gasFee: {
effective: { amount: '0.01' }, // 0.01 SOL
},
+ totalNetworkFee: {
+ amount: '0.01',
+ },
} as unknown as ReturnType['activeQuote'];
// User has 0.001 SOL
@@ -293,6 +300,35 @@ describe('useHasSufficientGas', () => {
expect(result.current).toBe(false);
});
});
+
+ describe('for Bitcoin', () => {
+ it('uses totalNetworkFee to validate BTC gas balance', () => {
+ const mockQuote: ReturnType['activeQuote'] =
+ {
+ quote: {
+ srcChainId: ChainId.BTC,
+ },
+ gasFee: {
+ effective: { amount: '0' },
+ },
+ totalNetworkFee: {
+ amount: '0.0001',
+ },
+ } as unknown as ReturnType['activeQuote'];
+
+ mockUseLatestBalance.mockReturnValue({
+ displayBalance: '0.001',
+ atomicBalance: BigNumber.from('100000'), // 0.001 BTC in sats
+ });
+
+ const { result } = renderHookWithProvider(
+ () => useHasSufficientGas({ quote: mockQuote }),
+ { state: {} },
+ );
+
+ expect(result.current).toBe(true);
+ });
+ });
});
describe('when quote is undefined', () => {
diff --git a/app/components/UI/Bridge/hooks/useIsNetworkFeeUnavailable/index.test.ts b/app/components/UI/Bridge/hooks/useIsNetworkFeeUnavailable/index.test.ts
new file mode 100644
index 000000000000..593dae84f751
--- /dev/null
+++ b/app/components/UI/Bridge/hooks/useIsNetworkFeeUnavailable/index.test.ts
@@ -0,0 +1,99 @@
+import { ChainId } from '@metamask/bridge-controller';
+import { mockQuoteWithMetadata } from '../../_mocks_/bridgeQuoteWithMetadata';
+import { isQuoteNetworkFeeUnavailable } from '.';
+import { useBridgeQuoteData } from '../useBridgeQuoteData';
+
+type ActiveQuote = ReturnType['activeQuote'];
+
+const createQuote = (
+ overrides: Partial> = {},
+): ActiveQuote =>
+ ({
+ ...mockQuoteWithMetadata,
+ ...overrides,
+ quote: {
+ ...mockQuoteWithMetadata.quote,
+ srcChainId: ChainId.BTC,
+ ...overrides.quote,
+ },
+ totalNetworkFee: {
+ ...mockQuoteWithMetadata.totalNetworkFee,
+ amount: '0.0001',
+ ...overrides.totalNetworkFee,
+ },
+ }) as ActiveQuote;
+
+describe('isQuoteNetworkFeeUnavailable', () => {
+ it('returns true for a BTC quote with zero network fee', () => {
+ expect(
+ isQuoteNetworkFeeUnavailable(
+ createQuote({
+ totalNetworkFee: {
+ ...mockQuoteWithMetadata.totalNetworkFee,
+ amount: '0',
+ },
+ }),
+ ),
+ ).toBe(true);
+ });
+
+ it('returns true for a BTC quote with negative network fee', () => {
+ expect(
+ isQuoteNetworkFeeUnavailable(
+ createQuote({
+ totalNetworkFee: {
+ ...mockQuoteWithMetadata.totalNetworkFee,
+ amount: '-0.0001',
+ },
+ }),
+ ),
+ ).toBe(true);
+ });
+
+ it('returns true for a BTC quote with missing network fee amount', () => {
+ expect(
+ isQuoteNetworkFeeUnavailable(
+ createQuote({
+ totalNetworkFee: {
+ ...mockQuoteWithMetadata.totalNetworkFee,
+ amount: undefined,
+ } as unknown as NonNullable['totalNetworkFee'],
+ }),
+ ),
+ ).toBe(true);
+ });
+
+ it('returns false for a BTC quote with positive network fee', () => {
+ expect(isQuoteNetworkFeeUnavailable(createQuote())).toBe(false);
+ });
+
+ it('returns false for a non-BTC quote with zero network fee', () => {
+ expect(
+ isQuoteNetworkFeeUnavailable(
+ createQuote({
+ quote: {
+ ...mockQuoteWithMetadata.quote,
+ srcChainId: 1,
+ },
+ totalNetworkFee: {
+ ...mockQuoteWithMetadata.totalNetworkFee,
+ amount: '0',
+ },
+ }),
+ ),
+ ).toBe(false);
+ });
+
+ it('returns false when the quote has no source chain', () => {
+ expect(
+ isQuoteNetworkFeeUnavailable(
+ createQuote({
+ quote: {
+ ...mockQuoteWithMetadata.quote,
+ srcChainId: undefined,
+ } as unknown as NonNullable['quote'],
+ }),
+ ),
+ ).toBe(false);
+ });
+});
diff --git a/app/components/UI/Bridge/hooks/useIsNetworkFeeUnavailable/index.ts b/app/components/UI/Bridge/hooks/useIsNetworkFeeUnavailable/index.ts
new file mode 100644
index 000000000000..6d614b8982f9
--- /dev/null
+++ b/app/components/UI/Bridge/hooks/useIsNetworkFeeUnavailable/index.ts
@@ -0,0 +1,27 @@
+import { useMemo } from 'react';
+import { BigNumber } from 'bignumber.js';
+import { isBitcoinChainId } from '@metamask/bridge-controller';
+import { useBridgeQuoteData } from '../useBridgeQuoteData';
+
+type ActiveQuote = ReturnType['activeQuote'];
+
+export const isQuoteNetworkFeeUnavailable = (
+ activeQuote: ActiveQuote,
+): boolean => {
+ const sourceChainId = activeQuote?.quote?.srcChainId;
+
+ if (!sourceChainId || !isBitcoinChainId(sourceChainId)) {
+ return false;
+ }
+
+ const networkFeeAmount = activeQuote.totalNetworkFee?.amount;
+ const networkFee =
+ networkFeeAmount == null ? undefined : new BigNumber(networkFeeAmount);
+
+ return (
+ networkFeeAmount == null || !networkFee?.isFinite() || networkFee.lte(0)
+ );
+};
+
+export const useIsNetworkFeeUnavailable = (activeQuote: ActiveQuote): boolean =>
+ useMemo(() => isQuoteNetworkFeeUnavailable(activeQuote), [activeQuote]);
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuyBottomSheet/useQuickBuyBottomSheet.test.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuyBottomSheet/useQuickBuyBottomSheet.test.ts
index 1b0d36a07c6e..5ef9cc8e5d2f 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuyBottomSheet/useQuickBuyBottomSheet.test.ts
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuyBottomSheet/useQuickBuyBottomSheet.test.ts
@@ -25,6 +25,7 @@ import { selectSelectedInternalAccountFormattedAddress } from '../../../../../..
import { usePriceImpactViewData } from '../../../../../UI/Bridge/hooks/usePriceImpactViewData';
import { TextColor } from '@metamask/design-system-react-native';
import type { BridgeToken } from '../../../../../UI/Bridge/types';
+import { ChainId } from '@metamask/bridge-controller';
jest.mock('react-redux', () => ({
useSelector: jest.fn(),
@@ -179,9 +180,16 @@ const createSourceToken = (overrides: Partial = {}): BridgeToken =>
...overrides,
}) as BridgeToken;
-const createActiveQuote = () => ({
+const createActiveQuote = (overrides: Record = {}) => ({
+ ...overrides,
quote: {
srcTokenAmount: '10000000000000000',
+ ...((overrides.quote as Record | undefined) ?? {}),
+ },
+ totalNetworkFee: {
+ amount: '0.0001',
+ ...((overrides.totalNetworkFee as Record | undefined) ??
+ {}),
},
});
@@ -355,6 +363,36 @@ describe('useQuickBuyBottomSheet', () => {
(useHasSufficientGas as jest.Mock).mockReturnValue(true);
});
+
+ it('returns the insufficient-funds label when BTC network fee is unavailable', () => {
+ const activeQuote = createActiveQuote({
+ quote: {
+ srcChainId: ChainId.BTC,
+ },
+ totalNetworkFee: {
+ amount: '0',
+ },
+ });
+
+ (useQuickBuyQuotes as jest.Mock).mockReturnValue({
+ activeQuote,
+ destTokenAmount: '1',
+ isQuoteLoading: false,
+ isNoQuotesAvailable: false,
+ quoteFetchError: null,
+ isActiveQuoteForCurrentTokenPair: true,
+ });
+
+ const { result } = renderHook(() =>
+ useQuickBuyBottomSheet(createPosition(), jest.fn()),
+ );
+
+ expect(result.current.buttonError).toBe('insufficient_balance');
+ expect(result.current.getButtonLabel()).toBe('bridge.insufficient_funds');
+ expect(useHasSufficientGas).toHaveBeenCalledWith({
+ quote: activeQuote,
+ });
+ });
});
describe('quoteOverride wiring', () => {
diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuyBottomSheet/useQuickBuyBottomSheet.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuyBottomSheet/useQuickBuyBottomSheet.ts
index 9e3db9272d77..79a55e7c453b 100644
--- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuyBottomSheet/useQuickBuyBottomSheet.ts
+++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuyBottomSheet/useQuickBuyBottomSheet.ts
@@ -37,6 +37,7 @@ import {
import { useLatestBalance } from '../../../../../UI/Bridge/hooks/useLatestBalance';
import useIsInsufficientBalance from '../../../../../UI/Bridge/hooks/useInsufficientBalance';
import { useHasSufficientGas } from '../../../../../UI/Bridge/hooks/useHasSufficientGas';
+import { useIsNetworkFeeUnavailable } from '../../../../../UI/Bridge/hooks/useIsNetworkFeeUnavailable';
import { useInitialSlippage } from '../../../../../UI/Bridge/hooks/useInitialSlippage';
import { usePriceImpactViewData } from '../../../../../UI/Bridge/hooks/usePriceImpactViewData';
import {
@@ -303,7 +304,10 @@ export function useQuickBuyBottomSheet(
quoteOverride: activeQuote ?? null,
});
+ const isNetworkFeeUnavailable = useIsNetworkFeeUnavailable(activeQuote);
const hasSufficientGas = useHasSufficientGas({ quote: activeQuote });
+ const hasInsufficientGas =
+ !isNetworkFeeUnavailable && hasSufficientGas === false;
const stxEnabled = useSelector(selectShouldUseSmartTransaction);
const hasDestinationPicker = isEvmNonEvmBridge || isNonEvmNonEvmBridge;
@@ -430,7 +434,8 @@ export function useQuickBuyBottomSheet(
isPendingQuoteRefresh ||
isQuoteLoading ||
hasInsufficientBalance ||
- hasSufficientGas === false ||
+ isNetworkFeeUnavailable ||
+ hasInsufficientGas ||
isSubmittingTx ||
hasError ||
isHardwareSolanaBlocked ||
@@ -442,16 +447,21 @@ export function useQuickBuyBottomSheet(
const isConfirmLoading = isSubmittingTx;
- const buttonError: QuickBuyButtonError | null = hasInsufficientBalance
- ? 'insufficient_balance'
- : hasSufficientGas === false
- ? 'insufficient_gas'
- : hasError
- ? 'no_quotes'
- : null;
-
- const confirmButtonState: 'idle' | 'loading' | 'success' =
- txPhase === 'success' ? 'success' : isConfirmLoading ? 'loading' : 'idle';
+ let buttonError: QuickBuyButtonError | null = null;
+ if (hasInsufficientBalance || isNetworkFeeUnavailable) {
+ buttonError = 'insufficient_balance';
+ } else if (hasInsufficientGas) {
+ buttonError = 'insufficient_gas';
+ } else if (hasError) {
+ buttonError = 'no_quotes';
+ }
+
+ let confirmButtonState: 'idle' | 'loading' | 'success' = 'idle';
+ if (txPhase === 'success') {
+ confirmButtonState = 'success';
+ } else if (isConfirmLoading) {
+ confirmButtonState = 'loading';
+ }
const getButtonLabel = useCallback(() => {
if (buttonError) return strings(BUTTON_ERROR_LABELS[buttonError]);
From 0fccfd357b2a5a0837391f45911692948cb2e595 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Thu, 21 May 2026 09:03:40 +0000
Subject: [PATCH 45/66] [skip ci] Bump version number to 5120
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index bc5769aa7fcc..d9ad93a69534 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5119
+ versionCode 5120
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 3a71a27adb17..42b08ccfdbb7 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5119
+ VERSION_NUMBER: 5120
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5119
+ FLASK_VERSION_NUMBER: 5120
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 1bb343517623..0bc6afc7282b 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5119;
+ CURRENT_PROJECT_VERSION = 5120;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5119;
+ CURRENT_PROJECT_VERSION = 5120;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5119;
+ CURRENT_PROJECT_VERSION = 5120;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5119;
+ CURRENT_PROJECT_VERSION = 5120;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From b349be49e2bf8d281808fdb00a521f7a0d4a636d Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 14:51:28 +0200
Subject: [PATCH 46/66] chore(runway): cherry-pick fix(analytics): normalise
Segment proxy URL to fix invalid-URL error in 2.23.0 cp-7.78.0 (#30496)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- fix(analytics): normalise Segment proxy URL to fix invalid-URL error
in 2.23.0 cp-7.78.0 (#30463)
## Summary
`@segment/analytics-react-native` 2.23.0 introduced a strict
`validateURL` regex via [PR
#1157](https://github.com/segmentio/analytics-react-native/pull/1157)
that only allows `[a-zA-Z0-9_.-]` in query-param values. The MetaMask
Segment proxy URL encodes the write key as standard base64 in a query
param (`?b===`), and the trailing `=` padding characters are
rejected by this regex.
When the URL fails validation `SegmentDestination.getEndpoint()`
silently falls back to `https://api.segment.io/v1/b`. Events reach
Segment's default endpoint but the proxy write key is only valid through
`fn.segmentapis.com`, so they are rejected — causing **no events to
appear in Mixpanel**.
## Change
Added `normalizeProxyUrl` in `platform-adapter.ts` that strips trailing
`=` padding from query-param values before passing the URL to the
Segment client config.
- Stripping base64 padding is safe: decoders infer it from data length
and the proxy server accepts both forms.
- The `=` key–value separator is preserved (the regex uses a lookahead
`(?=&|$)` to match only padding at the end of a param value).
- Contains a `TODO` to remove once upstream fixes the regex to accept
all RFC 3986 query characters.
## Test plan
- [ ] Run the app in dev mode and verify analytics events appear in
Mixpanel
- [ ] Verify no "Invalid URL has been passed" errors in the console
- [ ] Run unit tests: `yarn jest platform-adapter`
Made with [Cursor](https://cursor.com)
---
> [!NOTE]
> **Medium Risk**
> Moderate risk because it changes how the Segment client is configured
and could affect where analytics events are sent if the proxy URL is
altered incorrectly, though the change is narrowly scoped and covered by
unit tests.
>
> **Overview**
> Fixes Segment proxy URL validation failures by normalizing
`SEGMENT_PROXY_URL` before passing it to `createClient`, stripping
trailing base64 `=` padding from query-param values (while preserving
key/value separators).
>
> Adds focused unit coverage for `normalizeProxyUrl` across common URL
shapes (single/double padding, multi-param URLs) and a wiring test to
ensure `createPlatformAdapter` passes the normalized `proxy` value into
Segment client configuration.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ee00a416e2520dc6bd1b356eed866bf85ad65d80. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---------
Co-authored-by: João Santos
[66f0cb2](https://github.com/MetaMask/metamask-mobile/commit/66f0cb2fedec81d18fba4db5750e3b590dfdf26d)
Co-authored-by: tommasini <46944231+tommasini@users.noreply.github.com>
Co-authored-by: João Santos
---
.../platform-adapter.test.ts | 162 +++++++++++++++++-
.../analytics-controller/platform-adapter.ts | 23 ++-
2 files changed, 183 insertions(+), 2 deletions(-)
diff --git a/app/core/Engine/controllers/analytics-controller/platform-adapter.test.ts b/app/core/Engine/controllers/analytics-controller/platform-adapter.test.ts
index c853fba6c657..bc7bc10d9b4d 100644
--- a/app/core/Engine/controllers/analytics-controller/platform-adapter.test.ts
+++ b/app/core/Engine/controllers/analytics-controller/platform-adapter.test.ts
@@ -1,5 +1,6 @@
-import { createPlatformAdapter } from './platform-adapter';
+import { createPlatformAdapter, normalizeProxyUrl } from './platform-adapter';
import {
+ createClient,
type SegmentClient,
DestinationPlugin,
} from '@segment/analytics-react-native';
@@ -38,6 +39,145 @@ interface GlobalWithSegmentClient {
segmentMockClient: SegmentClient;
}
+const mockCreateClient = createClient as jest.MockedFunction<
+ typeof createClient
+>;
+
+// Realistic proxy URL shapes used by MetaMask (base64 write-key in query params).
+// The actual values are redacted in .js.env; the structural pattern is:
+// https://fn.segmentapis.com/v1/b?b=[=|==]
+// DEV keys typically produce single `=` padding; PROD keys often produce `==`.
+const DEV_PROXY_URL =
+ 'https://fn.segmentapis.com/v1/b?b=dGVzdC1kZXYta2V5MTIzNA==';
+const PROD_PROXY_URL =
+ 'https://fn.segmentapis.com/v1/b?b=dGVzdC1wcm9kLWtleUFCQ0Q=';
+const MULTI_PARAM_URL =
+ 'https://fn.segmentapis.com/v1/b?region=us-west&b=dGVzdC1rZXkxMjM=&v=2';
+
+describe('normalizeProxyUrl', () => {
+ it('returns undefined when url is undefined', () => {
+ const result = normalizeProxyUrl(undefined);
+
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined when url is an empty string', () => {
+ const result = normalizeProxyUrl('');
+
+ expect(result).toBeUndefined();
+ });
+
+ it('returns the URL unchanged when there is no base64 padding', () => {
+ const url = 'https://fn.segmentapis.com/v1/b?b=dGVzdGtleQ';
+
+ const result = normalizeProxyUrl(url);
+
+ expect(result).toBe('https://fn.segmentapis.com/v1/b?b=dGVzdGtleQ');
+ });
+
+ it('strips a single trailing = from the last query param', () => {
+ const result = normalizeProxyUrl(
+ 'https://fn.segmentapis.com/v1/b?b=dGVzdGtleQ=',
+ );
+
+ expect(result).toBe('https://fn.segmentapis.com/v1/b?b=dGVzdGtleQ');
+ });
+
+ it('strips double trailing == from the last query param', () => {
+ const result = normalizeProxyUrl(
+ 'https://fn.segmentapis.com/v1/b?b=dGVzdGtleQ==',
+ );
+
+ expect(result).toBe('https://fn.segmentapis.com/v1/b?b=dGVzdGtleQ');
+ });
+
+ it('strips trailing = padding from a mid-URL query param followed by another param', () => {
+ const result = normalizeProxyUrl(
+ 'https://fn.segmentapis.com/v1/b?b=dGVzdGtleQ==&v=2',
+ );
+
+ expect(result).toBe('https://fn.segmentapis.com/v1/b?b=dGVzdGtleQ&v=2');
+ });
+
+ it('preserves = key-value separators in query params', () => {
+ const result = normalizeProxyUrl(
+ 'https://fn.segmentapis.com/v1/b?region=us-west&b=dGVzdGtleQ==',
+ );
+
+ expect(result).toBe(
+ 'https://fn.segmentapis.com/v1/b?region=us-west&b=dGVzdGtleQ',
+ );
+ });
+
+ it('strips padding from multiple params that each carry base64 values', () => {
+ const result = normalizeProxyUrl(
+ 'https://fn.segmentapis.com/v1/b?a=dGVzdA==&b=dGVzdGtleQ=',
+ );
+
+ expect(result).toBe(
+ 'https://fn.segmentapis.com/v1/b?a=dGVzdA&b=dGVzdGtleQ',
+ );
+ });
+
+ it('returns the URL unchanged when it has no query string', () => {
+ const url = 'https://fn.segmentapis.com/v1/b';
+
+ const result = normalizeProxyUrl(url);
+
+ expect(result).toBe('https://fn.segmentapis.com/v1/b');
+ });
+
+ it('normalises the dev-environment proxy URL (double == padding)', () => {
+ const result = normalizeProxyUrl(DEV_PROXY_URL);
+
+ expect(result).toBe(
+ 'https://fn.segmentapis.com/v1/b?b=dGVzdC1kZXYta2V5MTIzNA',
+ );
+ });
+
+ it('normalises the prod-environment proxy URL (single = padding)', () => {
+ const result = normalizeProxyUrl(PROD_PROXY_URL);
+
+ expect(result).toBe(
+ 'https://fn.segmentapis.com/v1/b?b=dGVzdC1wcm9kLWtleUFCQ0Q',
+ );
+ });
+
+ it('normalises a multi-param proxy URL preserving non-base64 params', () => {
+ const result = normalizeProxyUrl(MULTI_PARAM_URL);
+
+ expect(result).toBe(
+ 'https://fn.segmentapis.com/v1/b?region=us-west&b=dGVzdC1rZXkxMjM&v=2',
+ );
+ });
+
+ it('passes Segment validateURL regex after normalisation for dev URL', () => {
+ const result = normalizeProxyUrl(DEV_PROXY_URL) as string;
+
+ // The Segment regex allows only [a-zA-Z0-9_.-] in query-param values.
+ // After normalisation no `=` padding should remain in any param value.
+ const queryString = result.split('?')[1] ?? '';
+ const paramValues = queryString
+ .split('&')
+ .map((pair) => pair.split('=')[1]);
+ paramValues.forEach((value) => {
+ expect(value).toMatch(/^[a-zA-Z0-9_.-]+$/);
+ });
+ });
+
+ it('passes Segment validateURL regex after normalisation for prod URL', () => {
+ const result = normalizeProxyUrl(PROD_PROXY_URL) as string;
+
+ const queryString = result.split('?')[1] ?? '';
+ const paramValues = queryString
+ .split('&')
+ .map((pair) => pair.split('=')[1]);
+ paramValues.forEach((value) => {
+ expect(value).toMatch(/^[a-zA-Z0-9_.-]+$/);
+ });
+ });
+});
+
describe('createPlatformAdapter', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -169,4 +309,24 @@ describe('createPlatformAdapter', () => {
expect(adapter1).not.toBe(adapter2);
});
});
+
+ describe('proxy URL normalisation', () => {
+ // babel-plugin-transform-inline-environment-variables bakes process.env.*
+ // at compile time, so env vars cannot be mutated at test runtime. The
+ // SEGMENT_PROXY_URL value is therefore always undefined in the Jest
+ // environment, which means createClient receives an undefined proxy. The
+ // normaliseProxyUrl unit tests above already prove the function handles
+ // all URL shapes. Here we verify the wiring: createClient receives the
+ // output of normalizeProxyUrl, whatever that resolves to.
+ it('passes the output of normalizeProxyUrl as the proxy config to createClient', () => {
+ mockCreateClient.mockClear();
+
+ createPlatformAdapter();
+
+ expect(mockCreateClient).toHaveBeenCalledTimes(1);
+ const calledConfig = mockCreateClient.mock.calls[0][0];
+ // The proxy field is present in the config object (value depends on env).
+ expect(calledConfig).toHaveProperty('proxy');
+ });
+ });
});
diff --git a/app/core/Engine/controllers/analytics-controller/platform-adapter.ts b/app/core/Engine/controllers/analytics-controller/platform-adapter.ts
index a80eb48da0cf..f6135eae590c 100644
--- a/app/core/Engine/controllers/analytics-controller/platform-adapter.ts
+++ b/app/core/Engine/controllers/analytics-controller/platform-adapter.ts
@@ -15,10 +15,31 @@ import { segmentPersistor } from '../../../../util/analytics/SegmentPersistor';
import Logger from '../../../../util/Logger';
import MetaMetricsPrivacySegmentPlugin from '../../../../util/analytics/privacySegmentPlugin';
+/**
+ * Strips trailing `=` padding from every query-param value in a URL.
+ *
+ * @segment/analytics-react-native ≥2.23.0 introduced a strict `validateURL`
+ * regex that only allows `[a-zA-Z0-9_.-]` in query-param values, which rejects
+ * the standard base64 `=` padding characters present in the Segment proxy write
+ * key. Stripping the padding is safe – base64 decoders always infer it from the
+ * data length, and the proxy server accepts both forms.
+ *
+ * TODO: remove once upstream fixes the regex to accept all RFC 3986 query chars.
+ * See: https://github.com/segmentio/analytics-react-native/pull/1157
+ */
+export const normalizeProxyUrl = (
+ url: string | undefined,
+): string | undefined => {
+ if (!url) return undefined;
+ // Replace any run of `=` that is followed by `&` (next param) or end-of-string
+ // (end of query). This strips base64 padding without touching `=` separators.
+ return url.replace(/[=]+(?=&|$)/g, '');
+};
+
const getSegmentClient = (): SegmentClient => {
const config: Config = {
writeKey: process.env.SEGMENT_WRITE_KEY as string,
- proxy: process.env.SEGMENT_PROXY_URL as string,
+ proxy: normalizeProxyUrl(process.env.SEGMENT_PROXY_URL),
debug: __DEV__,
// Use custom persistor to bridge Segment SDK with app's storage system
storePersistor: segmentPersistor,
From 491e5027260d380b1ac61898c49f1eb5e1e87eb8 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 15:04:16 +0200
Subject: [PATCH 47/66] chore(runway): cherry-pick fix: Perps withdraw
back-swipe toast cp-7.78.0 (#30516)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- fix: Perps withdraw back-swipe toast cp-7.78.0 (#30504)
## Summary
Fixes the Perps withdraw confirmation flow so user-initiated
confirmation rejection, including iOS back-swipe, does not show the
retryable "withdrawal wasn't started" error toast.
## Root Cause
PR #30299 added an `addTransactionBatch` catch path that navigates back
and shows the withdrawal-start failure toast for every rejected batch
initialization. Back-swiping the confirmation rejects the approval with
a user-rejected error, so that normal cancellation path was being
treated as a real initialization failure.
## Changes
- Detect user-rejected errors before showing the withdrawal-start
failure toast.
- Keep the existing retry toast behavior for real `addTransactionBatch`
failures.
- Add regression coverage for user rejection so it does not call
`goBack`, build the retry toast, or show the toast.
Fixes #30485.
---
> [!NOTE]
> **Low Risk**
> Low risk: scoped to Perps withdraw error handling and adds a
regression test; behavior only changes for user-rejected/cancel paths.
>
> **Overview**
> Prevents Perps withdraw confirmation cancellations (user-rejected
errors) from being treated as `addTransactionBatch` failures: on
rejection, the hook now rethrows without calling `navigation.goBack()`
or showing the retryable “withdrawalStartFailed” toast.
>
> Adds a regression test covering a
`providerErrors.userRejectedRequest()` rejection to ensure no back
navigation or toast is triggered, while keeping the existing retry-toast
behavior for real batch initialization failures.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c23f2c36aa0469447b4cce532c4c871160c9be07. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[b76275a](https://github.com/MetaMask/metamask-mobile/commit/b76275a34177bbe6cfcb0885498e199ca3aa3548)
Co-authored-by: Pedro Figueiredo
---
.../usePerpsWithdrawConfirmation.test.ts | 18 ++++++++
.../hooks/usePerpsWithdrawConfirmation.ts | 43 +++++++++++++++++++
2 files changed, 61 insertions(+)
diff --git a/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.test.ts b/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.test.ts
index d15f5762a5b1..9daf60ed8a1e 100644
--- a/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.test.ts
+++ b/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.test.ts
@@ -2,6 +2,7 @@ import { ORIGIN_METAMASK } from '@metamask/controller-utils';
import { useNavigation } from '@react-navigation/native';
import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller';
import { Hex } from '@metamask/utils';
+import { providerErrors } from '@metamask/rpc-errors';
import { renderHook, act, waitFor } from '@testing-library/react-native';
import { useSelector } from 'react-redux';
import { selectSelectedInternalAccountAddress } from '../../../../selectors/accountsController';
@@ -193,6 +194,23 @@ describe('usePerpsWithdrawConfirmation', () => {
expect(mockShowToast).toHaveBeenCalledWith(mockWithdrawalStartFailedToast);
});
+ it('does not show the start failure toast when the user rejects the confirmation', async () => {
+ const error = providerErrors.userRejectedRequest();
+ mockAddTransactionBatch.mockRejectedValueOnce(error);
+
+ const { result } = renderHook(() => usePerpsWithdrawConfirmation());
+
+ await expect(
+ act(async () => {
+ await result.current.withdrawWithConfirmation();
+ }),
+ ).rejects.toThrow(error.message);
+
+ expect(mockGoBack).not.toHaveBeenCalled();
+ expect(mockWithdrawalStartFailed).not.toHaveBeenCalled();
+ expect(mockShowToast).not.toHaveBeenCalled();
+ });
+
it('swallows retry failures after showing another retryable error toast', async () => {
mockAddTransactionBatch
.mockRejectedValueOnce(new Error('batch failed'))
diff --git a/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.ts b/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.ts
index 4f746df4beb9..77d313d60d45 100644
--- a/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.ts
+++ b/app/components/UI/Perps/hooks/usePerpsWithdrawConfirmation.ts
@@ -14,8 +14,47 @@ import { ARBITRUM_USDC } from '../../../Views/confirmations/constants/perps';
import { RootState } from '../../../../reducers';
import Routes from '../../../../constants/navigation/Routes';
import { ensureError } from '../../../../util/errorUtils';
+import { containsUserRejectedError } from '../../../../util/middlewares';
import usePerpsToasts from './usePerpsToasts';
+interface ErrorLike {
+ code?: unknown;
+ message?: unknown;
+}
+
+function getErrorLike(error: unknown): ErrorLike | undefined {
+ return typeof error === 'object' && error !== null
+ ? (error as ErrorLike)
+ : undefined;
+}
+
+function getErrorCode(error: unknown): number | undefined {
+ const code = getErrorLike(error)?.code;
+
+ if (typeof code === 'number') {
+ return code;
+ }
+
+ if (typeof code === 'string') {
+ const numericCode = Number(code);
+ return Number.isNaN(numericCode) ? undefined : numericCode;
+ }
+
+ return undefined;
+}
+
+function getErrorMessage(error: unknown, fallbackMessage: string): string {
+ const message = getErrorLike(error)?.message;
+ return typeof message === 'string' ? message : fallbackMessage;
+}
+
+function isUserRejectedError(error: unknown, fallbackMessage: string): boolean {
+ return containsUserRejectedError(
+ getErrorMessage(error, fallbackMessage),
+ getErrorCode(error),
+ );
+}
+
/**
* Hook that triggers the Perps "withdraw to any token" confirmation flow.
*
@@ -69,6 +108,10 @@ export function usePerpsWithdrawConfirmation() {
'usePerpsWithdrawConfirmation.withdrawWithConfirmation',
);
+ if (isUserRejectedError(error, errorObj.message)) {
+ throw errorObj;
+ }
+
navigation.goBack();
showToast(
PerpsToastOptions.accountManagement.withdrawal.withdrawalStartFailed(
From 5ee3434b188910cd549dc66a6fcc96b014dd3840 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Thu, 21 May 2026 13:33:37 +0000
Subject: [PATCH 48/66] [skip ci] Bump version number to 5129
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index d9ad93a69534..c72e9bc73eb5 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5120
+ versionCode 5129
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 42b08ccfdbb7..0a774d4162bf 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5120
+ VERSION_NUMBER: 5129
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5120
+ FLASK_VERSION_NUMBER: 5129
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 0bc6afc7282b..24d8a7234793 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5120;
+ CURRENT_PROJECT_VERSION = 5129;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5120;
+ CURRENT_PROJECT_VERSION = 5129;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5120;
+ CURRENT_PROJECT_VERSION = 5129;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5120;
+ CURRENT_PROJECT_VERSION = 5129;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From d74fbd956b2b933fe6c29269a6a694742a492ab9 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 16:03:38 +0200
Subject: [PATCH 49/66] chore(runway): cherry-pick fix(perps): apply safe area
top inset directly to TPSL header cp-7.78.0 (#30515)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- fix(perps): apply safe area top inset directly to TPSL header
cp-7.78.0 (#30503)
## **Description**
The TPSL (Take Profit / Stop Loss) page header was intermittently
positioned too high, overlapping the status bar / notch area. This
occurred both when creating a TPSL from the order screen and when
editing a TPSL for an open position.
**Root cause:** The screen relied on the shimmed `SafeAreaView` with
`edges={['top', 'bottom']}` to apply the top safe area inset. The shim
(`SafeAreaViewWithHookTopInset`) turns off the native top inset and
re-applies it via a hook-based `paddingTop`. With the `transparentModal`
presentation mode used by this screen, the hook-based top inset was
intermittently not applied, causing the header to render too high.
**Fix:** Follow the same proven pattern used by `PerpsOrderHeader` —
apply the top inset directly to the header view using
`useSafeAreaInsets()`, and only use `SafeAreaView` for the bottom edge.
This is deterministic and does not depend on the shimmed SafeAreaView
lifecycle for transparent modals.
## **Changelog**
CHANGELOG entry: Fixed TPSL page header overlapping the status bar area
## **Related issues**
Fixes: TAT-3213
## **Manual testing steps**
```gherkin
Feature: TPSL page header alignment
Scenario: user creates a TPSL from the order screen
Given the user is on the Perps order screen with an asset selected
When user taps the TPSL / Auto close button
Then the TPSL page opens with the header properly below the status bar / notch area
Scenario: user edits a TPSL for an open position
Given the user has an open Perps position with or without existing TP/SL values
When user taps the Auto close / Edit TPSL button on the position
Then the TPSL page opens with the header properly below the status bar / notch area
Scenario: user repeatedly opens and closes the TPSL page
Given the user is on the Perps order screen
When user opens and closes the TPSL page multiple times
Then the header is consistently aligned below the safe area every time
```
## **Screenshots/Recordings**
### **Before**
https://consensys.slack.com/archives/C092T3GPHQD/p1779353500168619
### **After**
Header is consistently positioned below the safe area.
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Low risk UI layout change limited to safe-area handling in the Perps
TPSL screen; no business logic or data flow changes.
>
> **Overview**
> Fixes an intermittent layout issue where the Perps TPSL header could
render under the status bar/notch.
>
> `PerpsTPSLView` now uses `useSafeAreaInsets()` to add top padding
directly on the header, and updates `SafeAreaView` to only apply the
bottom safe area (`edges={['bottom']}`) so top inset behavior is
deterministic in modal presentation.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f3dd563a409ac90ae220955bc0e08fc155b21c58. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[1635a36](https://github.com/MetaMask/metamask-mobile/commit/1635a36869baaa8a62b76c14df505fa017215362)
Co-authored-by: Michal Szorad
---
.../Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx
index c6d5d1fbac78..6909e34635ce 100644
--- a/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx
+++ b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx
@@ -6,7 +6,10 @@ import {
TouchableOpacity,
View,
} from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
+import {
+ SafeAreaView,
+ useSafeAreaInsets,
+} from 'react-native-safe-area-context';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { strings } from '../../../../../../locales/i18n';
import {
@@ -75,6 +78,7 @@ const PerpsTPSLView: React.FC = () => {
const [isUpdating, setIsUpdating] = useState(false);
const { colors } = useTheme();
const styles = createStyles(colors);
+ const { top: topInset } = useSafeAreaInsets();
const scrollViewRef = useRef(null);
@@ -447,11 +451,16 @@ const PerpsTPSLView: React.FC = () => {
return (
{/* Simple header with back button and title */}
-
+ 0 ? { paddingTop: 16 + topInset } : undefined,
+ ]}
+ >
Date: Thu, 21 May 2026 14:05:42 +0000
Subject: [PATCH 50/66] [skip ci] Bump version number to 5130
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index c72e9bc73eb5..1c27cc20e315 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5129
+ versionCode 5130
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 0a774d4162bf..deaef039ac2b 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5129
+ VERSION_NUMBER: 5130
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5129
+ FLASK_VERSION_NUMBER: 5130
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 24d8a7234793..3c3dc6bd1b2b 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5129;
+ CURRENT_PROJECT_VERSION = 5130;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5129;
+ CURRENT_PROJECT_VERSION = 5130;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5129;
+ CURRENT_PROJECT_VERSION = 5130;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5129;
+ CURRENT_PROJECT_VERSION = 5130;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 242ade0b8c3ef155755c8443216810a953da19ed Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 16:39:32 +0200
Subject: [PATCH 51/66] chore(runway): cherry-pick fix: add BTC swap reserve
enforcement with quote-aware fee handling (#30526)
- fix: add BTC swap reserve enforcement with quote-aware fee handling
(#30404)
## **Description**
This pull request enhances the `useInsufficientNativeReserveError` hook
to support Bitcoin (BTC) native reserve logic, ensuring accurate
handling of minimum reserve requirements and quote-aware calculations
for BTC swaps. It also improves test coverage for these scenarios and
refactors related code for better clarity and extensibility.
**BTC Native Reserve Support and Logic Improvements:**
* Added BTC mainnet support to the minimum native reserve calculation,
introducing a specific reserve threshold for BTC and handling BTC chain
IDs using CAIP format. The hook now accounts for BTC-specific
requirements when determining if a swap would violate the minimum
reserve.
**Testing Enhancements:**
* Added comprehensive test cases for BTC reserve logic, including
scenarios for reserve breaches, quote-aware calculations, and edge cases
where reserve or fees restrict swaps. Introduced BTC token and quote
mocks for robust test coverage.
**Refactoring and Type Improvements:**
* Refactored types and utility functions to support both EVM and non-EVM
(CAIP) chain IDs, improving maintainability and future extensibility.
**Integration Updates:**
* Updated `BridgeViewContent` and `SwapsConfirmButton` components to
pass the `activeQuote` prop to the hook, enabling quote-aware reserve
checks in the UI flow.
## **Changelog**
CHANGELOG entry: added BTC swap reserve enforcement with quote-aware fee
handling
## **Related issues**
Fixes: Bridging BTC is failing at the transaction crafting in certain
cases
## **Manual testing steps**
BTC max amount / reserve validation
1. Use a BTC account with a small balance.
2. Select BTC as source and ETH as destination.
3. Enter an amount that would leave less than 0.00003 BTC after the
swap.
4. Confirm the warning banner says Minimum BTC reserve balance is
required.
5. Confirm the message displays a reserve of 0.00003 BTC.
6. Confirm the CTA shows Insufficient funds.
7. Click Use max allowed.
8. Confirm the input is reduced to the max amount shown in the banner.
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Medium Risk**
> Updates swap/bridge validation for BTC to enforce a minimum native
reserve using quote-provided network fees/overhead, which can block
transactions if miscomputed. Changes are localized and covered by new
unit tests, but affect user-facing amount limits and submit eligibility.
>
> **Overview**
> **Adds BTC native reserve enforcement to swap/bridge submit
validation.** `useInsufficientNativeReserveError` now supports CAIP
chain IDs, defines a BTC mainnet minimum reserve (`0.00003`), and for
BTC uses `activeQuote` to subtract quoted network fees and source-side
overhead when computing the max swappable amount.
>
> **Integrates quote-aware checks in the UI flow.** `BridgeView` and
`SwapsConfirmButton` now pass `activeQuote` into the hook so
banners/CTAs and the confirm button correctly reflect BTC reserve + fee
constraints.
>
> **Expands test coverage.** Adds BTC-specific unit tests covering
reserve boundary conditions, quote fee consumption, decimal truncation,
and cases where gas validation should own the failure.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
da0093c0c0106683d74b1bb957cb273553cb3557. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[bc03176](https://github.com/MetaMask/metamask-mobile/commit/bc031760c042368dd16ce37e8d5248e779c8e00c)
Co-authored-by: Fred
---
.../UI/Bridge/Views/BridgeView/index.tsx | 1 +
.../components/SwapsConfirmButton/index.tsx | 21 +--
.../index.ts | 144 +++++++++++++----
...useInsufficientNativeReserveError.test.tsx | 148 +++++++++++++++++-
4 files changed, 271 insertions(+), 43 deletions(-)
diff --git a/app/components/UI/Bridge/Views/BridgeView/index.tsx b/app/components/UI/Bridge/Views/BridgeView/index.tsx
index 2cd3a591daee..33624110fca7 100644
--- a/app/components/UI/Bridge/Views/BridgeView/index.tsx
+++ b/app/components/UI/Bridge/Views/BridgeView/index.tsx
@@ -286,6 +286,7 @@ const BridgeViewContent = ({ latestSourceBalance }: BridgeViewContentProps) => {
token: sourceToken,
latestAtomicBalance: latestSourceBalance?.atomicBalance,
walletAddress,
+ activeQuote,
});
const isGasFeesSponsoredNetworkEnabled = useSelector(
diff --git a/app/components/UI/Bridge/components/SwapsConfirmButton/index.tsx b/app/components/UI/Bridge/components/SwapsConfirmButton/index.tsx
index 55b1b130ec75..5715d6637452 100644
--- a/app/components/UI/Bridge/components/SwapsConfirmButton/index.tsx
+++ b/app/components/UI/Bridge/components/SwapsConfirmButton/index.tsx
@@ -79,27 +79,28 @@ export const SwapsConfirmButton = ({
latestAtomicBalance: latestSourceBalance?.atomicBalance,
});
+ const {
+ activeQuote,
+ isLoading,
+ needsNewQuote,
+ blockaidError,
+ quoteFetchError,
+ isNoQuotesAvailable,
+ isActiveQuoteForCurrentTokenPair,
+ } = useBridgeQuoteDataContext();
+
const insufficientNativeReserveError = useInsufficientNativeReserveError({
amount: sourceAmount,
token: sourceToken,
latestAtomicBalance: latestSourceBalance?.atomicBalance,
walletAddress,
+ activeQuote,
});
const hasInsufficientNativeReserveError = Boolean(
insufficientNativeReserveError,
);
- const {
- activeQuote,
- isLoading,
- needsNewQuote,
- blockaidError,
- quoteFetchError,
- isNoQuotesAvailable,
- isActiveQuoteForCurrentTokenPair,
- } = useBridgeQuoteDataContext();
-
const handleConfirm = useBridgeConfirm({
activeQuote,
location,
diff --git a/app/components/UI/Bridge/hooks/useInsufficientNativeReserveError/index.ts b/app/components/UI/Bridge/hooks/useInsufficientNativeReserveError/index.ts
index df6780f63b14..c2408c64157c 100644
--- a/app/components/UI/Bridge/hooks/useInsufficientNativeReserveError/index.ts
+++ b/app/components/UI/Bridge/hooks/useInsufficientNativeReserveError/index.ts
@@ -1,9 +1,14 @@
import {
+ ChainId,
+ formatChainIdToCaip,
formatChainIdToHex,
+ isBitcoinChainId,
isNativeAddress,
isNonEvmChainId,
+ type QuoteMetadata,
+ type QuoteResponse,
} from '@metamask/bridge-controller';
-import { Hex } from '@metamask/utils';
+import type { CaipChainId, Hex } from '@metamask/utils';
import { BridgeToken } from '../../types';
import { useSelector } from 'react-redux';
import { getGasFeesSponsoredNetworkEnabled } from '../../../../../selectors/featureFlagController/gasFeesSponsored';
@@ -12,21 +17,42 @@ import { BigNumber as BigNumberJS } from 'bignumber.js';
import { formatUnits, parseUnits } from 'ethers/lib/utils';
import { isHardwareAccount } from '../../../../../util/address';
-const MINIMUM_NATIVE_RESERVE_BALANCE_PER_CHAIN: { [key: Hex]: string } = {
+type ChainIdHexOrCaip = Hex | CaipChainId;
+type ActiveQuote = (QuoteResponse & QuoteMetadata) | null | undefined;
+
+const BTC_MAINNET_CHAIN_ID = formatChainIdToCaip(ChainId.BTC);
+
+const MINIMUM_NATIVE_RESERVE_BALANCE_PER_CHAIN: {
+ [key in ChainIdHexOrCaip]?: string;
+} = {
'0x8f': '10',
+ [BTC_MAINNET_CHAIN_ID]: '0.00003',
};
const getMinimumReserveBalanceForTokenChainAndAddress = ({
- chainIdHex,
+ chainId,
tokenAddress,
}: {
- chainIdHex: Hex;
- tokenAddress: Hex;
+ chainId: ChainIdHexOrCaip;
+ tokenAddress: string;
}): string => {
if (!tokenAddress || !isNativeAddress(tokenAddress)) {
return '0';
}
- return MINIMUM_NATIVE_RESERVE_BALANCE_PER_CHAIN[chainIdHex] ?? '0';
+ return MINIMUM_NATIVE_RESERVE_BALANCE_PER_CHAIN[chainId] ?? '0';
+};
+
+const toBaseUnitBigNumber = (value: string, decimals: number) => {
+ const decimalValue = BigNumberJS(value);
+ if (!decimalValue.isFinite()) {
+ return undefined;
+ }
+
+ const tokenDecimalValue = decimalValue
+ .decimalPlaces(decimals, BigNumberJS.ROUND_DOWN)
+ .toFixed();
+
+ return BigNumberJS(parseUnits(tokenDecimalValue, decimals).toString());
};
interface UseInsufficientNativeReserveErrorParams {
@@ -34,21 +60,27 @@ interface UseInsufficientNativeReserveErrorParams {
token?: BridgeToken;
latestAtomicBalance?: BigNumber;
walletAddress?: string;
+ activeQuote?: ActiveQuote;
}
/**
- * Initially for gas-sponsored networks with a native reserve balance
- * logic, such as for Monad that needs 10 MON at all times.
+ * For networks with a native reserve balance requirement:
+ * - gas-sponsored EVM networks, such as Monad which needs 10 MON at all times
+ * - BTC mainnet swaps, which keep a buffer for network fees
*/
export const useInsufficientNativeReserveError = ({
amount,
token,
latestAtomicBalance,
walletAddress,
+ activeQuote,
}: UseInsufficientNativeReserveErrorParams) => {
const isGasFeesSponsoredNetworkEnabled = useSelector(
getGasFeesSponsoredNetworkEnabled,
);
+ const isBitcoinReserveChain = Boolean(
+ token?.chainId && isBitcoinChainId(token.chainId),
+ );
if (
!amount ||
@@ -56,36 +88,94 @@ export const useInsufficientNativeReserveError = ({
!token?.chainId ||
!latestAtomicBalance ||
!walletAddress ||
- isNonEvmChainId(token.chainId)
+ (isNonEvmChainId(token.chainId) && !isBitcoinReserveChain)
) {
return undefined;
}
+ const inputAmount = BigNumberJS(amount);
+ if (!inputAmount.isFinite()) {
+ return undefined;
+ }
+
const isHardwareWalletAccount = Boolean(
walletAddress && isHardwareAccount(walletAddress),
);
- const chainIdHex = formatChainIdToHex(token.chainId);
- const isNetworkGasSponsored = isNonEvmChainId(token.chainId)
- ? false
- : !isHardwareWalletAccount && isGasFeesSponsoredNetworkEnabled(chainIdHex);
+ const chainIdWithNativeReserve = isNonEvmChainId(token.chainId)
+ ? token.chainId
+ : formatChainIdToHex(token.chainId);
+ const chainIdHex = isNonEvmChainId(token.chainId)
+ ? undefined
+ : formatChainIdToHex(token.chainId);
+ const isNetworkGasSponsored = Boolean(
+ chainIdHex &&
+ !isHardwareWalletAccount &&
+ isGasFeesSponsoredNetworkEnabled(chainIdHex),
+ );
- const minimumNativeBalanceToBeKeptInAccount = isNetworkGasSponsored
- ? getMinimumReserveBalanceForTokenChainAndAddress({
- chainIdHex,
- tokenAddress: token.address as Hex,
- })
- : '0';
+ const minimumNativeBalanceToBeKeptInAccount =
+ isNetworkGasSponsored || isBitcoinReserveChain
+ ? getMinimumReserveBalanceForTokenChainAndAddress({
+ chainId: chainIdWithNativeReserve,
+ tokenAddress: token.address,
+ })
+ : '0';
- const maxSwappableNativeBalanceBaseUnits = BigNumberJS.max(
- new BigNumberJS(latestAtomicBalance.toString() ?? '0').minus(
- BigNumberJS(
- parseUnits(
+ const minimumNativeBalanceToBeKeptInAccountBaseUnits =
+ minimumNativeBalanceToBeKeptInAccount !== '0'
+ ? toBaseUnitBigNumber(
minimumNativeBalanceToBeKeptInAccount,
token.decimals,
- ).toString(),
- ),
- ),
+ )
+ : BigNumberJS(0);
+
+ if (!minimumNativeBalanceToBeKeptInAccountBaseUnits) {
+ return undefined;
+ }
+
+ let btcQuoteNetworkFeeBaseUnits = BigNumberJS(0);
+ let btcQuoteSourceOverheadBaseUnits = BigNumberJS(0);
+ if (isBitcoinReserveChain && activeQuote) {
+ const networkFeeAmount = activeQuote.totalNetworkFee?.amount;
+ const sentAmount = activeQuote.sentAmount?.amount;
+ const networkFeeBaseUnits = networkFeeAmount
+ ? toBaseUnitBigNumber(networkFeeAmount, token.decimals)
+ : undefined;
+ const sentAmountBaseUnits = sentAmount
+ ? toBaseUnitBigNumber(sentAmount, token.decimals)
+ : undefined;
+ const inputAmountBaseUnits = toBaseUnitBigNumber(amount, token.decimals);
+
+ if (
+ !networkFeeBaseUnits ||
+ !sentAmountBaseUnits ||
+ !inputAmountBaseUnits ||
+ networkFeeBaseUnits.lte(0)
+ ) {
+ return undefined;
+ }
+
+ if (
+ networkFeeBaseUnits
+ .plus(sentAmountBaseUnits)
+ .gte(BigNumberJS(latestAtomicBalance.toString()))
+ ) {
+ return undefined;
+ }
+
+ btcQuoteNetworkFeeBaseUnits = networkFeeBaseUnits;
+ btcQuoteSourceOverheadBaseUnits = BigNumberJS.max(
+ sentAmountBaseUnits.minus(inputAmountBaseUnits),
+ 0,
+ );
+ }
+
+ const maxSwappableNativeBalanceBaseUnits = BigNumberJS.max(
+ new BigNumberJS(latestAtomicBalance.toString() ?? '0')
+ .minus(minimumNativeBalanceToBeKeptInAccountBaseUnits)
+ .minus(btcQuoteNetworkFeeBaseUnits)
+ .minus(btcQuoteSourceOverheadBaseUnits),
0,
);
@@ -94,7 +184,7 @@ export const useInsufficientNativeReserveError = ({
);
return minimumNativeBalanceToBeKeptInAccount !== '0' &&
- maxSwappableNativeBalance.lt(amount)
+ maxSwappableNativeBalance.lt(inputAmount)
? {
minimumNativeBalanceToBeKeptInAccount,
maxSwappableNativeBalance: maxSwappableNativeBalance.toString(),
diff --git a/app/components/UI/Bridge/hooks/useInsufficientNativeReserveError/useInsufficientNativeReserveError.test.tsx b/app/components/UI/Bridge/hooks/useInsufficientNativeReserveError/useInsufficientNativeReserveError.test.tsx
index 8093ca6f33dd..46825819b208 100644
--- a/app/components/UI/Bridge/hooks/useInsufficientNativeReserveError/useInsufficientNativeReserveError.test.tsx
+++ b/app/components/UI/Bridge/hooks/useInsufficientNativeReserveError/useInsufficientNativeReserveError.test.tsx
@@ -6,10 +6,16 @@ import { legacy_createStore as createStore, Store } from 'redux';
import { BridgeToken } from '../../types';
import { BigNumber } from 'ethers';
import { CHAIN_IDS } from '@metamask/transaction-controller';
+import {
+ ChainId,
+ type QuoteMetadata,
+ type QuoteResponse,
+} from '@metamask/bridge-controller';
import { initialState } from '../../_mocks_/initialState';
import { useInsufficientNativeReserveError } from './index';
import { ZERO_ADDRESS } from '../../../../Views/confirmations/constants/address';
import { getGasFeesSponsoredNetworkEnabled } from '../../../../../selectors/featureFlagController/gasFeesSponsored';
+import { mockQuoteWithMetadata } from '../../_mocks_/bridgeQuoteWithMetadata';
jest.mock('../../../../../selectors/featureFlagController/gasFeesSponsored');
jest.mock('../../../../../util/address', () => ({
@@ -58,6 +64,13 @@ const ethTokenOnMainnet: BridgeToken = {
chainId: CHAIN_IDS.MAINNET as `0x${string}`,
};
+const btcTokenOnBitcoin: BridgeToken = {
+ address: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
+ symbol: 'BTC',
+ decimals: 8,
+ chainId: 'bip122:000000000019d6689c085ae165831e93',
+};
+
const nonEvmNativeTokens: BridgeToken[] = [
{
address: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
@@ -65,12 +78,6 @@ const nonEvmNativeTokens: BridgeToken[] = [
decimals: 9,
chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
},
- {
- address: 'bip122:000000000019d6689c085ae165831e93/slip44:0',
- symbol: 'BTC',
- decimals: 8,
- chainId: 'bip122:000000000019d6689c085ae165831e93',
- },
{
address: 'tron:728126428/slip44:195',
symbol: 'TRX',
@@ -88,6 +95,29 @@ function renderHookWithWrapper<
return renderHook(callback, { wrapper: wrapper(createMockStore()) });
}
+const createBtcQuote = ({
+ networkFeeAmount = '0.00000001',
+ sentAmount = '0.99997',
+}: {
+ networkFeeAmount?: string;
+ sentAmount?: string;
+} = {}): QuoteResponse & QuoteMetadata =>
+ ({
+ ...mockQuoteWithMetadata,
+ quote: {
+ ...mockQuoteWithMetadata.quote,
+ srcChainId: ChainId.BTC,
+ },
+ sentAmount: {
+ ...mockQuoteWithMetadata.sentAmount,
+ amount: sentAmount,
+ },
+ totalNetworkFee: {
+ ...mockQuoteWithMetadata.totalNetworkFee,
+ amount: networkFeeAmount,
+ },
+ }) as QuoteResponse & QuoteMetadata;
+
describe('useInsufficientNativeReserveError', () => {
beforeEach(() => {
mockGetGasFeesSponsoredNetworkEnabled.mockReturnValue(() => true);
@@ -181,6 +211,112 @@ describe('useInsufficientNativeReserveError', () => {
expect(result.current).toStrictEqual(undefined);
});
+ it('returns a insufficientNativeReserveError when BTC amount goes beyond reserve', () => {
+ const { result } = renderHookWithWrapper(() =>
+ useInsufficientNativeReserveError({
+ amount: '0.999995',
+ token: btcTokenOnBitcoin,
+ latestAtomicBalance: BigNumber.from('100000000'), // 1 BTC
+ walletAddress: 'bc1q7m7cq86p5fnz29s3e0nl8g5ep3p5d4rz2f3duz',
+ }),
+ );
+
+ expect(result.current).toStrictEqual({
+ maxSwappableNativeBalance: '0.99997',
+ minimumNativeBalanceToBeKeptInAccount: '0.00003',
+ });
+ });
+
+ it('returns insufficientNativeReserveError=undefined when BTC amount covers exactly the reserve', () => {
+ const { result } = renderHookWithWrapper(() =>
+ useInsufficientNativeReserveError({
+ amount: '0.99997',
+ token: btcTokenOnBitcoin,
+ latestAtomicBalance: BigNumber.from('100000000'), // 1 BTC
+ walletAddress: 'bc1q7m7cq86p5fnz29s3e0nl8g5ep3p5d4rz2f3duz',
+ }),
+ );
+
+ expect(result.current).toStrictEqual(undefined);
+ });
+
+ it('returns a quote-aware insufficientNativeReserveError for BTC when quote fees consume the reserve', () => {
+ const { result } = renderHookWithWrapper(() =>
+ useInsufficientNativeReserveError({
+ amount: '0.99997',
+ token: btcTokenOnBitcoin,
+ latestAtomicBalance: BigNumber.from('100000000'), // 1 BTC
+ walletAddress: 'bc1q7m7cq86p5fnz29s3e0nl8g5ep3p5d4rz2f3duz',
+ activeQuote: createBtcQuote({
+ networkFeeAmount: '0.00000001',
+ sentAmount: '0.99997',
+ }),
+ }),
+ );
+
+ expect(result.current).toStrictEqual({
+ maxSwappableNativeBalance: '0.99996999',
+ minimumNativeBalanceToBeKeptInAccount: '0.00003',
+ });
+ });
+
+ it('truncates BTC quote amounts to token decimals before parsing', () => {
+ const { result } = renderHookWithWrapper(() =>
+ useInsufficientNativeReserveError({
+ amount: '0.999970009',
+ token: btcTokenOnBitcoin,
+ latestAtomicBalance: BigNumber.from('100000000'), // 1 BTC
+ walletAddress: 'bc1q7m7cq86p5fnz29s3e0nl8g5ep3p5d4rz2f3duz',
+ activeQuote: createBtcQuote({
+ networkFeeAmount: '0.000000019',
+ sentAmount: '0.999970009',
+ }),
+ }),
+ );
+
+ expect(result.current).toStrictEqual({
+ maxSwappableNativeBalance: '0.99996999',
+ minimumNativeBalanceToBeKeptInAccount: '0.00003',
+ });
+ });
+
+ it('includes BTC source-side quote overhead in the max swappable amount', () => {
+ const { result } = renderHookWithWrapper(() =>
+ useInsufficientNativeReserveError({
+ amount: '0.99997',
+ token: btcTokenOnBitcoin,
+ latestAtomicBalance: BigNumber.from('100000000'), // 1 BTC
+ walletAddress: 'bc1q7m7cq86p5fnz29s3e0nl8g5ep3p5d4rz2f3duz',
+ activeQuote: createBtcQuote({
+ networkFeeAmount: '0.00000001',
+ sentAmount: '0.999971',
+ }),
+ }),
+ );
+
+ expect(result.current).toStrictEqual({
+ maxSwappableNativeBalance: '0.99996899',
+ minimumNativeBalanceToBeKeptInAccount: '0.00003',
+ });
+ });
+
+ it('returns insufficientNativeReserveError=undefined for BTC when gas validation should own the failure', () => {
+ const { result } = renderHookWithWrapper(() =>
+ useInsufficientNativeReserveError({
+ amount: '0.99997',
+ token: btcTokenOnBitcoin,
+ latestAtomicBalance: BigNumber.from('100000000'), // 1 BTC
+ walletAddress: 'bc1q7m7cq86p5fnz29s3e0nl8g5ep3p5d4rz2f3duz',
+ activeQuote: createBtcQuote({
+ networkFeeAmount: '0.000031',
+ sentAmount: '0.99997',
+ }),
+ }),
+ );
+
+ expect(result.current).toStrictEqual(undefined);
+ });
+
it('returns insufficientNativeReserveError=undefined when token is not the native one', () => {
const { result } = renderHookWithWrapper(() =>
useInsufficientNativeReserveError({
From 72e27e9f96d7ed813d4ebaa74fec1dc8c24ce85b Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Thu, 21 May 2026 14:41:59 +0000
Subject: [PATCH 52/66] [skip ci] Bump version number to 5131
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 1c27cc20e315..80f4ebf4758b 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5130
+ versionCode 5131
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index deaef039ac2b..5800010ed5d3 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5130
+ VERSION_NUMBER: 5131
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5130
+ FLASK_VERSION_NUMBER: 5131
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 3c3dc6bd1b2b..828ceb0982f3 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5130;
+ CURRENT_PROJECT_VERSION = 5131;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5130;
+ CURRENT_PROJECT_VERSION = 5131;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5130;
+ CURRENT_PROJECT_VERSION = 5131;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5130;
+ CURRENT_PROJECT_VERSION = 5131;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From e8b41b237bfa79a92825eda1c5568d2610ce3aa2 Mon Sep 17 00:00:00 2001
From: sophieqgu <37032128+sophieqgu@users.noreply.github.com>
Date: Thu, 21 May 2026 13:45:39 -0400
Subject: [PATCH 53/66] fix(rewards): copyable field container (#30471)
## **Description**
## **Changelog**
CHANGELOG entry: null
## **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.
#### Performance checks (if applicable)
- [x] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [x] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [x] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Primarily deletes agent/command documentation and updates
`.cursor/BUGBOT.md` to reference new testing docs; no runtime app logic
changes, with minor risk of broken internal tool references if any
removed skill files are still expected by automation.
>
> **Overview**
> Removes a large set of repo-local AI skill and command documentation
under `.agents/skills/` and `.claude/` (including related reference
guides), effectively decommissioning those legacy harness entrypoints.
>
> Updates `.cursor/BUGBOT.md` to reference the new SSOT testing docs
under `docs/testing/` for unit, e2e, and component view test guidelines
instead of the previous `.cursor/rules/*` references.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
164cbb6bebb5270521987c11c515c9054e53ab87. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---
.../UI/Rewards/components/ReferralDetails/CopyableField.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/components/UI/Rewards/components/ReferralDetails/CopyableField.tsx b/app/components/UI/Rewards/components/ReferralDetails/CopyableField.tsx
index 6bf5b7a0cc96..8e6a97baf4fc 100644
--- a/app/components/UI/Rewards/components/ReferralDetails/CopyableField.tsx
+++ b/app/components/UI/Rewards/components/ReferralDetails/CopyableField.tsx
@@ -46,7 +46,7 @@ const CopyableField: React.FC = ({
)}
From 1d215f0004271189b6ee325f0c14b0f1bfb586ee Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Thu, 21 May 2026 17:47:31 +0000
Subject: [PATCH 54/66] [skip ci] Bump version number to 5134
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 80f4ebf4758b..d97d944ffbce 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5131
+ versionCode 5134
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 5800010ed5d3..5f182b6f9a9e 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5131
+ VERSION_NUMBER: 5134
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5131
+ FLASK_VERSION_NUMBER: 5134
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 828ceb0982f3..4d544fcc0680 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5131;
+ CURRENT_PROJECT_VERSION = 5134;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5131;
+ CURRENT_PROJECT_VERSION = 5134;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5131;
+ CURRENT_PROJECT_VERSION = 5134;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5131;
+ CURRENT_PROJECT_VERSION = 5134;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 8f39d770b9e42c8e15cae174d0ccfa96ea53fdf4 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 20:01:56 +0200
Subject: [PATCH 55/66] chore(runway): cherry-pick fix: show fallback token
icons in confirmation rows cp-7.78.0 (#30533)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- fix: show fallback token icons in confirmation rows cp-7.78.0 (#30502)
## **Description**
Fixes missing token icons in confirmation rows when token metadata is
unavailable in local token state. The confirmation `TokenIcon` now falls
back to the shared MetaMask token icon URL when it has an address, chain
ID, and symbol, while still rendering nothing when neither token
metadata nor a symbol is available.
This also passes the receive token symbol from `PayWithRow` into
`TokenIcon`, so Perps and Predict deposit/withdraw rows can render the
token icon fallback for mUSD and other supported ERC-20 tokens with
missing local metadata.
## **Changelog**
CHANGELOG entry: Show fallback token icons in confirmation rows
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/CONF-1411
## **Manual testing steps**
```gherkin
Feature: Confirmation token icon fallback
Scenario: user withdraws mUSD from Perps or Predict
Given the user has an mUSD balance
And the user starts a Perps or Predict withdraw flow
When the Receive row displays mUSD
Then the mUSD token icon is shown next to the token symbol
```
## **Screenshots/Recordings**
### **Before**
### **After**
N/A
## **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
- [x] 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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Low Risk**
> Low risk UI change that only affects how token icons are
resolved/rendered in confirmation rows. Main risk is incorrect fallback
URL generation for some chains, but it’s covered by new unit tests.
>
> **Overview**
> Fixes missing token icons in confirmation flows by updating
`TokenIcon` to accept an optional `symbol` and, when local token
metadata is absent, fall back to a MetaMask-hosted token icon URL (while
still rendering nothing when neither metadata nor `symbol` is
available).
>
> Updates `PayWithRow` to pass the displayed token `symbol` into
`TokenIcon` (notably for *Receive/withdraw* rows), and expands unit
tests to validate symbol propagation and fallback URL generation across
multiple supported chains.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
8921cc44d9382d74ed659710e5f6f642fd6500c1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[a0f1af8](https://github.com/MetaMask/metamask-mobile/commit/a0f1af8f9ed91a43643e4d0f6f22af0bdcc93ebc)
Co-authored-by: Daniel <80175477+dan437@users.noreply.github.com>
---
.../rows/pay-with-row/pay-with-row.test.tsx | 14 +++-
.../rows/pay-with-row/pay-with-row.tsx | 1 +
.../components/token-icon/token-icon.test.tsx | 69 ++++++++++++++++++-
.../components/token-icon/token-icon.tsx | 21 +++++-
4 files changed, 100 insertions(+), 5 deletions(-)
diff --git a/app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.test.tsx b/app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.test.tsx
index c17030a11ff6..39cd3934dc3f 100644
--- a/app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.test.tsx
+++ b/app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.test.tsx
@@ -57,7 +57,12 @@ jest.mock('../../../hooks/metrics/useConfirmationMetricEvents', () => ({
jest.mock('../../token-icon/', () => ({
TokenIcon: (props: TokenIconProps) => (
- {`${props.address} ${props.chainId}`}
+ <>
+ {`${props.address} ${props.chainId}`}
+
+ {`icon-symbol:${props.symbol ?? ''}`}
+
+ >
),
TokenIconVariant: { Default: 'default', Row: 'row', Hero: 'hero' },
}));
@@ -199,6 +204,13 @@ describe('PayWithRow', () => {
expect(getByText('test')).toBeDefined();
});
+ it('passes the receive token symbol to the token icon', () => {
+ const { getByTestId } = render();
+ expect(getByTestId('token-icon-symbol')).toHaveTextContent(
+ 'icon-symbol:test',
+ );
+ });
+
it('hides balance in withdraw mode', () => {
const { queryByTestId } = render();
expect(queryByTestId('pay-with-balance')).toBeNull();
diff --git a/app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.tsx b/app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.tsx
index a883e6f4468c..d2219e56f297 100644
--- a/app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.tsx
+++ b/app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.tsx
@@ -161,6 +161,7 @@ export function PayWithRow() {
{
+ const ReactActual = jest.requireActual('react');
+ const { Text, View } = jest.requireActual('react-native');
+
+ return {
+ __esModule: true,
+ default: ({
+ icon,
+ symbol,
+ testID,
+ }: {
+ icon?: string;
+ symbol?: string;
+ testID?: string;
+ }) =>
+ icon
+ ? ReactActual.createElement(View, {
+ testID,
+ accessibilityLabel: icon,
+ })
+ : ReactActual.createElement(
+ Text,
+ { testID },
+ symbol?.[0]?.toUpperCase(),
+ ),
+ };
+});
+
const ADDRESS_MOCK = tokenAddress1Mock;
const CHAIN_ID_MOCK = '0x1';
+const TOKEN_ICON_URL_MOCK = `https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/${ADDRESS_MOCK}.png`;
+const SUPPORTED_CHAIN_ICON_CASES: [string, Hex, string][] = [
+ ['mainnet', '0x1', '1'],
+ ['Linea', '0xe708', '59144'],
+ ['BSC', '0x38', '56'],
+ ['Monad', '0x8f', '143'],
+];
const STATE_MOCK = merge(
{},
@@ -36,7 +72,9 @@ describe('TokenIcon', () => {
chainId: CHAIN_ID_MOCK,
});
- expect(getByTestId('token-icon')).toHaveTextContent('T');
+ expect(getByTestId('token-icon').props.accessibilityLabel).toBe(
+ TOKEN_ICON_URL_MOCK,
+ );
});
it('renders nothing if token not found', () => {
@@ -47,4 +85,33 @@ describe('TokenIcon', () => {
expect(queryByTestId('token-icon')).toBeNull();
});
+
+ it('renders token icon URL fallback when token metadata is missing', () => {
+ const address = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef';
+ const { getByTestId } = render({
+ address,
+ chainId: CHAIN_ID_MOCK,
+ symbol: 'ABC',
+ });
+
+ expect(getByTestId('token-icon').props.accessibilityLabel).toBe(
+ `https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/${address}.png`,
+ );
+ });
+
+ it.each(SUPPORTED_CHAIN_ICON_CASES)(
+ 'renders token icon URL fallback for %s when token metadata is missing',
+ (_networkName, chainId, decimalChainId) => {
+ const address = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef';
+ const { getByTestId } = render({
+ address,
+ chainId,
+ symbol: 'ABC',
+ });
+
+ expect(getByTestId('token-icon').props.accessibilityLabel).toBe(
+ `https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/${decimalChainId}/erc20/${address}.png`,
+ );
+ },
+ );
});
diff --git a/app/components/Views/confirmations/components/token-icon/token-icon.tsx b/app/components/Views/confirmations/components/token-icon/token-icon.tsx
index 48816bb9f02b..e540f3107f32 100644
--- a/app/components/Views/confirmations/components/token-icon/token-icon.tsx
+++ b/app/components/Views/confirmations/components/token-icon/token-icon.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { getNativeTokenAddress } from '@metamask/assets-controllers';
import { Hex } from '@metamask/utils';
import BaseTokenIcon from '../../../../Base/TokenIcon';
import styleSheet from './token-icon.styles';
@@ -11,11 +12,13 @@ import Badge, {
} from '../../../../../component-library/components/Badges/Badge';
import { getNetworkImageSource } from '../../../../../util/networks';
import { useTokenWithBalance } from '../../hooks/tokens/useTokenWithBalance';
+import { getAssetImageUrl } from '../../../../UI/Bridge/hooks/useAssetMetadata/utils';
export interface TokenIconProps {
address: Hex;
chainId: Hex;
showNetwork?: boolean;
+ symbol?: string;
variant?: TokenIconVariant;
}
@@ -29,16 +32,20 @@ export const TokenIcon: React.FC = ({
address,
chainId,
showNetwork = true,
+ symbol: symbolProp,
variant = TokenIconVariant.Default,
}) => {
const { styles } = useStyles(styleSheet, { variant });
const token = useTokenWithBalance(address, chainId);
+ const symbol = token?.symbol ?? symbolProp;
- if (!token) {
+ if (!token && !symbol) {
return null;
}
+ const icon = token?.image ?? getTokenIconUrl(address, chainId);
+
const networkImageSource = getNetworkImageSource({
chainId,
});
@@ -59,10 +66,18 @@ export const TokenIcon: React.FC = ({
>
);
};
+
+function getTokenIconUrl(address: Hex, chainId: Hex) {
+ if (address.toLowerCase() === getNativeTokenAddress(chainId).toLowerCase()) {
+ return undefined;
+ }
+
+ return getAssetImageUrl(address, chainId);
+}
From 27f940d15bd4bb894138fc346f9f0536183a2a05 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Thu, 21 May 2026 18:03:49 +0000
Subject: [PATCH 56/66] [skip ci] Bump version number to 5137
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index d97d944ffbce..5eebd14ee820 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5134
+ versionCode 5137
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 5f182b6f9a9e..6a43eec85478 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5134
+ VERSION_NUMBER: 5137
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5134
+ FLASK_VERSION_NUMBER: 5137
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 4d544fcc0680..624af390d712 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5134;
+ CURRENT_PROJECT_VERSION = 5137;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5134;
+ CURRENT_PROJECT_VERSION = 5137;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5134;
+ CURRENT_PROJECT_VERSION = 5137;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5134;
+ CURRENT_PROJECT_VERSION = 5137;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From f3923447ca828470c4c4ef94a2460f6102c40354 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 20:10:34 +0200
Subject: [PATCH 57/66] chore(runway): cherry-pick feat(predict): Enable Bottom
Sheet via Explore page (#30535)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- feat(predict): Enable Bottom Sheet via Explore page cp-7.78.0 (#30481)
## **Description**
Predict market cards on the Explore feed now open the Buy/Sell preview
as an in-place bottom sheet (matching the dedicated Predict feed) when
the `predictBottomSheet` feature flag is on, instead of routing to the
full-page bet slip.
### What changed
1. **Mounted `PredictPreviewSheetProvider` at the `HomeTabs` level** in
`app/components/Nav/Main/MainNavigator.js`, wrapping the
`Tab.Navigator`. The provider was previously only mounted inside
`PredictScreenStack`, so triggering `openBuySheet`/`openSellSheet` from
any other tab fell back to navigation. Mounting at `HomeTabs` makes the
sheet usable from Explore (and any future tab that needs it) while
keeping the existing in-Predict behavior untouched (`PredictScreenStack`
still mounts its own provider; the inner one shadows the outer for
usage).
2. **Why mount at `HomeTabs` and not inside the tab itself?**
`BottomSheet` from `@metamask/design-system-react-native` uses `absolute
inset-0` for its container. If the provider is mounted inside an
individual tab's content area, the sheet's parent is smaller than the
viewport and the sheet gets clipped at the top of the screen and below
by the bottom tab bar. Mounting at `HomeTabs` puts the sheet's parent at
the full-viewport `Home` Stack.Screen card, which is the smallest level
above the tab bar that gives correct dimensions.
3. **Fixed a duplicate-toast and stale-suppression bug** that the new
placement exposes. With both `HomeTabs` and `PredictScreenStack`
providers now mounted simultaneously while the user is inside the
Predict stack, both used to:
- independently fire the state-based "Try again" failure toast on
`activeOrder.error` transitions (no dedup in `ToastService`);
- increment the same `_providerSheetModeCount` counter that gates
`shouldSuppressLegacyOrderFailureToast()`, which then swallowed the
legacy failure toast in tabs/flows where the active provider could not
actually fire its own toast (e.g. Wallet/Trade/Money/Rewards, or
HomepageDiscoveryTabs which mounts in `disableBottomSheet` mode).
Replaced the module-level counter with a registration **stack**
(`_sheetModeProviders`). Each entry holds the provider's id and a
`hasBuyParams()` getter. The topmost (most recently mounted, innermost
in the tree) entry is the only "active" one:
- The state-based toast effect in `PredictPreviewSheetContext.tsx` bails
out unless the current provider is active — so only the innermost
provider fires the Retry toast.
- `shouldSuppressLegacyOrderFailureToast()` now consults the active
entry's `hasBuyParams()`, so the legacy toast is only suppressed when
the active provider will actually surface its own toast.
4. **Test coverage** for the multi-provider scenario in
`PredictPreviewSheetContext.test.tsx`:
- Topmost provider fires the failure toast exactly once when both are
mounted.
- Outer provider becomes active again after the inner unmounts.
- Outer (sheet-mode) provider still fires when the inner provider is
mounted with `disableBottomSheet`.
- `shouldSuppressLegacyOrderFailureToast` correctly tracks the topmost
provider across mount/unmount.
5. Added `PredictPreviewSheetProvider` to the Predict barrel
(`app/components/UI/Predict/index.ts`) for consistency, and a rationale
comment in `MainNavigator.js` explaining why the wrap lives at
`HomeTabs` (so a future maintainer doesn't move it back inside a tab).
### Files touched
- `app/components/Nav/Main/MainNavigator.js`
- `app/components/Nav/Main/MainNavigator.test.tsx` (mock update)
- `app/components/UI/Predict/contexts/PredictPreviewSheetContext.tsx`
-
`app/components/UI/Predict/contexts/PredictPreviewSheetContext.test.tsx`
- `app/components/UI/Predict/index.ts`
## **Changelog**
CHANGELOG entry: Added in-place buy/sell bottom sheet to Predict market
cards on the Explore feed when the `predictBottomSheet` feature flag is
enabled.
## **Related issues**
Fixes:
## **Manual testing steps**
```gherkin
Feature: Predict bottom sheet on Explore feed
Scenario: open buy sheet from Explore with feature flag ON
Given the predictBottomSheet feature flag is enabled
And the user is on the Explore tab
And the Explore feed is showing Predict market cards
When the user taps "Yes" on a Predict market card
Then a buy preview bottom sheet opens in place
And the sheet is anchored above the bottom tab bar (no clipping at the top or bottom)
And the user can swipe the sheet down to dismiss it
Scenario: navigation fallback when feature flag is OFF
Given the predictBottomSheet feature flag is disabled
And the user is on the Explore tab
When the user taps "Yes" on a Predict market card
Then the app navigates to the full-page bet slip (legacy behavior, unchanged)
Scenario: Predict tab behavior is unchanged
Given the predictBottomSheet feature flag is enabled
And the user is on the Predict tab
When the user taps an outcome on a market card
Then the buy preview bottom sheet opens in place (existing behavior)
And there is exactly one Retry toast if the order subsequently fails
Scenario: only the topmost provider fires the failure toast
Given the predictBottomSheet feature flag is enabled
And the user opened and dismissed a buy sheet from Explore
And the user navigated to the Predict tab and opened/dismissed another buy sheet
And the user is now on the Predict tab with both sheets dismissed
When the active Predict order transitions to a failed state
Then the user sees exactly one "Try again" toast (not two)
And tapping Retry reopens the most recently used sheet's market context
```
## **Screenshots/Recordings**
### **Before**
### **After**
https://github.com/user-attachments/assets/6bcb24e9-b81c-4c8f-b193-295440cd5805
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- [x] I've tested with a power user scenario
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
> _Performance checks N/A: this PR only repositions an existing React
provider higher in the tree and refactors a module-level counter into a
registration stack. No new subscriptions/renders on hot paths; the
failure-toast effect now does strictly less work in non-active
providers._
## **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]
> **Medium Risk**
> Touches top-level navigation composition and refactors module-scoped
toast suppression/dedup logic for Predict order failures; regressions
could affect bottom sheet rendering or toast behavior across tabs.
>
> **Overview**
> Enables Predict market cards opened outside the Predict tab (e.g.
Explore) to use the in-place Buy/Sell preview bottom sheet by mounting
`PredictPreviewSheetProvider` above the home `Tab.Navigator`.
>
> Refactors `PredictPreviewSheetContext` to handle multiple
simultaneously-mounted providers via a registration stack: only the
topmost sheet-mode provider can fire the state-driven Retry toast, and
legacy order-failure toast suppression now depends on the active
provider having remembered buy params (reducing stale suppression).
Tests are updated/added to cover multi-provider dedup and the new
suppression behavior, and `PredictPreviewSheetProvider` is exported from
the Predict barrel.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
51b78178bc4543e21070641b11849599b09855f8. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
[22e0934](https://github.com/MetaMask/metamask-mobile/commit/22e0934089cb0e0d49fdfc53274e40fc652a12c9)
Co-authored-by: Aslau Mario-Daniel
---
app/components/Nav/Main/MainNavigator.js | 133 +++++++-------
.../Nav/Main/MainNavigator.test.tsx | 20 ++-
.../PredictPreviewSheetContext.test.tsx | 163 +++++++++++++++++-
.../contexts/PredictPreviewSheetContext.tsx | 78 +++++++--
app/components/UI/Predict/contexts/index.ts | 2 +-
.../usePredictToastRegistrations.test.tsx | 2 +-
.../hooks/usePredictToastRegistrations.tsx | 12 +-
app/components/UI/Predict/index.ts | 1 +
8 files changed, 320 insertions(+), 91 deletions(-)
diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js
index ef3b1b36448a..415023eaf08e 100644
--- a/app/components/Nav/Main/MainNavigator.js
+++ b/app/components/Nav/Main/MainNavigator.js
@@ -116,6 +116,7 @@ import {
import {
PredictScreenStack,
PredictModalStack,
+ PredictPreviewSheetProvider,
selectPredictEnabledFlag,
} from '../../UI/Predict';
import {
@@ -822,73 +823,87 @@ const HomeTabs = () => {
};
return (
-
- {/* Home Tab */}
-
-
- {/* Explore Tab (w/ hidden browser) */}
- <>
+ /*
+ * PredictPreviewSheetProvider is mounted here (above Tab.Navigator) so its
+ * BottomSheet renders inside the full-viewport Home Stack.Screen card.
+ * BottomSheet uses `absolute inset-0` (see
+ * @metamask/design-system-react-native) and would be clipped by an
+ * individual tab's content area if mounted lower in the tree.
+ *
+ * A nested provider in PredictScreenStack still shadows this one for
+ * usage; the registration stack in PredictPreviewSheetContext keeps only
+ * the innermost (most recently mounted) provider active for state-based
+ * Retry toasts so we don't double-fire when both are mounted.
+ */
+
+
+ {/* Home Tab */}
- [Routes.TRENDING_VIEW, Routes.BROWSER.HOME].includes(
- rootScreenName,
- ),
- }}
- component={ExploreHome}
+ name={Routes.WALLET.HOME}
+ options={options.home}
+ component={WalletTabModalFlow}
/>
- {children}}
- />
- >
- {/* Trade Tab */}
-
+ {/* Explore Tab (w/ hidden browser) */}
+ <>
+
+ [Routes.TRENDING_VIEW, Routes.BROWSER.HOME].includes(
+ rootScreenName,
+ ),
+ }}
+ component={ExploreHome}
+ />
+ {children}}
+ />
+ >
- {/* Activity Tab (replaced by Money when feature flag is on) */}
- {isMoneyHomeScreenEnabled ? (
+ {/* Trade Tab */}
- ) : (
+
+ {/* Activity Tab (replaced by Money when feature flag is on) */}
+ {isMoneyHomeScreenEnabled ? (
+
+ ) : (
+ {children}}
+ />
+ )}
+
+ {/* Rewards Tab */}
{children}}
+ name={Routes.REWARDS_VIEW}
+ options={options.rewards}
+ component={RewardsHome}
+ layout={({ children }) => UnmountOnBlurComponent(children)}
/>
- )}
-
- {/* Rewards Tab */}
- UnmountOnBlurComponent(children)}
- />
-
+
+
);
};
diff --git a/app/components/Nav/Main/MainNavigator.test.tsx b/app/components/Nav/Main/MainNavigator.test.tsx
index 0422c5378d4e..01841ac091d8 100644
--- a/app/components/Nav/Main/MainNavigator.test.tsx
+++ b/app/components/Nav/Main/MainNavigator.test.tsx
@@ -35,12 +35,20 @@ jest.mock('../../UI/Perps', () => ({
selectPerpsEnabledFlag: (state: unknown) => mockSelectPerpsEnabledFlag(state),
}));
-jest.mock('../../UI/Predict', () => ({
- PredictScreenStack: () => 'PredictScreenStack',
- PredictModalStack: () => 'PredictModalStack',
- selectPredictEnabledFlag: (state: unknown) =>
- mockSelectPredictEnabledFlag(state),
-}));
+jest.mock('../../UI/Predict', () => {
+ const { Fragment } = jest.requireActual('react');
+ return {
+ PredictScreenStack: () => 'PredictScreenStack',
+ PredictModalStack: () => 'PredictModalStack',
+ PredictPreviewSheetProvider: ({
+ children,
+ }: {
+ children: React.ReactNode;
+ }) => jest.requireActual('react').createElement(Fragment, null, children),
+ selectPredictEnabledFlag: (state: unknown) =>
+ mockSelectPredictEnabledFlag(state),
+ };
+});
jest.mock('../../UI/MarketInsights', () => ({
MarketInsightsView: () => 'MarketInsightsView',
diff --git a/app/components/UI/Predict/contexts/PredictPreviewSheetContext.test.tsx b/app/components/UI/Predict/contexts/PredictPreviewSheetContext.test.tsx
index 43bcb01b8d41..ef8a07900dab 100644
--- a/app/components/UI/Predict/contexts/PredictPreviewSheetContext.test.tsx
+++ b/app/components/UI/Predict/contexts/PredictPreviewSheetContext.test.tsx
@@ -3,8 +3,8 @@ import { act, render, screen, fireEvent } from '@testing-library/react-native';
import { Text, TouchableOpacity, View } from 'react-native';
import { TEST_HEX_COLORS as mockTestHexColors } from '../testUtils/mockColors';
import {
- isPredictSheetProviderMounted,
PredictPreviewSheetProvider,
+ shouldSuppressLegacyOrderFailureToast,
usePredictPreviewSheet,
} from './PredictPreviewSheetContext';
import type {
@@ -616,6 +616,138 @@ describe('PredictPreviewSheetContext', () => {
});
});
+ describe('multi-provider dedup', () => {
+ // Mirrors production reality: HomeTabs mounts a sheet-mode provider above
+ // Tab.Navigator (so the BottomSheet's parent is the full-viewport stack
+ // card), and PredictScreenStack mounts another sheet-mode provider when
+ // the user navigates into the Predict tab. Both stay mounted while inside
+ // Predict. The toast effect must fire from the topmost (most recently
+ // mounted, innermost in the tree) provider only.
+
+ it('fires the failure toast only from the topmost (most recently mounted) sheet-mode provider', () => {
+ const outer = render(
+
+
+ ,
+ );
+ // Outer "remembers" buy params from a prior open + dismiss.
+ fireEvent.press(outer.getByTestId('open-buy'));
+ fireEvent.press(outer.getByTestId('dismiss-sheet'));
+
+ const inner = render(
+
+
+ ,
+ );
+ // Inner also remembers buy params from a prior open + dismiss.
+ fireEvent.press(inner.getByTestId('open-buy'));
+ fireEvent.press(inner.getByTestId('dismiss-sheet'));
+
+ mockActiveOrder = { error: 'order/failed' };
+ outer.rerender(
+
+
+ ,
+ );
+ inner.rerender(
+
+
+ ,
+ );
+
+ expect(mockToastShowToast).toHaveBeenCalledTimes(1);
+
+ outer.unmount();
+ inner.unmount();
+ });
+
+ it('outer provider becomes active after inner unmounts and fires for the next error transition', () => {
+ const outer = render(
+
+
+ ,
+ );
+ fireEvent.press(outer.getByTestId('open-buy'));
+ fireEvent.press(outer.getByTestId('dismiss-sheet'));
+
+ const inner = render(
+
+
+ ,
+ );
+ fireEvent.press(inner.getByTestId('open-buy'));
+ fireEvent.press(inner.getByTestId('dismiss-sheet'));
+
+ // First error transition — only inner (topmost) fires.
+ mockActiveOrder = { error: 'order/failed' };
+ outer.rerender(
+
+
+ ,
+ );
+ inner.rerender(
+
+
+ ,
+ );
+ expect(mockToastShowToast).toHaveBeenCalledTimes(1);
+
+ // Inner unmounts — outer becomes the topmost provider.
+ inner.unmount();
+
+ // Drive a fresh falsy -> truthy transition for the outer provider so
+ // its `previousErrorRef` flips correctly.
+ mockActiveOrder = null;
+ outer.rerender(
+
+
+ ,
+ );
+ mockActiveOrder = { error: 'order/failed-2' };
+ outer.rerender(
+
+
+ ,
+ );
+
+ expect(mockToastShowToast).toHaveBeenCalledTimes(2);
+
+ outer.unmount();
+ });
+
+ it('shouldSuppressLegacyOrderFailureToast tracks the topmost provider after unmount order', () => {
+ const outer = render(
+
+
+ ,
+ );
+ // Outer alone, no opens — suppression off.
+ expect(shouldSuppressLegacyOrderFailureToast()).toBe(false);
+
+ fireEvent.press(outer.getByTestId('open-buy'));
+ expect(shouldSuppressLegacyOrderFailureToast()).toBe(true);
+
+ // Inner mounts on top of outer — its `lastBuyParamsRef` is null,
+ // so suppression flips back to false until inner has its own open.
+ const inner = render(
+
+
+ ,
+ );
+ expect(shouldSuppressLegacyOrderFailureToast()).toBe(false);
+
+ fireEvent.press(inner.getByTestId('open-buy'));
+ expect(shouldSuppressLegacyOrderFailureToast()).toBe(true);
+
+ // Unmounting inner falls back to outer (still has params).
+ inner.unmount();
+ expect(shouldSuppressLegacyOrderFailureToast()).toBe(true);
+
+ outer.unmount();
+ expect(shouldSuppressLegacyOrderFailureToast()).toBe(false);
+ });
+ });
+
describe('failure toast auto-clear timer', () => {
beforeEach(() => {
jest.useFakeTimers();
@@ -690,23 +822,40 @@ describe('PredictPreviewSheetContext', () => {
});
});
- describe('isPredictSheetProviderMounted', () => {
- it('returns false when provider is not mounted', () => {
- expect(isPredictSheetProviderMounted()).toBe(false);
+ describe('shouldSuppressLegacyOrderFailureToast', () => {
+ it('returns false when no provider is mounted', () => {
+ expect(shouldSuppressLegacyOrderFailureToast()).toBe(false);
});
- it('returns true while provider is mounted and false after unmount', () => {
+ it('returns false while provider is mounted but no sheet has been opened yet', () => {
+ // Suppression is gated on the topmost provider's `lastBuyParamsRef` so
+ // the legacy toast keeps firing for tabs/flows where the user has not
+ // initiated a sheet (e.g. order failure surfaces from elsewhere).
const { unmount } = render(
,
);
- expect(isPredictSheetProviderMounted()).toBe(true);
+ expect(shouldSuppressLegacyOrderFailureToast()).toBe(false);
+
+ unmount();
+ });
+
+ it('returns true after openBuySheet is called and false after unmount', () => {
+ const { unmount } = render(
+
+
+ ,
+ );
+
+ fireEvent.press(screen.getByTestId('open-buy'));
+
+ expect(shouldSuppressLegacyOrderFailureToast()).toBe(true);
unmount();
- expect(isPredictSheetProviderMounted()).toBe(false);
+ expect(shouldSuppressLegacyOrderFailureToast()).toBe(false);
});
});
diff --git a/app/components/UI/Predict/contexts/PredictPreviewSheetContext.tsx b/app/components/UI/Predict/contexts/PredictPreviewSheetContext.tsx
index 54aa2048cbdb..a92642da0305 100644
--- a/app/components/UI/Predict/contexts/PredictPreviewSheetContext.tsx
+++ b/app/components/UI/Predict/contexts/PredictPreviewSheetContext.tsx
@@ -51,17 +51,49 @@ import { usePredictActiveOrder } from '../hooks/usePredictActiveOrder';
import { PredictDismissalMethod } from '../constants/eventNames';
import { parseAnalyticsProperties } from '../utils/analytics';
-let _providerMounted = false;
+// Registration stack of sheet-mode providers — multiple providers can be
+// mounted simultaneously (e.g. HomeTabs + PredictScreenStack when the user
+// navigates from Explore into Predict), so a single flag cannot tell us
+// which one is "active". The top of the stack (most recently mounted, i.e.
+// innermost in the tree) is the only provider that should fire its
+// state-based Retry toast — earlier-mounted providers stay silent to avoid
+// duplicate toasts for the same `activeOrder.error` transition.
+interface SheetModeProviderEntry {
+ id: number;
+ hasBuyParams: () => boolean;
+}
+
+let _sheetModeProviders: SheetModeProviderEntry[] = [];
+let _nextSheetModeProviderId = 0;
+
+function registerSheetModeProvider(hasBuyParams: () => boolean): number {
+ const id = ++_nextSheetModeProviderId;
+ _sheetModeProviders = [..._sheetModeProviders, { id, hasBuyParams }];
+ return id;
+}
+
+function unregisterSheetModeProvider(id: number): void {
+ _sheetModeProviders = _sheetModeProviders.filter((entry) => entry.id !== id);
+}
+
+function isActiveSheetModeProvider(id: number): boolean {
+ return _sheetModeProviders[_sheetModeProviders.length - 1]?.id === id;
+}
/**
- * Returns whether `PredictPreviewSheetProvider` is currently mounted somewhere
- * in the tree. Used by `usePredictToastRegistrations` to decide whether to
- * suppress the order failure toast — when the provider is mounted, its
- * state-based trigger surfaces a persistent Retry toast and the legacy plain
- * toast would be a duplicate.
+ * Returns true only when the active (top-of-stack) sheet-mode provider has
+ * remembered buy params and will therefore surface its own Retry toast.
+ * Used by `usePredictToastRegistrations` to decide whether to suppress the
+ * legacy order-failure toast.
+ *
+ * Checking `hasBuyParams()` (rather than just "any provider mounted")
+ * avoids suppressing the legacy toast when no sheet-mode provider is
+ * positioned to fire — e.g. the active provider has not yet had
+ * `openBuySheet` called on it.
*/
-export function isPredictSheetProviderMounted(): boolean {
- return _providerMounted;
+export function shouldSuppressLegacyOrderFailureToast(): boolean {
+ const top = _sheetModeProviders[_sheetModeProviders.length - 1];
+ return Boolean(top?.hasBuyParams());
}
const SellSheetHeader: React.FC<{ params: PredictSellPreviewParams }> = ({
@@ -205,16 +237,27 @@ export const PredictPreviewSheetProvider: React.FC<
*/
const clearErrorTimerRef = useRef | null>(null);
+ /**
+ * Module-level registration id for this provider instance. Set on mount
+ * (when not disabled) and used to guard the failure-toast effect so only
+ * the topmost (most recently mounted) provider fires.
+ */
+ const providerIdRef = useRef(null);
+ const hasBuyParams = useCallback(() => lastBuyParamsRef.current !== null, []);
+
useEffect(() => {
- _providerMounted = true;
+ providerIdRef.current = registerSheetModeProvider(hasBuyParams);
return () => {
- _providerMounted = false;
+ if (providerIdRef.current !== null) {
+ unregisterSheetModeProvider(providerIdRef.current);
+ providerIdRef.current = null;
+ }
if (clearErrorTimerRef.current) {
clearTimeout(clearErrorTimerRef.current);
clearErrorTimerRef.current = null;
}
};
- }, []);
+ }, [hasBuyParams]);
const openBuySheet = useCallback(
(params: PredictBuyPreviewParams) => {
@@ -279,6 +322,19 @@ export const PredictPreviewSheetProvider: React.FC<
return;
}
+ // When multiple sheet-mode providers are mounted simultaneously (e.g.
+ // HomeTabs + PredictScreenStack while the user is inside the Predict
+ // stack), only the topmost (most recently mounted, innermost in the
+ // tree) provider should fire the toast — earlier-mounted providers
+ // also hold their own `lastBuyParamsRef` and would otherwise duplicate
+ // the toast (and the `clearOrderError` timer).
+ if (
+ providerIdRef.current === null ||
+ !isActiveSheetModeProvider(providerIdRef.current)
+ ) {
+ return;
+ }
+
const lastParams = lastBuyParamsRef.current;
// Use `closeButtonOptions` (with `ButtonVariants.Link`) rather than
// `linkButtonOptions` so the Retry sits inline on the right of the row
diff --git a/app/components/UI/Predict/contexts/index.ts b/app/components/UI/Predict/contexts/index.ts
index b3b5d3172318..b0a25da96559 100644
--- a/app/components/UI/Predict/contexts/index.ts
+++ b/app/components/UI/Predict/contexts/index.ts
@@ -4,7 +4,7 @@ export {
} from './PredictEntryPointContext';
export {
- isPredictSheetProviderMounted,
PredictPreviewSheetProvider,
+ shouldSuppressLegacyOrderFailureToast,
usePredictPreviewSheet,
} from './PredictPreviewSheetContext';
diff --git a/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx b/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx
index 0721c9f22608..88b8679b72fe 100644
--- a/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx
+++ b/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx
@@ -108,7 +108,7 @@ jest.mock('../selectors/featureFlags', () => ({
}));
jest.mock('../contexts/PredictPreviewSheetContext', () => ({
- isPredictSheetProviderMounted: jest.fn(() => mockProviderMounted),
+ shouldSuppressLegacyOrderFailureToast: jest.fn(() => mockProviderMounted),
}));
describe('usePredictToastRegistrations', () => {
diff --git a/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx b/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx
index d32cec056946..db5c866cf5cd 100644
--- a/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx
+++ b/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx
@@ -27,7 +27,7 @@ import { usePredictWithdraw } from './usePredictWithdraw';
import { store } from '../../../../store';
import { resolveWithdrawTokenInfo } from '../../../Views/confirmations/utils/withdraw-token-resolution';
import { selectPredictBottomSheetEnabledFlag } from '../selectors/featureFlags';
-import { isPredictSheetProviderMounted } from '../contexts/PredictPreviewSheetContext';
+import { shouldSuppressLegacyOrderFailureToast } from '../contexts/PredictPreviewSheetContext';
const showPendingToast = ({
showToast,
@@ -367,11 +367,11 @@ export const usePredictToastRegistrations = (): ToastRegistration[] => {
}
if (status === 'failed') {
- // When the bottom-sheet flow is on and the provider is mounted,
- // the provider's state-based trigger (in PredictPreviewSheetContext)
- // surfaces a persistent Retry toast for the same error. Skip here
- // to avoid double-firing.
- if (bottomSheetEnabled && isPredictSheetProviderMounted()) {
+ // When the bottom-sheet flow is on and the active sheet-mode
+ // provider has buy params remembered, its own state-based trigger
+ // (in PredictPreviewSheetContext) surfaces a persistent Retry
+ // toast for the same error. Skip here to avoid double-firing.
+ if (bottomSheetEnabled && shouldSuppressLegacyOrderFailureToast()) {
return;
}
showErrorToast({
diff --git a/app/components/UI/Predict/index.ts b/app/components/UI/Predict/index.ts
index 6f7b56c29d3d..64cf7842f4b2 100644
--- a/app/components/UI/Predict/index.ts
+++ b/app/components/UI/Predict/index.ts
@@ -5,5 +5,6 @@ export { default as PredictScreenStack } from './routes';
export { selectPredictEnabledFlag } from './selectors/featureFlags';
export { default as PredictSellPreview } from './views/PredictSellPreview/PredictSellPreview';
export { PredictModalStack } from './routes';
+export { PredictPreviewSheetProvider } from './contexts';
export * from './types';
From 1035902aa165e821aa3aa7fb706114d6459f4190 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Thu, 21 May 2026 18:12:29 +0000
Subject: [PATCH 58/66] [skip ci] Bump version number to 5138
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 5eebd14ee820..52a65577da32 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5137
+ versionCode 5138
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 6a43eec85478..5ac73292c1e9 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5137
+ VERSION_NUMBER: 5138
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5137
+ FLASK_VERSION_NUMBER: 5138
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 624af390d712..534c1cbb67e3 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5137;
+ CURRENT_PROJECT_VERSION = 5138;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5137;
+ CURRENT_PROJECT_VERSION = 5138;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5137;
+ CURRENT_PROJECT_VERSION = 5138;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5137;
+ CURRENT_PROJECT_VERSION = 5138;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From caf0bb8974d46c80f968caf45f22ce4171e7bbe6 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 22:46:28 +0200
Subject: [PATCH 59/66] chore(runway): cherry-pick chore: New Crowdin
Translations by GitHub Action cp-7.78.0 (#30545)
- chore: New Crowdin Translations by GitHub Action cp-7.78.0 (#30163)
Co-authored-by: metamaskbot
Co-authored-by: Laurel <153323700+i18nlaurel@users.noreply.github.com>
[7d19702](https://github.com/MetaMask/metamask-mobile/commit/7d197020d9d2e662a65affec42c7e6812d3ee3a3)
Co-authored-by: metamaskbotv2[bot] <214045046+metamaskbotv2[bot]@users.noreply.github.com>
Co-authored-by: metamaskbot
Co-authored-by: Laurel <153323700+i18nlaurel@users.noreply.github.com>
---
locales/languages/de.json | 383 ++++++++++++++++++++++++-----------
locales/languages/el.json | 381 ++++++++++++++++++++++++-----------
locales/languages/es.json | 385 ++++++++++++++++++++++++-----------
locales/languages/fr.json | 397 ++++++++++++++++++++++++------------
locales/languages/hi.json | 385 ++++++++++++++++++++++++-----------
locales/languages/id.json | 381 ++++++++++++++++++++++++-----------
locales/languages/ja.json | 385 ++++++++++++++++++++++++-----------
locales/languages/ko.json | 387 ++++++++++++++++++++++++-----------
locales/languages/pt.json | 383 ++++++++++++++++++++++++-----------
locales/languages/ru.json | 383 ++++++++++++++++++++++++-----------
locales/languages/tl.json | 367 ++++++++++++++++++++++++----------
locales/languages/tr.json | 411 ++++++++++++++++++++++++++------------
locales/languages/vi.json | 381 ++++++++++++++++++++++++-----------
locales/languages/zh.json | 383 ++++++++++++++++++++++++-----------
14 files changed, 3725 insertions(+), 1667 deletions(-)
diff --git a/locales/languages/de.json b/locales/languages/de.json
index a834dc81b19b..af0ee18452a9 100644
--- a/locales/languages/de.json
+++ b/locales/languages/de.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "Keine Geldmittel verfügbar. Verwenden Sie ein anderes Konto."
},
+ "headless_buy_error": {
+ "title": "Fiat-Kauf fehlgeschlagen",
+ "message": "Bei Ihrem Fiat-Kauf ist etwas schiefgelaufen. Bitte versuchen Sie es erneut."
+ },
"mmpay_hardware_account": {
"title": "Wallet wird nicht unterstützt",
"message": "Hardware-Wallets werden nicht unterstützt.\nWechseln Sie die Wallets, um fortzufahren."
@@ -133,8 +137,8 @@
"message": "Die Empfängeradresse unterstützt womöglich keine direkten Token-Übertragungen, was zu Geldverlusten führen kann. Fahren Sie nur fort, wenn Sie sicher sind, dass dieser Contract Ihre Übertragung empfangen kann."
},
"gas_sponsorship_reserve_balance": {
- "message": "Gas-Sponsoring ist für diese Transaktion nicht verfügbar. Sie benötigen mindestens %{minBalance} %{nativeTokenSymbol} auf Ihrem Konto.",
- "title": "Gas-Sponsoring nicht verfügbar"
+ "message": "Dieses spezifische Netzwerk erfordert, dass Sie eine Reserve von %{minBalance} %{nativeTokenSymbol} auf Ihrem Konto aufrechterhalten.",
+ "title": "Reservesaldo ist erforderlich"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "Sie senden Tokens an die Kontraktadresse des Tokens. Dies kann zum Verlust dieser Tokens führen.",
"smart_contract_address": "Smart-Contract-Adresse",
"smart_contract_address_warning": "Die Empfängeradresse unterstützt womöglich keine direkten Token-Übertragungen, was zu Geldverlusten führen kann. Fahren Sie nur fort, wenn Sie sicher sind, dass dieser Contract Ihre Übertragung empfangen kann.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "Update",
"i_understand": "Ich verstehe",
"cancel": "Stornieren",
"new_address_title": "Neue Adresse",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "Wert",
"pnl_percent": "GuV %",
- "recent": "Recent"
+ "recent": "Aktuell"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "Handeln Sie {{symbol}}-Perp",
"subtitle": "Vervielfachen Sie Ihre GuV bis zu {{leverage}}"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "Support kontaktieren"
+ },
"today": "Heute",
"yesterday": "Gestern",
"unrealized_pnl": "Nicht realisierte GuV",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC wurden Ihre Wallet hinzugefügt",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} wurde Ihrer Wallet hinzugefügt",
"toast_error_title": "Hoppla! Etwas ist schiefgelaufen ...",
- "toast_error_description": "Fortfahren mit Auszahlung fehlgeschlagen"
+ "toast_error_description": "Fortfahren mit Auszahlung fehlgeschlagen",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Erneut versuchen"
},
"quote": {
"network_fee": "Netzwerk-Gebühr",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "MetaMask Predictions",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "Weltmeisterschaft",
+ "banner_title": "Weltmeisterschaft 2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
+ "all": "Alle",
"props": "Props",
"live": "Live"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
- "final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_stage": "Gruppenphase",
+ "round_of_32": "Sechzehntelfinale",
+ "round_of_16": "Achtelfinale",
+ "quarterfinals": "Viertelfinale",
+ "semifinals": "Halbfinale",
+ "third_place": "Dritter Platz",
+ "final": "Finale",
+ "group_a": "Gruppe A",
+ "group_b": "Gruppe B",
+ "group_c": "Gruppe C",
+ "group_d": "Gruppe D",
+ "group_e": "Gruppe E",
+ "group_f": "Gruppe F",
+ "group_g": "Gruppe G",
+ "group_h": "Gruppe H",
+ "group_i": "Gruppe I",
+ "group_j": "Gruppe J",
+ "group_k": "Gruppe K",
+ "group_l": "Gruppe L",
+ "third_place_match": "Spiel um Platz Drei"
}
},
"prediction_markets": "Prognosemärkte",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "Auszahlung abgeschlossen",
"withdraw_completed_subtitle": "{{amount}} USDC wurden Ihre Wallet hinzugefügt",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}} wurde Ihrer Wallet hinzugefügt",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "Auszahlungen vorübergehend nicht verfügbar",
+ "unavailable_description": "Für dringende Unterstützung wenden Sie sich bitte an den Kundendienst.",
"unavailable_got_it": "Verstanden",
"error_title": "Hoppla! Etwas ist schiefgelaufen ...",
"error_description": "Fortfahren mit Auszahlung fehlgeschlagen",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "Akzeptieren und schließen",
"pna25_open_settings_button": "Einstellungen öffnen"
},
+ "onboarding_interest_questionnaire": {
+ "title": "Was möchten Sie mit MetaMask tun?",
+ "description": "Wählen Sie alles Zutreffende.",
+ "option_buy_and_sell_crypto": "Krypto kaufen und verkaufen",
+ "option_consolidate_wallets": "Fassen Sie Ihre Wallets zusammen",
+ "option_advanced_trades": "Tätigen Sie fortschrittliche Trades",
+ "option_predict_sports_events": "Tippen Sie auf Sport und Events",
+ "option_crypto_as_money": "Verwenden Sie Krypto als Geld",
+ "option_connect_apps_sites": "Verbinden Sie sich mit Apps oder Websites",
+ "continue": "Fortfahren"
+ },
"template_confirmation": {
"ok": "OK",
"cancel": "Stornieren"
@@ -3261,8 +3286,27 @@
"notifications_desc": "Verwalten Sie Ihre Benachrichtigungen",
"allow_notifications": "Benachrichtigungen erlauben",
"enable_push_notifications": "Push-Benachrichtigungen aktivieren",
- "allow_notifications_desc": "Bleiben Sie mit Benachrichtigungen stets darüber auf dem Laufenden, was in Ihrer Wallet passiert. Zur Nutzung von Benachrichtigungen verwenden wir ein Profil, um bestimmte Einstellungen auf Ihren Geräten zu synchronisieren.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "Aus",
+ "select_all": "Alle auswählen",
+ "deselect_all": "Alle abwählen",
+ "select_accounts_title": "Konten",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "Passen Sie Ihre Benachrichtigungen an",
"customize_session_desc": "Schalten Sie die Arten von Benachrichtigungen ein, die Sie erhalten möchten:",
"account_session_title": "Kontoaktivität",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snaps",
"snaps_desc": "Neue Features und Updates",
"products_announcements_title": "Produktankündigungen",
- "products_announcements_desc": "Neue Produkte und Features",
- "perps_title": "Perps-Trading"
+ "products_announcements_desc": "Neue Produkte und Features"
},
"contacts_title": "Kontakte",
"contacts_desc": "Konten hinzufügen, bearbeiten, entfernen und verwalten.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "Karte",
"reset_onboarding_description": "Setzen Sie den Onboarding-Status der Karte zurück, um den Onboarding-Prozess von vorn zu beginnen.",
- "reset_onboarding_button": "Onboarding-Status zurücksetzen"
+ "reset_onboarding_button": "Onboarding-Status zurücksetzen",
+ "unlink_money_account_description": "Widerrufen Sie die USDC-Ausgabelimit-Erlaubnis, die der Karte gestattet, Ausgaben über Ihr Geldkonto zu tätigen. Dies übermittelt eine approve(0)-Transaktion und markiert das Geldkonto im Backend der Karte als „nicht delegiert“.",
+ "unlink_money_account_button": "Geldkonto von der Karte trennen",
+ "unlink_money_account_disabled_hint": "Keine aktive Verknüpfung zum Entfernen vorhanden."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "Haptik",
@@ -3842,8 +3893,8 @@
"predict_button": "Prognosen",
"add_collectible_button": "Hinzufügen",
"info": "Info",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "Batch-Verkauf",
+ "batch_sell_new_label": "Neu",
"swap": "Swappen",
"convert": "Konvertieren",
"bridge": "Bridge",
@@ -3883,7 +3934,7 @@
"troubleshoot": "Problemlösung",
"deposit_description": "Kostengünstige Bank- oder Kartenüberweisung",
"buy_description": "Ideal für den Kauf eines speziellen Tokens",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "Verkaufen Sie bis zu 5 Token gegen einen Stablecoin",
"sell_description": "Krypto für Bargeld verkaufen",
"swap_description": "Tausch zwischen Token",
"bridge_description": "Tokens zwischen Netzwerken übertragen",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "Einstellungen > Benachrichtigungen.",
"cancel": "Stornieren",
"cta": "Einschalten"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "Verpassen Sie keine Bewegung",
+ "body": "Erhalten Sie Echtzeit-Benachrichtigungen, sobald Preise Ihre Zielwerte erreichen, Ihre Trades bestätigt werden und sich Ihr Portfolio verändert. Dazu gibt es Produkt-Updates und Belohnungen. Wir senden Ihnen Updates, die auf Ihren Interaktionen mit MetaMask basieren.",
+ "button_yes": "Ja",
+ "button_not_now": "Nicht jetzt",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "jetzt",
+ "title": "ETH ist heute um 4,2 % gestiegen",
+ "message": "Jetzt bei 2.668,51 $ – über Ihrer Preisbenachrichtigung"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "Vor 1 Std.",
+ "title": "0,25 ETH empfangen",
+ "message": "Von 0x9a21…4f8c · 640,29 $"
+ }
+ },
+ "existing_user": {
+ "title": "Vorstellung von personalisierten Benachrichtigungen",
+ "body": "Erhalten Sie Mitteilungen, die zu Ihrer Handelsweise passen. Jederzeit anpassbar.",
+ "card_title": "Was Sie erhalten werden",
+ "card_description": "Personalisierte Benachrichtigungen und Updates, zugeschnitten auf Ihre Handelsaktivitäten.",
+ "button_confirm": "Bestätigen",
+ "button_not_now": "Nicht jetzt"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "Bezahlen mit",
"buying_via": "Kauf über {{providerName}}.",
"change_provider": "Anbieter wechseln.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "Etwas ist schiefgelaufen! Bitte versuchen Sie es erneut.",
"no_payment_methods_available": "Es stehen keine Zahlungsmethoden zur Verfügung.",
"error_fetching_quotes": "Etwas ist schiefgelaufen! Bitte versuchen Sie es erneut.",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "Zu mUSD konvertieren",
"get_a_percentage_musd_bonus": "Erhalten Sie {{percentage}} % mUSD-Bonus",
"convert": "Konvertieren",
+ "confirm": "Bestätigen",
+ "convert_tooltip_description": "Konvertieren Sie Ihre Stablecoins in mUSD und verdienen Sie einen Bonus von bis zu {{percentage}} % auf jährlicher Basis, den Sie täglich einfordern können. Angetrieben von Relay.",
"fetching_quote": "Angebot einholen...",
"you_convert": "Sie konvertieren",
"network_fee": "Netzwerkgebühr",
@@ -6597,7 +6679,7 @@
"step_progress": "Schritt {{current}} von {{total}}",
"title": "Geld aufladen",
"description": "Laden Sie Ihr Konto auf und beginnen Sie, Effektivvertrag zu verdienen.",
- "add": "Hinzufügen",
+ "add": "Gelder hinzufügen",
"step2_title": "Sichern Sie sich Ihre MetaMask Card",
"step2_description": "Geben Sie Ihr Geldguthaben aus, während es Gewinne erwirtschaftet, überall dort, wo Mastercard akzeptiert wird.",
"step2_cta": "Karte erhalten",
@@ -6605,6 +6687,19 @@
"link_card_description": "Geben Sie Ihr Guthaben aus, während es Gewinne erwirtschaftet, überall dort, wo Mastercard akzeptiert wird.",
"link_card_cta": "Karte verknüpfen"
},
+ "rive_onboarding": {
+ "step1_title": "Geldkonten sind hier",
+ "step1_body": "Verdienen Sie bis zu {{percentage}} % Effektivertrag auf Ihr Saldo – verfügbar für Ihre gesamte Wallet.",
+ "step1_footer_text": "Der Effektivertrag ist variabel und kann sich jederzeit ändern.",
+ "step2_title": "Automatisch verdienen",
+ "step2_body": "Bewegen Sie Stablecoins ohne Tauschgebühren. Ihre Gelder beginnen sofort, Erträge zu erwirtschaften.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "Überall ausgeben",
+ "step3_body": "Erhalten Sie bis zu {{percentage}} % auf Ihre Einkäufe zurück, indem Sie Ihr Geldkonto mit einer MetaMask-Karte verknüpfen.",
+ "step4_title": "Handeln und verdienen an einem Ort",
+ "step4_body": "Nutzen Sie Ihren Geldsaldo, um über MetaMask zu handeln und dabei weiterhin Erträge zu erzielen.",
+ "continue": "Fortfahren"
+ },
"action": {
"add": "Hinzufügen",
"transfer": "Übertragung",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "Wie es funktioniert",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "Zahlen Sie mUSD auf Ihr Geldknto ein und verdienen Sie bis zu",
+ "description_suffix": ". Ihr Saldo ist durch US-Dollar gedeckt und jederzeit bereit zum Ausgeben, Handeln oder Versenden."
},
"musd_row": {
"add": "Hinzufügen"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Money-Saldo",
"add": "Hinzufügen",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Money-Saldo",
+ "info_sheet_body": "Ihr durch US-Dollar gedecktes mUSD-Saldo steht Ihnen jederzeit zum Ausgeben, Senden oder Handeln zur Verfügung. Wir rechnen dies nicht in Ihren gesamten Kontostand ein.\n\nAuszahlungen werden umgehend bearbeitet, vorbehaltlich der üblichen Bestätigungszeiten des jeweiligen Blockchain-Netzwerks. Bei geringer Liquidität kann es zu vorübergehenden Verzögerungen kommen."
},
"potential_earnings": {
"title": "Verdienen Sie an Ihrem Krypto",
"description": "Beobachten Sie, wie Ihr Geld mit der Zeit wächst, indem Sie Ihre Kryptowährung in mUSD konvertieren.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "Wandeln Sie Ihr {{total}} in Vermögenswerte um und Sie könnten verdienen bis zu",
+ "description_with_amounts_suffix": "in einem Jahr.",
"convert": "Konvertieren",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "Wandeln Sie Ihr Krypto um",
"no_fee": "Keine MetaMask-Gebühr",
"view_all": "Alle anzeigen",
"view_potential_earnings": "Mögliche Einnahmen anzeigen"
@@ -6649,41 +6744,44 @@
"cashback": "{{percentage}} % mUSD zurück",
"get_now": "Jetzt sichern",
"link_title": "MetaMask Card verknüpfen",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "Geben Sie Ihren Geldsaldo aus und verdienen Sie bei Einkäufen. Plus: bis zu {{apy}} % Effektivertrag auf Ihr Guthaben.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "Erhalten Sie {{percentage}} % mUSD zurück",
+ "link_bullet_apy": "Verdienen Sie bis zu {{apy}} % Effektivertrag",
"link_card": "Karte verknüpfen",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "Ausgeben und verdienen",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "Karte verknüpfen",
+ "manage_card": "Verwalten",
+ "avail_balance": "Verfügbares Guthaben"
},
"what_you_get": {
"title": "Was Sie erhalten",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "Automatisch verdienen bis zu",
"benefit_dollar_backed": "Halten Sie Ihr Geld sicher in mUSD, einem 1:1 durch den Dollar gedeckten Stablecoin",
"benefit_liquidity": "Erhalten Sie volle Liquidität ohne Sperrfristen, sodass Sie jederzeit handeln oder auszahlen können",
"benefit_spend_prefix": "Geben Sie bei über 150 Mio. Händlern mit MetaMask Card aus und verdienen Sie dabei ",
"benefit_spend_cashback": "1 bis 3 % mUSD zurück",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "Überweisen Sie an jede Ihrer Wallets über MetaMask",
+ "benefit_global": "Senden und empfangen Sie Geld weltweit",
"learn_more": "Mehr erfahren"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "Gelder hinzufügen"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "Gelder hinzufügen",
"convert_crypto": "Krypto umwandeln",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "Von jedem Konto",
"deposit_funds": "Gelder einzahlen",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "Von Debitkarte oder Bank",
+ "move_musd": "Überweisen Sie Ihre {{amount}} mUSD",
+ "move_musd_no_amount": "Überweisen Sie Ihre mUSD",
+ "move_musd_description": "Von Ihrem Saldo",
"receive_external": "Von externer Wallet erhalten",
"coming_soon": "Demnächst verfügbar"
},
@@ -6694,7 +6792,7 @@
"contact_support": "Support kontaktieren"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "Geld überweisen",
"between_accounts": "Zwischen Konten",
"perps_account": "Perps-Konto",
"predictions_account": "Predictions-Konto",
@@ -6712,8 +6810,8 @@
"body": "Eine Schätzung, wie viel Sie über einen bestimmten Zeitraum basierend auf Ihrem aktuellen Saldo und dem heutigen Effektivvertrag verdienen könnten. Bei diesen Schätzungen handelt es sich nicht um garantierte Erträge; sie bleiben vorbehaltlich etwaiger Änderungen."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "Verdienen Sie an Ihrem Krypto",
+ "body": "Die Beispielrechnung geht davon aus, dass der Effektivertrag von {{percentage}} % über einen Zeitraum von einem Jahr unverändert bleibt. Der Effektivertrag ist variabel und kann sich aufgrund verschiedener Faktoren ändern. Es besteht keine Renditegarantie."
},
"activity": {
"title": "Aktivität",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "Wie es funktioniert",
- "how_it_works_subtitle": "Erleben Sie, wie Ihr Geld für Sie arbeitet",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "Erfahren Sie mehr über mUSD",
"what_you_get_title": "Was Sie erhalten",
@@ -6738,7 +6836,8 @@
"sent": "Versendet",
"transferred": "Überwiesen",
"card_transaction": "Kartentransaktion",
- "converted": "Konvertiert"
+ "converted": "Konvertiert",
+ "failed": "Fehlgeschlagen"
},
"convert_stablecoins": {
"title": "Konvertieren Sie Ihre Stablecoins",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "Money",
"section_title": "Wie es funktioniert",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "Zahlen Sie mUSD auf Ihr Geldkonto ein und erhalten Sie automatisch bis zu {{percentage}} % Effektivertrag (variabel). Das Geld fließt in einen DeFi-Tresor, der auf geprüften Kreditmärkten Renditen erzielt – ganz ohne Staking, ohne Auszahlungsanträge und ohne Sperrfristen.",
"description_2": "Ihr Geldguthaben ist Ihr Ausgabensaldo. Verknüpfen Sie Ihre MetaMask Card, um bei mehr als 150 Millionen Händlern weltweit zu bezahlen. Ihr Geld verdient so lange, bis Sie es nutzen.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "Häufig gestellte Fragen",
"faq_placeholder_answer": "Demnächst verfügbar.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "Wie funktioniert der {{percentage}}-%-Effektivertrag?",
"faq_q2": "Was ist mUSD?",
"faq_q3": "Woher kommt die Rendite?",
"faq_q4": "Ist mein Geld gesperrt? Kann ich es jederzeit auszahlen lassen?",
@@ -7030,11 +7129,12 @@
"confirm": "Bestätigen",
"pay_with_bottom_sheet": {
"title": "Bezahlen mit",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "Zuletzt verwendet",
+ "bank_and_card": "Bank und Karte",
+ "crypto": "Krypto",
+ "available_balance": "{{balance}} verfügbar",
+ "other_assets": "Weitere Vermögenswerte",
+ "other_assets_description": "Wählen Sie aus Ihren Token"
},
"staking_footer": {
"part1": "Indem Sie fortfahren, bestätigen Sie unsere ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "Die Gebühren für die mUSD-Konvertierung beinhalten Netzwerkkosten und können Anbietergebühren beinhalten. Bei der Konvertierung in mUSD fällt keine MetaMask-Gebühr an."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMask tauscht Ihre mUSD gegen das von Ihnen gewünschte Token. Swap-Anbieter können Gebühren erheben, MetaMask jedoch nicht."
},
"title": {
"transaction_fee": "Gebühren"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "Gelder hinzufügen",
"deposit_edit_amount_predict_withdraw": "Auszahlen",
"deposit_edit_amount_musd_conversion": "Zu mUSD konvertieren",
- "preparing_order": "Preparing order"
+ "preparing_order": "Order wird vorbereitet"
},
"change_in_simulation_modal": {
"title": "Ergebnisse haben sich geändert",
@@ -7250,20 +7350,33 @@
"confirm_swap": "Swap",
"terms_and_conditions": "Geschäftsbedingungen",
"select_token": "Token auswählen",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "Wählen Sie bis zu 5 Token",
+ "batch_sell_select_subtitle": "Alle Token müssen sich im selben Netzwerk befinden.",
+ "batch_sell_empty_state_title": "Keine Token. Kein Problem.",
+ "batch_sell_empty_state_description": "Keine Token. Kein Problem. Entdecken und kaufen Sie Token für den Batch-Verkauf.",
+ "batch_sell_continue_with_one_token": "Mit (1) Token fortfahren",
+ "batch_sell_continue_with_tokens": "Mit ({{tokenCount}}) Token fortfahren",
+ "batch_sell_max_tokens_allowed": "Max. 5 Token erlaubt",
+ "batch_sell_single_token_dialog_title": "Hochpreisbenachrichtigung",
+ "batch_sell_single_token_dialog_description": "Batch-Verkauf von einem Token könnte zu einem höheren Preis führen. Möchten Sie stattdessen swappen?",
+ "batch_sell_swap_instead": "Ja, swappen",
+ "batch_sell_review_title": "Batch-Verkauf-Review",
+ "batch_sell_select_stablecoin": "Stablecoin wählen",
+ "batch_sell_total_received": "Insgesamt empfangen",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "Überprüfen",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "{{tokenSymbol}} anpassen",
+ "batch_sell_remove_token": "{{tokenSymbol}} entfernen",
+ "batch_sell_checkbox_label": "Batch-Verkauf-Token",
+ "sort_balance": "Kontostand",
+ "next": "Weiter",
+ "explore_tokens": "Token erkunden",
"no_tokens_found": "Keine Tokens gefunden",
"no_tokens_found_description": "Wir konnten keine Tokens mit diesem Namen finden. Versuchen Sie eine andere Suche.",
"select_network": "Netzwerk wählen",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "Sie verlieren bei diesem Swap etwa {{priceImpact}} des Wertes Ihres Tokens. Versuchen Sie, den Betrag zu senken oder eine liquidere Route zu wählen.",
"proceed": "Fortfahren",
"cancel": "Stornieren",
+ "close": "Schließen",
"slippage_info_title": "Slippage",
"slippage_info_description": "Die prozentuale Preisänderung, die Sie zuzulassen bereit sind, bevor Ihre Transaktion storniert wird.",
"blockaid_error_title": "Diese Transaktion wird rückgängig gemacht",
@@ -8107,7 +8221,14 @@
"retry": "Erneut versuchen",
"on_linea": "auf Linea",
"account_label": "Konto",
- "token_label": "Token"
+ "token_label": "Token",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "mUSD zurück",
@@ -8335,7 +8456,7 @@
},
"main_title": "Belohnungen",
"vip": {
- "bps_unit": "bps",
+ "bps_unit": "Basispunkte",
"swaps_label": "Swaps",
"perps_label": "Perps",
"error_title": "We couldn’t load your VIP dashboard",
@@ -8445,6 +8566,7 @@
"show_less": "Weniger anzeigen",
"linking_progress": "Konten werden hinzufügen ... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} angemeldet",
+ "accounts_added": "Accounts added",
"add_all_accounts": "Alle Konten hinzufügen",
"environment_selector": "Umgebung",
"environment_cancel": "Stornieren",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "Rewards-Fortschritt löschen",
- "description": "Dadurch werden Ihre Konten aus dem Rewards-Programm entfernt und Ihr Fortschritt gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
- "confirm": "Löschen",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "Sind Sie sicher?",
- "confirmation_description": "Dadurch wird Ihr gesamter Fortschritt gelöscht und kann nicht rückgängig gemacht werden. Wenn Sie später wieder am Belohnungsprogramm teilnehmen, beginnen Sie wieder bei 0.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "Stornieren",
"confirm": "Bestätigen",
+ "error_title": "Hoppla! Etwas ist schiefgelaufen ...",
"error_message": "Abmeldung von Belohnungen fehlgeschlagen. Bitte versuchen Sie es erneut.",
"processing": "Verarbeitung..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "Verwerfen",
@@ -8562,7 +8691,8 @@
"title_claim": "Vorteil einfordern",
"action": "Einfordern",
"empty-list": "Sie haben derzeit keine Vorteile.",
- "powered_by": "Unterstützt durch"
+ "powered_by": "Unterstützt durch",
+ "available_count": "{{count}} verfügbar"
},
"end_of_season_rewards": {
"confirm_label_default": "Bestätigen",
@@ -8695,7 +8825,7 @@
"label_your_rank": "Ihr Rang",
"label_portfolio": "Portfolio",
"label_net_inflow": "Nettozufluss",
- "label_total_inflow": "Gesamtzufluss",
+ "label_total_inflow": "Gesamtzustrom",
"label_outflow": "Kapitalabfluss",
"label_days_held": "Tage gehalten",
"qualified_title": "Sie sind qualifiziert",
@@ -8880,9 +9010,11 @@
"musd_claim": "mUSD beanspruchen",
"perps_deposit": "Gelder hinzufügen",
"perps_withdraw": "Auszahlung",
+ "predict_withdraw": "{{sourceSymbol}} von {{sourceChain}} abheben",
"predict_deposit": "Gelder hinzufügen",
"swap": "Tokens tauschen",
- "swap_approval": "Tokens genehmigen"
+ "swap_approval": "Tokens genehmigen",
+ "fiat_purchase": "{{token}} mit {{paymentMethod}} kaufen"
},
"perps_deposit_solution": "Sie haben nun {{fiat}} von USDC auf Arbitrum. Versuchen Sie Ihre Einzahlung erneut."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "Hoch bis niedrig",
"low_to_high": "Niedrig bis hoch",
"apply": "Anwenden",
- "search_placeholder": "Tokens, Websites, URLs suchen",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "Stornieren",
"perps": "Perps",
"rwa_perps_section": "Perps",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "Perps",
"predictions": "Prognosen",
"no_results": "Keine Ergebnisse gefunden",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "Beliebt",
"sites": "Websites",
"popular_sites": "Beliebte Websites",
"search_sites": "Websites durchsuchen",
"view_all": "Alle anzeigen",
+ "view_more": "Mehr anzeigen",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "Grundlegende Funktionen aktivieren",
"basic_functionality_disabled_title": "Entdecken ist nicht verfügbar",
"basic_functionality_disabled_description": "Die erforderlichen Metadaten können nicht abgerufen werden, wenn die Basisfunktionalität deaktiviert ist.",
@@ -8995,6 +9131,14 @@
"crypto": "Krypto",
"sports": "Sport",
"dapps": "Websites"
+ },
+ "search_tabs": {
+ "all": "Alle",
+ "crypto": "Cryptos",
+ "perps": "Perps",
+ "stocks": "Aktien",
+ "predictions": "Prognosen",
+ "sites": "Websites"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "Kein mUSD in diesem Netzwerk. Wechseln Sie das Netzwerk, um Ihre mUSD einzusehen.",
"money_empty_state": {
"get_started": "Erste Schritte",
+ "earn": "Verdienen",
"earn_apy": "{{percentage}} % Effektivertrag verdienen"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "Zugehörige Assets",
"perpetuals": "Perpetuals",
"predictions": "Prognosen",
- "whats_happening": "Was ist passiert?",
- "whats_happening_ai": "KI",
- "whats_happening_impact": {
- "bullish": "Haussierend",
- "bearish": "Baissierend",
- "neutral": "Neutral"
- },
"top_traders": "Top-Trader",
- "whats_happening_categories": {
- "geopolitical": "Geopolitisch",
- "macro": "Makro",
- "regulatory": "Regulatorisch",
- "technical": "Technisch",
- "social": "Sozial",
- "other": "Sonstiges"
- },
"defi": "DeFi",
"nfts": "NFTs",
"trending_tokens": "Angesagt",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "Beliebt"
+ },
+ "whats_happening": {
+ "title": "Was ist passiert?",
+ "ai": "KI",
+ "impact": {
+ "bullish": "Haussierend",
+ "bearish": "Baissierend",
+ "neutral": "Neutral"
+ },
+ "categories": {
+ "geopolitical": "Geopolitisch",
+ "macro": "Makro",
+ "regulatory": "Regulatorisch",
+ "technical": "Technisch",
+ "social": "Sozial",
+ "other": "Sonstiges"
+ }
}
}
diff --git a/locales/languages/el.json b/locales/languages/el.json
index 285ce4074809..8f0a88fdfbad 100644
--- a/locales/languages/el.json
+++ b/locales/languages/el.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "Δεν υπάρχουν διαθέσιμα χρήματα. Δοκιμάστε με άλλον λογαριασμό."
},
+ "headless_buy_error": {
+ "title": "Η αγορά με παραδοσιακό νόμισμα απέτυχε",
+ "message": "Παρουσιάστηκε πρόβλημα με την αγορά σας με παραδοσιακό νόμισμα. Προσπαθήστε ξανά."
+ },
"mmpay_hardware_account": {
"title": "Το πορτοφόλι δεν υποστηρίζεται",
"message": "Τα πορτοφόλια υλικού δεν υποστηρίζονται. \nΑλλάξτε πορτοφόλι για να συνεχίσετε."
@@ -133,8 +137,8 @@
"message": "Η διεύθυνση παραλήπτη ενδέχεται να μην υποστηρίζει άμεσες μεταφορές tokens, γεγονός που μπορεί να οδηγήσει σε απώλεια κεφαλαίων. Συνεχίστε μόνο εάν είστε βέβαιοι ότι το συγκεκριμένο συμβόλαιο μπορεί να λάβει τη μεταφορά."
},
"gas_sponsorship_reserve_balance": {
- "message": "Η κάλυψη των τελών δεν είναι διαθέσιμη για αυτή τη συναλλαγή. Θα χρειαστεί να διατηρείτε τουλάχιστον %{minBalance} %{nativeTokenSymbol} στον λογαριασμό σας.",
- "title": "Η κάλυψη των τελών δεν είναι διαθέσιμη"
+ "message": "Αυτό το συγκεκριμένο δίκτυο απαιτεί τη διατήρηση ενός αποθεματικού ύψους %{minBalance} %{nativeTokenSymbol} στον λογαριασμό σας.",
+ "title": "Απαιτείται υπόλοιπο ως αποθεματικό"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "Πρόκειται να στείλετε tokens στη διεύθυνση συμβολαίου του token. Αυτό μπορεί να οδηγήσει σε απώλεια των token.",
"smart_contract_address": "Διεύθυνση έξυπνου συμβολαίου",
"smart_contract_address_warning": "Η διεύθυνση παραλήπτη ενδέχεται να μην υποστηρίζει άμεσες μεταφορές tokens, γεγονός που μπορεί να οδηγήσει σε απώλεια κεφαλαίων. Συνεχίστε μόνο εάν είστε βέβαιοι ότι το συγκεκριμένο συμβόλαιο μπορεί να λάβει τη μεταφορά.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "Ενημέρωση",
"i_understand": "Κατανοώ",
"cancel": "Άκυρο",
"new_address_title": "Νέα διεύθυνση",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "Αξία",
"pnl_percent": "Κ&Ζ %",
- "recent": "Recent"
+ "recent": "Πρόσφατα"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "Κάντε συναλλαγές με perp του {{symbol}}",
"subtitle": "Πολλαπλασιάστε τα κέρδη και τις ζημίες σας έως και {{leverage}}"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "Επικοινωνία με την υποστήριξη"
+ },
"today": "Σήμερα",
"yesterday": "Χθες",
"unrealized_pnl": "Μη οριστικοποιημένα κέρδη & ζημίες",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC μεταφέρθηκαν στο πορτοφόλι σας",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} μεταφέρθηκαν στο πορτοφόλι σας",
"toast_error_title": "Κάτι πήγε στραβά",
- "toast_error_description": "Δεν ήταν δυνατή η συνέχιση της ανάληψης"
+ "toast_error_description": "Δεν ήταν δυνατή η συνέχιση της ανάληψης",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Προσπαθήστε ξανά"
},
"quote": {
"network_fee": "Τέλη δικτύου",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "Προβλέψεις στο MetaMask",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "Παγκόσμιο Κύπελλο",
+ "banner_title": "Παγκόσμιο Κύπελλο 2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
+ "all": "Όλα",
"props": "Props",
- "live": "Live"
+ "live": "Ζωντανά"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
- "final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_stage": "Φάση ομίλων",
+ "round_of_32": "Φάση των 32",
+ "round_of_16": "Φάση των 16",
+ "quarterfinals": "Προημιτελικοί",
+ "semifinals": "Ημιτελικοί",
+ "third_place": "Μικρός τελικός",
+ "final": "Λήξη",
+ "group_a": "Όμιλος Α",
+ "group_b": "Όμιλος Β",
+ "group_c": "Όμιλος Γ",
+ "group_d": "Όμιλος Δ",
+ "group_e": "Όμιλος Ε",
+ "group_f": "Όμιλος ΣΤ",
+ "group_g": "Όμιλος Ζ",
+ "group_h": "Όμιλος Η",
+ "group_i": "Όμιλος Ι",
+ "group_j": "Όμιλος Θ",
+ "group_k": "Όμιλος Κ",
+ "group_l": "Όμιλος Λ",
+ "third_place_match": "Αγώνας για την 3η θέση"
}
},
"prediction_markets": "Αγορές προβλέψεων",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "Η ανάληψη ολοκληρώθηκε",
"withdraw_completed_subtitle": "{{amount}} USDC μεταφέρθηκαν στο πορτοφόλι σας",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}} μεταφέρθηκαν στο πορτοφόλι σας",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "Οι αναλήψεις είναι προσωρινά μη διαθέσιμες",
+ "unavailable_description": "Για επείγουσα βοήθεια, επικοινωνήστε με την Εξυπηρέτηση Πελατών.",
"unavailable_got_it": "Κατανοητό",
"error_title": "Κάτι πήγε στραβά",
"error_description": "Δεν ήταν δυνατή η συνέχιση της ανάληψης",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "Αποδοχή και κλείσιμο",
"pna25_open_settings_button": "Άνοιγμα Ρυθμίσεων"
},
+ "onboarding_interest_questionnaire": {
+ "title": "Τι θέλετε να κάνετε στο MetaMask;",
+ "description": "Επιλέξτε όλα όσα ισχύουν.",
+ "option_buy_and_sell_crypto": "Αγορά και πώληση κρυπτονομισμάτων",
+ "option_consolidate_wallets": "Συγχώνευση πορτοφολιών",
+ "option_advanced_trades": "Εκτέλεση προχωρημένων συναλλαγών",
+ "option_predict_sports_events": "Προβλέψεις για αθλητικά γεγονότα",
+ "option_crypto_as_money": "Χρήση κρυπτονομισμάτων ως μέσο πληρωμής",
+ "option_connect_apps_sites": "Σύνδεση με εφαρμογές ή ιστότοπους",
+ "continue": "Συνεχίστε"
+ },
"template_confirmation": {
"ok": "Εντάξει",
"cancel": "Άκυρο"
@@ -3261,8 +3286,27 @@
"notifications_desc": "Διαχειριστείτε τις ειδοποιήσεις σας",
"allow_notifications": "Επιτρέψτε τις ειδοποιήσεις",
"enable_push_notifications": "Ενεργοποίηση ειδοποιήσεων ώθησης",
- "allow_notifications_desc": "Μείνετε ενήμεροι για ό, τι συμβαίνει στο πορτοφόλι σας με τις ειδοποιήσεις. Για να χρησιμοποιήσετε τις ειδοποιήσεις, κάνουμε χρήση ενός προφίλ για να συγχρονίσουμε ορισμένες ρυθμίσεις στις συσκευές σας.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "Απεν.",
+ "select_all": "Επιλογή όλων",
+ "deselect_all": "Αποεπιλογή όλων",
+ "select_accounts_title": "Λογαριασμοί",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "Προσαρμόστε τις ειδοποιήσεις σας",
"customize_session_desc": "Ενεργοποιήστε τους τύπους ειδοποιήσεων που θέλετε να λαμβάνετε:",
"account_session_title": "Δραστηριότητα λογαριασμού",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snaps",
"snaps_desc": "Νέες λειτουργίες και ενημερώσεις",
"products_announcements_title": "Ανακοινώσεις προϊόντων",
- "products_announcements_desc": "Νέα προϊόντα και λειτουργίες",
- "perps_title": "Συναλλαγές με συμβόλαια αορίστου διάρκειας"
+ "products_announcements_desc": "Νέα προϊόντα και λειτουργίες"
},
"contacts_title": "Επαφές",
"contacts_desc": "Προσθέστε, επεξεργαστείτε, αφαιρέστε και διαχειριστείτε τους λογαριασμούς σας.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "Κάρτα",
"reset_onboarding_description": "Επαναφορά της διαδικασίας ενεργοποίησης της Κάρτας για να ξεκινήσει η διαδικασία από την αρχή.",
- "reset_onboarding_button": "Επαναφορά της διαδικασίας ενεργοποίησης"
+ "reset_onboarding_button": "Επαναφορά της διαδικασίας ενεργοποίησης",
+ "unlink_money_account_description": "Ανακαλέστε το όριο δαπανών σε USDC που επιτρέπει στην κάρτα να ξοδεύει από τον λογαριασμό σας. Αυτό υποβάλλει μια συναλλαγή έγκρισης(0) και επισημαίνει τον λογαριασμό σας ως μη εξουσιοδοτημένο για ανάθεση στο σύστημα της κάρτας.",
+ "unlink_money_account_button": "Αποσύνδεση του Λογαριασμού από την κάρτα",
+ "unlink_money_account_disabled_hint": "Δεν υπάρχει ενεργή σύνδεση για αφαίρεση."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "Απτική ανάδραση",
@@ -3842,8 +3893,8 @@
"predict_button": "Προβλέψεις",
"add_collectible_button": "Προσθήκη",
"info": "Πληροφορίες",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "Μαζική πώληση",
+ "batch_sell_new_label": "Νέα",
"swap": "Ανταλλαγή",
"convert": "Μετατροπή",
"bridge": "Διασύνδεση",
@@ -3883,7 +3934,7 @@
"troubleshoot": "Αντιμετώπιση Προβλημάτων",
"deposit_description": "Τραπεζική μεταφορά ή με κάρτα με χαμηλή χρέωση",
"buy_description": "Κατάλληλο για την αγορά συγκεκριμένου token",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "Πουλήστε έως 5 tokens για ένα stablecoin",
"sell_description": "Πουλήστε κρυπτονομίσματα για μετρητά",
"swap_description": "Ανταλλαγή μεταξύ tokens",
"bridge_description": "Μεταφορά token μεταξύ δικτύων",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "Ρυθμίσεις > Ειδοποιήσεις.",
"cancel": "Άκυρο",
"cta": "Ενεργοποίηση"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "Μην χάνετε καμία κίνηση της αγοράς",
+ "body": "Λάβετε ειδοποιήσεις σε πραγματικό χρόνο όταν οι τιμές επιτυγχάνουν τους στόχους σας, οι συναλλαγές σας επιβεβαιώνονται και το χαρτοφυλάκιό σας μεταβάλλεται. Επιπλέον, θα λαμβάνετε ενημερώσεις προϊόντος και ανταμοιβές. Θα σας στέλνουμε ενημερώσεις βάσει των αλληλεπιδράσεών σας με το MetaMask.",
+ "button_yes": "Ναι",
+ "button_not_now": "Όχι τώρα",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "τώρα",
+ "title": "Το ETH έχει ανέβει 4,2% σήμερα",
+ "message": "Τώρα στα $2.668,51 — πάνω από το όριο ειδοποίησής σας"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "1 ώρα πριν",
+ "title": "Λάβατε 0,25 ETH",
+ "message": "Από 0x9a21…4f8c · $640,29"
+ }
+ },
+ "existing_user": {
+ "title": "Παρουσιάζουμε τις εξατομικευμένες ειδοποιήσεις",
+ "body": "Λάβετε ειδοποιήσεις σύμφωνα με τον τρόπο που κάνετε συναλλαγές. Μπορείτε να τις αλλάξετε οποιαδήποτε στιγμή.",
+ "card_title": "Τι θα λάβετε",
+ "card_description": "Εξατομικευμένες ειδοποιήσεις και ενημερώσεις προσαρμοσμένες στη δραστηριότητά σας στις συναλλαγές.",
+ "button_confirm": "Επιβεβαίωση",
+ "button_not_now": "Όχι τώρα"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "Πληρωμή με",
"buying_via": "Αγορά μέσω {{providerName}}.",
"change_provider": "Αλλαγή παρόχου.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "Κάτι πήγε στραβά. Παρακαλούμε προσπαθήστε ξανά.",
"no_payment_methods_available": "Δεν υπάρχουν διαθέσιμες μέθοδοι πληρωμής.",
"error_fetching_quotes": "Κάτι πήγε στραβά. Παρακαλούμε προσπαθήστε ξανά.",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "Μετατροπή σε mUSD",
"get_a_percentage_musd_bonus": "Κερδίστε {{percentage}}% μπόνους σε mUSD",
"convert": "Μετατροπή",
+ "confirm": "Επιβεβαίωση",
+ "convert_tooltip_description": "Μετατρέψτε τα stablecoins σας σε mUSD και κερδίστε έως και {{percentage}}% ετήσιο μπόνους που μπορείτε να εξαργυρώσετε καθημερινά. Με την υποστήριξη της Relay.",
"fetching_quote": "Λήψη προσφοράς...",
"you_convert": "Μετατρέπετε",
"network_fee": "Τέλη δικτύου",
@@ -6597,7 +6679,7 @@
"step_progress": "Βήμα {{current}} από {{total}}",
"title": "Προσθήκη χρημάτων",
"description": "Χρηματοδοτήστε τον λογαριασμό σας και ξεκινήστε να κερδίζετε APY.",
- "add": "Προσθήκη",
+ "add": "Προσθήκη κεφαλαίων",
"step2_title": "Αποκτήστε την MetaMask Card",
"step2_description": "Ξοδέψτε το υπόλοιπο του λογαριασμού σας όσο αυτό αποδίδει, οπουδήποτε γίνεται δεκτή η Mastercard.",
"step2_cta": "Αποκτήστε την κάρτα",
@@ -6605,6 +6687,19 @@
"link_card_description": "Ξοδέψτε το υπόλοιπό του λογαριασμού σας όσο αυτό αποδίδει, οπουδήποτε γίνεται δεκτή η Mastercard.",
"link_card_cta": "Σύνδεση κάρτας"
},
+ "rive_onboarding": {
+ "step1_title": "Οι καταθετικοί λογαριασμοί είναι εδώ",
+ "step1_body": "Κερδίστε έως και {{percentage}}% APY στο υπόλοιπό σας, διαθέσιμο στο πορτοφόλι σας.",
+ "step1_footer_text": "Το APY μεταβάλλεται και μπορεί να αλλάξει ανά πάσα στιγμή.",
+ "step2_title": "Κερδίζετε αυτόματα",
+ "step2_body": "Μεταφέρετε τα stablecoins χωρίς προμήθειες ανταλλαγής. Τα κεφάλαιά σας αρχίζουν να αποδίδουν άμεσα.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "Κάντε αγορές οπουδήποτε",
+ "step3_body": "Κερδίστε έως και {{percentage}}% επιστροφή στις αγορές σας συνδέοντας τον λογαριασμό σας με μια MetaMask Card.",
+ "step4_title": "Κάντε συναλλαγές και κερδίστε σε ένα μέρος",
+ "step4_body": "Χρησιμοποιήστε το υπόλοιπο του λογαριασμού σας για συναλλαγές στο MetaMask, ενώ συνεχίζετε να κερδίζετε σε απόδοση.",
+ "continue": "Συνεχίστε"
+ },
"action": {
"add": "Προσθήκη",
"transfer": "Μεταφορά",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "Πώς λειτουργεί",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "Καταθέστε mUSD στον λογαριασμό σας και κερδίστε έως και",
+ "description_suffix": ". Το υπόλοιπό σας είναι συνδεδεμένο με το δολάριο και έτοιμο για χρήση, συναλλαγές ή αποστολή οποιαδήποτε στιγμή."
},
"musd_row": {
"add": "Προσθήκη"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Υπόλοιπο χρημάτων",
"add": "Προσθήκη",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Υπόλοιπο χρημάτων",
+ "info_sheet_body": "Το υπόλοιπό σας σε mUSD, συνδεδεμένο με το δολάριο, είναι πάντα διαθέσιμο για χρήση, αποστολή ή συναλλαγές. Δεν το υπολογίζουμε στο συνολικό υπόλοιπο του λογαριασμού σας.\n\nΟι αναλήψεις επεξεργάζονται άμεσα, με την επιφύλαξη των συνήθων χρόνων επιβεβαίωσης του δικτύου στο αντίστοιχο blockchain. Αν η ρευστότητα είναι περιορισμένη, ενδέχεται να υπάρξουν προσωρινές καθυστερήσεις."
},
"potential_earnings": {
"title": "Κερδίστε από τα κρυπτονομίσματά σας",
"description": "Δείτε πώς τα χρήματά σας μπορούν να αυξηθούν με τον χρόνο, μετατρέποντας τα κρυπτονομίσματά σας σε mUSD.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "Μετατρέψτε τα περιουσιακά σας στοιχεία αξίας {{total}} και μπορείτε να κερδίσετε έως και",
+ "description_with_amounts_suffix": "σε ένα έτος.",
"convert": "Μετατροπή",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "Μετατρέψτε τα κρυπτονομίσματά σας",
"no_fee": "Χωρίς χρεώσεις από το MetaMask",
"view_all": "Προβολή όλων",
"view_potential_earnings": "Δείτε τα πιθανά κέρδη"
@@ -6649,41 +6744,44 @@
"cashback": "Απόδοση {{percentage}}% σε mUSD",
"get_now": "Αποκτήστε την τώρα",
"link_title": "Σύνδεση της MetaMask Card",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "Ξοδέψτε το υπόλοιπο του λογαριασμού σας και κερδίστε από τις αγορές σας. Επιπλέον, έως και {{apy}}% APY στο υπόλοιπό σας.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "Κερδίστε {{percentage}}% mUSD ως επιστροφή",
+ "link_bullet_apy": "Κερδίστε έως και {{apy}}% APY",
"link_card": "Σύνδεση κάρτας",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "Κάντε αγορές και κερδίστε",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "Σύνδεση κάρτας",
+ "manage_card": "Διαχείριση",
+ "avail_balance": "Διαθέσιμο υπόλοιπο"
},
"what_you_get": {
"title": "Τι σας προσφέρει",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "Αυτόματη απόδοση έως και",
"benefit_dollar_backed": "Κρατήστε τα χρήματά σας ασφαλή σε mUSD, ένα stablecoin με αναλογία 1:1 σε δολάρια",
"benefit_liquidity": "Πλήρης ρευστότητα χωρίς δεσμεύσεις, ώστε να μπορείτε να κάνετε συναλλαγές ή ανάληψη οποιαδήποτε στιγμή",
"benefit_spend_prefix": "Κάντε αγορές με τη MetaMask Card σε 150M+ σημεία και κερδίστε ",
"benefit_spend_cashback": "1-3% απόδοση σε mUSD",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "Μεταφορά σε οποιοδήποτε από τα πορτοφόλια σας στο MetaMask",
+ "benefit_global": "Στείλτε και λάβετε κεφάλαια παγκοσμίως",
"learn_more": "Μάθετε περισσότερα"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "Προσθήκη κεφαλαίων"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "Προσθήκη κεφαλαίων",
"convert_crypto": "Μετατροπή κρυπτονομισμάτων",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "Από οποιονδήποτε λογαριασμό",
"deposit_funds": "Κατάθεση χρημάτων",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "Από χρεωστική κάρτα ή τραπεζικό λογαριασμό",
+ "move_musd": "Μεταφέρετε {{amount}} mUSD",
+ "move_musd_no_amount": "Μεταφέρετε mUSD",
+ "move_musd_description": "Από το διαθέσιμο υπόλοιπό σας",
"receive_external": "Λήψη από εξωτερικό πορτοφόλι",
"coming_soon": "Προσεχώς"
},
@@ -6694,7 +6792,7 @@
"contact_support": "Επικοινωνία με την υποστήριξη"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "Μεταφορά κεφαλαίων",
"between_accounts": "Μεταξύ λογαριασμών",
"perps_account": "Λογαριασμός Perps",
"predictions_account": "Λογαριασμός προβλέψεων",
@@ -6712,8 +6810,8 @@
"body": "Μια εκτίμηση για το πόσα θα μπορούσατε να κερδίσετε σε μια χρονική περίοδο, με βάση το τρέχον υπόλοιπό σας και το σημερινό APY. Οι εκτιμήσεις δεν αποτελούν εγγυημένες αποδόσεις και ενδέχεται να αλλάξουν."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "Κερδίστε από τα κρυπτονομίσματά σας",
+ "body": "Η απεικόνιση υποθέτει ότι το {{percentage}}% ετήσιο επιτόκιο (APY) παραμένει αμετάβλητο για ένα έτος. Το APY μεταβάλλεται και μπορεί να αλλάξει λόγω διαφόρων παραγόντων. Δεν υπάρχει εγγύηση απόδοσης."
},
"activity": {
"title": "Δραστηριότητα",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "Πώς λειτουργεί",
- "how_it_works_subtitle": "Δείτε πώς τα χρήματά σας αποδίδουν για εσάς",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "Μάθετε περισσότερα για το mUSD",
"what_you_get_title": "Τι σας προσφέρει",
@@ -6738,7 +6836,8 @@
"sent": "Εστάλη",
"transferred": "Μεταφέρθηκε",
"card_transaction": "Συναλλαγή με κάρτα",
- "converted": "Μετατράπηκε"
+ "converted": "Μετατράπηκε",
+ "failed": "Απέτυχε"
},
"convert_stablecoins": {
"title": "Μετατρέψτε τα stablecoins σας",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "Οικονομικά",
"section_title": "Πώς λειτουργεί",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "Καταθέστε mUSD στον λογαριασμό σας και κερδίστε έως και {{percentage}}% APY (μεταβλητό) αυτόματα. Τα κεφάλαια τοποθετούνται σε ένα DeFi vault που παράγει αποδόσεις μέσω ελεγμένων αγορών δανεισμού — χωρίς δέσμευση κεφαλαίων, χωρίς απαιτήσεις, χωρίς χρονικές δεσμεύσεις.",
"description_2": "Το υπόλοιπο του λογαριασμού σας είναι το διαθέσιμο υπόλοιπο για δαπάνες. Συνδέστε την MetaMask Card για να πραγματοποιείτε συναλλαγές σε περισσότερους από 150 εκατομμύρια εμπόρους παγκοσμίως. Τα χρήματά σας συνεχίζουν να αποδίδουν μέχρι τη στιγμή που θα τα χρησιμοποιήσετε.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "Συχνές ερωτήσεις",
"faq_placeholder_answer": "Προσεχώς.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "Πώς λειτουργεί το {{percentage}}% APY;",
"faq_q2": "Τι είναι τα mUSD;",
"faq_q3": "Από πού προέρχεται η απόδοση;",
"faq_q4": "Είναι τα χρήματά μου δεσμευμένα; Μπορώ να κάνω ανάληψη οποιαδήποτε στιγμή;",
@@ -7030,11 +7129,12 @@
"confirm": "Επιβεβαίωση",
"pay_with_bottom_sheet": {
"title": "Πληρωμή με",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "Τελευταία χρήση",
+ "bank_and_card": "Τραπεζικός λογαριασμός και κάρτα",
+ "crypto": "Κρύπτο",
+ "available_balance": "{{balance}} διαθέσιμα",
+ "other_assets": "Άλλα περιουσιακά στοιχεία",
+ "other_assets_description": "Επιλέξτε από τα tokens σας"
},
"staking_footer": {
"part1": "Συνεχίζοντας, συμφωνείτε με τους ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "Τα τέλη μετατροπής σε mUSD περιλαμβάνουν τα έξοδα δικτύου και ενδέχεται να περιλαμβάνουν τα τέλη παρόχου. Δεν υπάρχει χρέωση στο MetaMask όταν μετατρέπετε σε mUSD."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "Το MetaMask θα ανταλλάξει τα mUSD σας με το token που επιθυμείτε. Οι πάροχοι ανταλλαγών ενδέχεται να χρεώνουν προμήθεια, αλλά το MetaMask όχι."
},
"title": {
"transaction_fee": "Τέλη"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "Προσθήκη κεφαλαίων",
"deposit_edit_amount_predict_withdraw": "Ανάληψη",
"deposit_edit_amount_musd_conversion": "Μετατροπή σε mUSD",
- "preparing_order": "Preparing order"
+ "preparing_order": "Προετοιμασία εντολής"
},
"change_in_simulation_modal": {
"title": "Τα αποτελέσματα έχουν αλλάξει",
@@ -7250,20 +7350,33 @@
"confirm_swap": "Ανταλλαγή",
"terms_and_conditions": "Όροι & Προϋποθέσεις",
"select_token": "Επιλέξτε token",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "Επιλέξτε έως 5 tokens",
+ "batch_sell_select_subtitle": "Όλα τα tokens πρέπει να βρίσκονται στο ίδιο δίκτυο.",
+ "batch_sell_empty_state_title": "Δεν έχετε tokens. Κανένα πρόβλημα.",
+ "batch_sell_empty_state_description": "Δεν έχετε tokens. Κανένα πρόβλημα. Εξερευνήστε και αγοράστε tokens για μαζική πώληση.",
+ "batch_sell_continue_with_one_token": "Συνεχίστε με (1) token",
+ "batch_sell_continue_with_tokens": "Συνεχίστε με ({{tokenCount}}) tokens",
+ "batch_sell_max_tokens_allowed": "Επιτρέπονται έως 5 tokens",
+ "batch_sell_single_token_dialog_title": "Ειδοποίηση υψηλής μεταβλητότητας",
+ "batch_sell_single_token_dialog_description": "Η μαζική πώληση ενός token μπορεί να οδηγήσει σε υψηλότερη τιμή. Θέλετε να κάνετε ανταλλαγή αντί γι' αυτό;",
+ "batch_sell_swap_instead": "Ναι, ανταλλαγή",
+ "batch_sell_review_title": "Επισκόπηση μαζικής πώλησης",
+ "batch_sell_select_stablecoin": "Επιλέξτε ένα stablecoin",
+ "batch_sell_total_received": "Συνολικό ποσό που λάβατε",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "Επισκόπηση",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "Προσαρμογή {{tokenSymbol}}",
+ "batch_sell_remove_token": "Αφαίρεση του {{tokenSymbol}}",
+ "batch_sell_checkbox_label": "Μαζική πώληση token",
+ "sort_balance": "Υπόλοιπο",
+ "next": "Επόμενο",
+ "explore_tokens": "Εξερευνήστε tokens",
"no_tokens_found": "Δεν βρέθηκαν token",
"no_tokens_found_description": "Δεν βρέθηκαν token με αυτό το όνομα “{}”. Δοκιμάστε άλλη αναζήτηση.",
"select_network": "Επιλέξτε δίκτυο",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "Θα χάσετε περίπου {{priceImpact}} από την αξία του token σας σε αυτή την ανταλλαγή. Προσπαθήστε να μειώσετε το ποσό ή να επιλέξετε ένα κανάλι με μεγαλύτερη ρευστότητα.",
"proceed": "Συνεχίστε",
"cancel": "Άκυρο",
+ "close": "Κλείσιμο",
"slippage_info_title": "Απόκλιση",
"slippage_info_description": "Το ποσοστό μεταβολής στην τιμή που είστε διατεθειμένοι να αποδεχθείτε πριν ακυρωθεί η συναλλαγή.",
"blockaid_error_title": "Αυτή η συναλλαγή θα ακυρωθεί",
@@ -8107,7 +8221,14 @@
"retry": "Προσπαθήστε ξανά",
"on_linea": "στο δίκτυο Linea",
"account_label": "Λογαριασμός",
- "token_label": "Token"
+ "token_label": "Token",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "Απόδοση σε mUSD",
@@ -8445,6 +8566,7 @@
"show_less": "Εμφάνιση λιγότερων",
"linking_progress": "Προσθήκη λογαριασμών… ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} εγγεγραμμένοι",
+ "accounts_added": "Accounts added",
"add_all_accounts": "Προσθήκη όλων των λογαριασμών",
"environment_selector": "Περιβάλλον",
"environment_cancel": "Άκυρο",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "Διαγραφή προόδου Ανταμοιβών",
- "description": "Αυτό θα αφαιρέσει τους λογαριασμούς σας από το πρόγραμμα Ανταμοιβών και θα διαγράψει την πρόοδό σας. Δεν θα μπορείτε να το αναιρέσετε.",
- "confirm": "Διαγραφή",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "Είστε σίγουροι;",
- "confirmation_description": "Αυτό θα διαγράψει όλη την πρόοδό σας και δεν μπορεί να αναιρεθεί. Αν επανέλθετε αργότερα στο πρόγραμμα Ανταμοιβών, θα ξεκινήσετε από το 0.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "Άκυρο",
"confirm": "Επιβεβαίωση",
+ "error_title": "Κάτι πήγε στραβά",
"error_message": "Αποτυχία αποχώρησης από το πρόγραμμα Ανταμοιβών. Παρακαλούμε προσπαθήστε ξανά.",
"processing": "Επεξεργασία..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "Απόρριψη",
@@ -8562,7 +8691,8 @@
"title_claim": "Εξαργύρωση ανταμοιβών",
"action": "Εξαργύρωση",
"empty-list": "Δεν έχετε διαθέσιμες ανταμοιβές αυτή τη στιγμή.",
- "powered_by": "Με την υποστήριξη του"
+ "powered_by": "Με την υποστήριξη του",
+ "available_count": "{{count}} διαθέσιμα"
},
"end_of_season_rewards": {
"confirm_label_default": "Επιβεβαίωση",
@@ -8880,9 +9010,11 @@
"musd_claim": "Κάντε εξαργύρωση σε mUSD",
"perps_deposit": "Προσθήκη κεφαλαίων",
"perps_withdraw": "Ανάληψη",
+ "predict_withdraw": "Ανάληψη {{sourceSymbol}} από {{sourceChain}}",
"predict_deposit": "Προσθήκη κεφαλαίων",
"swap": "Ανταλλαγή tokens",
- "swap_approval": "Έγκριση token"
+ "swap_approval": "Έγκριση token",
+ "fiat_purchase": "Αγορά {{token}} με {{paymentMethod}}"
},
"perps_deposit_solution": "Έχετε πλέον {{fiat}} σε USDC στο Arbitrum. Δοκιμάστε ξανά να κάνετε κατάθεση."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "Από το υψηλότερο στο χαμηλότερο",
"low_to_high": "Από το χαμηλότερο στο υψηλότερο",
"apply": "Εφαρμογή",
- "search_placeholder": "Αναζήτηση token, ιστότοπων, διευθύνσεων URL",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "Άκυρο",
"perps": "Συμβ.αορ.",
"rwa_perps_section": "Συμβ.αορ.",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "Συμβ.αορ.",
"predictions": "Προβλέψεις",
"no_results": "Δεν βρέθηκαν αποτελέσματα",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "Δημοφιλή",
"sites": "Ιστότοποι",
"popular_sites": "Δημοφιλείς ιστότοποι",
"search_sites": "Αναζήτηση ιστότοπων",
"view_all": "Προβολή όλων",
+ "view_more": "Δείτε περισσότερα",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "Ενεργοποίηση βασικής λειτουργικότητας",
"basic_functionality_disabled_title": "Η Εξερεύνηση δεν είναι διαθέσιμη",
"basic_functionality_disabled_description": "Δεν είναι δυνατή η ανάκτηση των απαιτούμενων μεταδεδομένων όταν η βασική λειτουργικότητα είναι απενεργοποιημένη.",
@@ -8995,6 +9131,14 @@
"crypto": "Κρύπτο",
"sports": "Αθλητικά",
"dapps": "Ιστότοποι"
+ },
+ "search_tabs": {
+ "all": "Όλα",
+ "crypto": "Cryptos",
+ "perps": "Συμβ.αορ.",
+ "stocks": "Μετοχές",
+ "predictions": "Προβλέψεις",
+ "sites": "Ιστότοποι"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "Δεν έχετε mUSD σε αυτό το δίκτυο. Αλλάξτε δίκτυο για να δείτε τα mUSD σας.",
"money_empty_state": {
"get_started": "Ξεκινήστε",
+ "earn": "Κερδίστε",
"earn_apy": "Κερδίστε {{percentage}}% APY"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "Σχετικά περιουσιακά στοιχεία",
"perpetuals": "Συμβόλαια αορίστου διάρκειας",
"predictions": "Προβλέψεις",
- "whats_happening": "Τι συμβαίνει",
- "whats_happening_ai": "AI",
- "whats_happening_impact": {
- "bullish": "Ανοδικό",
- "bearish": "Πτωτικό",
- "neutral": "Ουδέτερο"
- },
"top_traders": "Κορυφαίοι Traders",
- "whats_happening_categories": {
- "geopolitical": "Γεωπολιτικά",
- "macro": "Μακροοικονομικά",
- "regulatory": "Κανονισμοί",
- "technical": "Τεχνικά",
- "social": "Κοινωνικά",
- "other": "Άλλο"
- },
"defi": "DeFi",
"nfts": "NFT",
"trending_tokens": "Δημοφιλή",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "Δημοφιλή"
+ },
+ "whats_happening": {
+ "title": "Τι συμβαίνει",
+ "ai": "AI",
+ "impact": {
+ "bullish": "Ανοδικό",
+ "bearish": "Πτωτικό",
+ "neutral": "Ουδέτερο"
+ },
+ "categories": {
+ "geopolitical": "Γεωπολιτικά",
+ "macro": "Μακροοικονομικά",
+ "regulatory": "Κανονισμοί",
+ "technical": "Τεχνικά",
+ "social": "Κοινωνικά",
+ "other": "Άλλο"
+ }
}
}
diff --git a/locales/languages/es.json b/locales/languages/es.json
index 4fb9604c5a97..0ba4636b0436 100644
--- a/locales/languages/es.json
+++ b/locales/languages/es.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "No hay fondos disponibles. Usa otra cuenta."
},
+ "headless_buy_error": {
+ "title": "Error en la compra de moneda fiduciaria",
+ "message": "Se produjo un error en tu compra de moneda fiduciaria. Inténtalo de nuevo."
+ },
"mmpay_hardware_account": {
"title": "Billetera no admitida",
"message": "Las billeteras físicas no son compatibles.\nCambia de billetera para continuar."
@@ -133,8 +137,8 @@
"message": "Es posible que la dirección del destinatario no admita transferencias directas de tokens, lo que podría provocar la pérdida de fondos. Continúa solo si estás seguro de que este contrato puede recibir tu transferencia."
},
"gas_sponsorship_reserve_balance": {
- "message": "El patrocinio de gas no está disponible para esta transacción. Deberás mantener al menos %{minBalance} %{nativeTokenSymbol} en tu cuenta.",
- "title": "Patrocinio de gas no disponible"
+ "message": "Esta red en concreto requiere mantener una reserva de %{minBalance} %{nativeTokenSymbol} en tu cuenta.",
+ "title": "Se requiere un saldo de reserva"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "Estás enviando tokens a la dirección del contrato del token. Esto podría resultar en la pérdida de estos tokens.",
"smart_contract_address": "Dirección del contrato inteligente",
"smart_contract_address_warning": "Es posible que la dirección del destinatario no admita transferencias directas de tokens, lo que podría provocar la pérdida de fondos. Continúa solo si estás seguro de que este contrato puede recibir tu transferencia.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "Actualizar",
"i_understand": "Comprendo",
"cancel": "Cancelar",
"new_address_title": "Nueva dirección",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "Valor",
"pnl_percent": "P&L %",
- "recent": "Recent"
+ "recent": "Recientes"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "Opera con contratos perpetuos de {{symbol}}",
"subtitle": "Multiplica tus pérdidas y ganancias hasta {{leverage}}"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "Contactar a soporte"
+ },
"today": "Hoy",
"yesterday": "Ayer",
"unrealized_pnl": "P&L no realizado",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "Se han transferido {{amount}} USDC a tu billetera",
"toast_completed_any_token_subtitle": "Se transfirieron {{amount}} {{token}} a tu billetera",
"toast_error_title": "Algo salió mal",
- "toast_error_description": "Error al procesar el retiro"
+ "toast_error_description": "Error al procesar el retiro",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Inténtalo de nuevo"
},
"quote": {
"network_fee": "Tarifa de red",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "Predicciones de MetaMask",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "Copa del Mundo",
+ "banner_title": "Copa del Mundo 2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
- "props": "Props",
- "live": "Live"
+ "all": "Todas",
+ "props": "Apuestas especiales",
+ "live": "En vivo"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
+ "group_stage": "Fase de grupos",
+ "round_of_32": "Dieciseisavos de final",
+ "round_of_16": "Octavos de final",
+ "quarterfinals": "Cuartos de final",
+ "semifinals": "Semifinales",
+ "third_place": "Tercer puesto",
"final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_a": "Grupo A",
+ "group_b": "Grupo B",
+ "group_c": "Grupo C",
+ "group_d": "Grupo D",
+ "group_e": "Grupo E",
+ "group_f": "Grupo B",
+ "group_g": "Grupo G",
+ "group_h": "Grupo H",
+ "group_i": "Grupo I",
+ "group_j": "Grupo J",
+ "group_k": "Grupo K",
+ "group_l": "Grupo L",
+ "third_place_match": "Partido por el tercer puesto"
}
},
"prediction_markets": "Mercados de predicción",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "Retiro finalizado",
"withdraw_completed_subtitle": "Se han transferido {{amount}} USDC a tu billetera",
"withdraw_any_token_completed_subtitle": "Se transfirieron {{amount}} {{token}} a tu billetera",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "Retiros temporalmente no disponibles",
+ "unavailable_description": "Para asistencia urgente, comuníquese con el servicio al cliente.",
"unavailable_got_it": "Entendido",
"error_title": "Algo salió mal",
"error_description": "No se pudo proceder con el retiro",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "Aceptar y cerrar",
"pna25_open_settings_button": "Abrir Configuración"
},
+ "onboarding_interest_questionnaire": {
+ "title": "¿Qué deseas hacer con MetaMask?",
+ "description": "Selecciona todas las opciones que correspondan.",
+ "option_buy_and_sell_crypto": "Comprar y vender criptomonedas",
+ "option_consolidate_wallets": "Consolidar tus billeteras",
+ "option_advanced_trades": "Realizar operaciones avanzadas",
+ "option_predict_sports_events": "Predecir resultados deportivos y eventos",
+ "option_crypto_as_money": "Usar criptomonedas como dinero",
+ "option_connect_apps_sites": "Conectarse a aplicaciones o sitios web",
+ "continue": "Continuar"
+ },
"template_confirmation": {
"ok": "OK",
"cancel": "Cancelar"
@@ -3261,8 +3286,27 @@
"notifications_desc": "Administre sus notificaciones",
"allow_notifications": "Permitir notificaciones",
"enable_push_notifications": "Activar notificaciones push",
- "allow_notifications_desc": "Manténgase informado sobre lo que sucede en su billetera con notificaciones. Para usar notificaciones, usamos un perfil para sincronizar algunas configuraciones en sus dispositivos.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "Desact.",
+ "select_all": "Seleccionar todo",
+ "deselect_all": "Deseleccionar todo",
+ "select_accounts_title": "Cuentas",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "Personalice sus notificaciones",
"customize_session_desc": "Active los tipos de notificaciones que desea recibir:",
"account_session_title": "Actividad de la cuenta",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snaps",
"snaps_desc": "Nuevas características y actualizaciones",
"products_announcements_title": "Anuncios de producto",
- "products_announcements_desc": "Nuevos productos y características",
- "perps_title": "Trading con contratos perpetuos"
+ "products_announcements_desc": "Nuevos productos y características"
},
"contacts_title": "Contactos",
"contacts_desc": "Agregar, editar, quitar y administrar tus cuentas.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "Tarjeta",
"reset_onboarding_description": "Restablecer el estado de incorporación de la tarjeta para iniciar el proceso de incorporación desde el principio.",
- "reset_onboarding_button": "Restablecer estado de incorporación"
+ "reset_onboarding_button": "Restablecer estado de incorporación",
+ "unlink_money_account_description": "Revocar el límite de gasto de USDC que autoriza a la Tarjeta a gastar desde tu cuenta Finanzas. Esto envía una transacción approve(0) y marca la cuenta Finanzas como no delegada en el backend de la Tarjeta.",
+ "unlink_money_account_button": "Desvincular la cuenta Finanzas de la tarjeta",
+ "unlink_money_account_disabled_hint": "No hay ninguna vinculación activa para eliminar."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "Háptica",
@@ -3842,8 +3893,8 @@
"predict_button": "Predicciones",
"add_collectible_button": "Agregar",
"info": "Información",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "Venta por lotes",
+ "batch_sell_new_label": "Nuevo",
"swap": "Canjear",
"convert": "Convertir",
"bridge": "Puentear",
@@ -3883,7 +3934,7 @@
"troubleshoot": "Solucionar problemas",
"deposit_description": "Transferencia bancaria o con tarjeta con tarifas bajas",
"buy_description": "Ideal para comprar un token específico",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "Vende hasta 5 tokens por una moneda estable",
"sell_description": "Vender cripto por dinero en efectivo",
"swap_description": "Cambiar entre tokens",
"bridge_description": "Transferir tokens entre redes",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "Configuración > Notificaciones.",
"cancel": "Cancelar",
"cta": "Activar"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "No te pierdas ningún movimiento",
+ "body": "Recibe alertas en tiempo real cuando los precios alcancen tus objetivos, se confirmen tus operaciones y se mueva tu portafolio. Además de actualizaciones de productos y recompensas mientras tanto. Compartiremos actualizaciones basadas en tus interacciones con MetaMask.",
+ "button_yes": "Sí",
+ "button_not_now": "Ahora no",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "ahora",
+ "title": "ETH ha subido un 4,2 % hoy",
+ "message": "Ahora está en $2668,51 — por encima de tu alerta de precio"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "Hace 1 h",
+ "title": "Recibiste 0,25 ETH",
+ "message": "Desde 0x9a21…4f8c · $640,29"
+ }
+ },
+ "existing_user": {
+ "title": "Presentamos las alertas personalizadas",
+ "body": "Recibe notificaciones que se adapten a tu forma de operar. Actualízalas en cualquier momento.",
+ "card_title": "Lo que recibirás",
+ "card_description": "Alertas personalizadas y actualizaciones adaptadas a tu actividad de trading.",
+ "button_confirm": "Confirmar",
+ "button_not_now": "Ahora no"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "Pagar con",
"buying_via": "Comprando a través de {{providerName}}.",
"change_provider": "Cambiar de proveedor.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "Se produjo un error. Inténtalo de nuevo.",
"no_payment_methods_available": "No hay métodos de pago disponibles.",
"error_fetching_quotes": "Se produjo un error. Inténtalo de nuevo.",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "Convertir a mUSD",
"get_a_percentage_musd_bonus": "Obtén un bono del {{percentage}} % en mUSD",
"convert": "Convertir",
+ "confirm": "Confirmar",
+ "convert_tooltip_description": "Convierte tus monedas estables a mUSD y gana hasta un {{percentage}} % de bono anualizado que puedes reclamar diariamente.",
"fetching_quote": "Obteniendo cotización...",
"you_convert": "Conviertes",
"network_fee": "Tarifa de red",
@@ -6597,7 +6679,7 @@
"step_progress": "Paso {{current}} de {{total}}",
"title": "Agregar dinero",
"description": "Deposita fondos en tu cuenta y empieza a ganar APY.",
- "add": "Agregar",
+ "add": "Agregar fondos",
"step2_title": "Obtén tu tarjeta MetaMask",
"step2_description": "Gasta tu saldo de Money mientras genera ganancias, en cualquier lugar donde se acepte Mastercard.",
"step2_cta": "Obtener la tarjeta",
@@ -6605,6 +6687,19 @@
"link_card_description": "Gasta tu saldo mientras genera ganancias, en cualquier lugar donde se acepte Mastercard.",
"link_card_cta": "Vincular tarjeta"
},
+ "rive_onboarding": {
+ "step1_title": "Ya llegaron las cuentas Finanzas",
+ "step1_body": "Gana hasta un {{percentage}} % de APY sobre tu saldo, disponible en toda tu billetera.",
+ "step1_footer_text": "El APY es variable y puede cambiar en cualquier momento.",
+ "step2_title": "Gana automáticamente",
+ "step2_body": "Transfiere monedas estables sin comisiones de cambio. Los fondos comienzan a generar ganancias de inmediato.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "Gasta en cualquier lugar",
+ "step3_body": "Obtén hasta un {{percentage}} % de cashback en tus compras vinculando tu cuenta Finanzas a una tarjeta MetaMask.",
+ "step4_title": "Opera y gana en un solo lugar",
+ "step4_body": "Usa tu saldo de Finanzas para operar en MetaMask mientras sigues obteniendo rendimiento.",
+ "continue": "Continuar"
+ },
"action": {
"add": "Agregar",
"transfer": "Transferir",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "Cómo funciona",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "Deposita mUSD en tu cuenta Finanzas y gana hasta",
+ "description_suffix": ". Tu saldo está respaldado por dólares y disponible para gastar, operar o enviar en cualquier momento."
},
"musd_row": {
"add": "Agregar"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Saldo de Finanzas",
"add": "Agregar",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Saldo de Finanzas",
+ "info_sheet_body": "Tu saldo de mUSD respaldado por dólares, siempre disponible para gastar, enviar u operar en cualquier momento. No lo incluimos en el saldo total de tu cuenta.\n\nLos retiros se procesan de inmediato, sujetos a los tiempos de confirmación estándar de la red en la cadena de bloques correspondiente. Si la liquidez es escasa, puede haber retrasos temporales."
},
"potential_earnings": {
"title": "Gana con tus criptomonedas",
"description": "Descubre cómo tu dinero puede crecer con el tiempo al convertir tus criptomonedas a mUSD.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "Convierte tu {{total}} en activos y podrías ganar hasta",
+ "description_with_amounts_suffix": "en un año.",
"convert": "Convertir",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "Convierte tus criptomonedas",
"no_fee": "Sin tarifa de MetaMask",
"view_all": "Ver todo",
"view_potential_earnings": "Ver ganancias potenciales"
@@ -6649,41 +6744,44 @@
"cashback": "{{percentage}} % de mUSD de cashback",
"get_now": "Obtener ahora",
"link_title": "Vincular la tarjeta MetaMask",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "Gasta tu saldo de Finanzas y gana con tus compras. Además, hasta un {{apy}} % de APY sobre tu saldo.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "Obtén un {{percentage}} % de mUSD de cashback",
+ "link_bullet_apy": "Gana hasta un {{apy}} % de APY",
"link_card": "Vincular tarjeta",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "Gasta y gana",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "Vincular tarjeta",
+ "manage_card": "Gestionar",
+ "avail_balance": "Saldo disponible"
},
"what_you_get": {
"title": "Lo que obtienes",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "Gana automáticamente hasta",
"benefit_dollar_backed": "Mantén tu dinero seguro en mUSD, una moneda estable respaldada por el dólar en una proporción de 1:1",
"benefit_liquidity": "Obtén liquidez total sin bloqueos, para que puedas operar o retirar fondos en cualquier momento",
"benefit_spend_prefix": "Gasta en más de 150 millones de comercios con la tarjeta MetaMask y gana ",
"benefit_spend_cashback": "1-3 % de mUSD de cashback",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "Transfiere a cualquiera de tus billeteras en MetaMask",
+ "benefit_global": "Envía y recibe fondos a nivel mundial",
"learn_more": "Conozca más"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "Agregar fondos"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "Agregar fondos",
"convert_crypto": "Convertir criptomonedas",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "Desde cualquier cuenta",
"deposit_funds": "Depositar fondos",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "Desde tarjeta de débito o banco",
+ "move_musd": "Transfiere tus {{amount}} mUSD",
+ "move_musd_no_amount": "Transfiere tus mUSD",
+ "move_musd_description": "Desde tu saldo",
"receive_external": "Recibir desde una billetera externa",
"coming_soon": "Próximamente"
},
@@ -6694,7 +6792,7 @@
"contact_support": "Contactar a soporte"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "Transfiere fondos",
"between_accounts": "Entre cuentas",
"perps_account": "Cuenta de Perps",
"predictions_account": "Cuenta de Predicciones",
@@ -6712,8 +6810,8 @@
"body": "Una estimación de cuánto podrías ganar durante un período determinado, basada en tu saldo actual y el APY de hoy. Las estimaciones no son rendimientos garantizados y están sujetas a cambios."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "Gana con tus criptomonedas",
+ "body": "La ilustración asume que el {{percentage}}% de rendimiento porcentual anual (APY) se mantiene sin cambios durante un año. El APY es variable y puede cambiar debido a diversos factores. No hay garantía de rendimiento."
},
"activity": {
"title": "Actividad",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "Cómo funciona",
- "how_it_works_subtitle": "Observa cómo tu dinero trabaja para ti",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "Más información sobre mUSD",
"what_you_get_title": "Lo que obtienes",
@@ -6738,7 +6836,8 @@
"sent": "Enviado",
"transferred": "Transferido",
"card_transaction": "Transacción con tarjeta",
- "converted": "Convertido"
+ "converted": "Convertido",
+ "failed": "Fallido"
},
"convert_stablecoins": {
"title": "Convierte tus monedas estables",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "Money",
"section_title": "Cómo funciona",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "Deposita mUSD en tu cuenta Finanzas y gana hasta un {{percentage}} % de APY (variable) automáticamente. Los fondos se depositan en una bóveda DeFi que genera rendimientos en mercados de préstamos auditados: sin staking, sin reclamaciones, sin bloqueos.",
"description_2": "Tu saldo Money es tu saldo para gastar. Vincula tu tarjeta MetaMask para comprar en más de 150 millones de comercios en todo el mundo. Tu dinero sigue generando intereses hasta el momento en que lo uses.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "Preguntas frecuentes",
"faq_placeholder_answer": "Próximamente.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "¿Cómo funciona el {{percentage}} % de APY?",
"faq_q2": "¿Qué es mUSD?",
"faq_q3": "¿De dónde proviene el rendimiento?",
"faq_q4": "¿Mi dinero está bloqueado? ¿Puedo retirarlo en cualquier momento?",
@@ -7030,11 +7129,12 @@
"confirm": "Confirmar",
"pay_with_bottom_sheet": {
"title": "Pagar con",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "Último uso",
+ "bank_and_card": "Banco y tarjeta",
+ "crypto": "Cripto",
+ "available_balance": "{{balance}} disponible(s)",
+ "other_assets": "Otros activos",
+ "other_assets_description": "Selecciona entre tus tokens"
},
"staking_footer": {
"part1": "Al continuar, acepta nuestros ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "Las tarifas de conversión a mUSD incluyen costos de red y pueden incluir tarifas del proveedor. No se aplica ninguna tarifa de MetaMask al convertir a mUSD."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMask canjeará tus mUSD por el token que desees. Los proveedores de canje pueden cobrar una comisión, pero MetaMask no."
},
"title": {
"transaction_fee": "Tarifas"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "Agregar fondos",
"deposit_edit_amount_predict_withdraw": "Retirar",
"deposit_edit_amount_musd_conversion": "Convertir a mUSD",
- "preparing_order": "Preparing order"
+ "preparing_order": "Preparando orden"
},
"change_in_simulation_modal": {
"title": "Los resultados cambiaron",
@@ -7250,20 +7350,33 @@
"confirm_swap": "Canjear",
"terms_and_conditions": "Términos y condiciones",
"select_token": "Seleccione un token",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "Selecciona hasta 5 tokens",
+ "batch_sell_select_subtitle": "Todos los tokens deben estar en la misma red.",
+ "batch_sell_empty_state_title": "¿No tienes tokens? No hay problema.",
+ "batch_sell_empty_state_description": "No tienes tokens. No hay problema. Explora y compra tokens para venderlos en lote.",
+ "batch_sell_continue_with_one_token": "Continuar con (1) token",
+ "batch_sell_continue_with_tokens": "Continuar con ({{tokenCount}}) tokens",
+ "batch_sell_max_tokens_allowed": "Se permite un máximo de 5 tokens",
+ "batch_sell_single_token_dialog_title": "Alerta de tasa alta",
+ "batch_sell_single_token_dialog_description": "La venta en lote de un token podría generar una tasa más alta. ¿Prefieres hacer un canje?",
+ "batch_sell_swap_instead": "Sí, canjear",
+ "batch_sell_review_title": "Revisión de venta en lote",
+ "batch_sell_select_stablecoin": "Selecciona una moneda estable",
+ "batch_sell_total_received": "Total recibido",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "Revisar",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "Personalizar {{tokenSymbol}}",
+ "batch_sell_remove_token": "Eliminar {{tokenSymbol}}",
+ "batch_sell_checkbox_label": "Vender token en lote",
+ "sort_balance": "Saldo",
+ "next": "Siguiente",
+ "explore_tokens": "Explorar tokens",
"no_tokens_found": "No se encontraron tokens",
"no_tokens_found_description": "No encontramos ningún token con este nombre. Prueba con otra búsqueda.",
"select_network": "Seleccionar red",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "Perderás aproximadamente {{priceImpact}} del valor de tu token en este canje. Intenta reducir el monto o elegir una ruta más líquida.",
"proceed": "Continuar",
"cancel": "Cancelar",
+ "close": "Cerrar",
"slippage_info_title": "Deslizamiento",
"slippage_info_description": "El % de cambio en el precio que estás dispuesto a permitir antes de que se cancele tu transacción.",
"blockaid_error_title": "Esta transacción se revertirá",
@@ -8107,7 +8221,14 @@
"retry": "Inténtalo de nuevo",
"on_linea": "en Linea",
"account_label": "Cuenta",
- "token_label": "Token"
+ "token_label": "Token",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "Cashback en mUSD",
@@ -8336,7 +8457,7 @@
"main_title": "Recompensas",
"vip": {
"bps_unit": "bps",
- "swaps_label": "Swaps",
+ "swaps_label": "Canjes",
"perps_label": "Perps",
"error_title": "We couldn’t load your VIP dashboard",
"error_description": "Check your connection and try again.",
@@ -8445,6 +8566,7 @@
"show_less": "Mostrar menos",
"linking_progress": "Agregando cuentas... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} inscritos",
+ "accounts_added": "Accounts added",
"add_all_accounts": "Agregar todas las cuentas",
"environment_selector": "Entorno",
"environment_cancel": "Cancelar",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "Eliminar del programa de Recompensas",
- "description": "Esta acción eliminará tus cuentas del programa de Recompensas y borrará tu progreso. No podrás deshacer esta acción.",
- "confirm": "Eliminar",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "¿Estás seguro?",
- "confirmation_description": "Esto eliminará todo tu progreso y no se podrá revertir. Si te reincorporas al programa de Recompensas más adelante, comenzarás desde cero.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "Cancelar",
"confirm": "Confirmar",
+ "error_title": "Algo salió mal",
"error_message": "Error al excluirse del programa de Recompensas. Inténtalo de nuevo.",
"processing": "Procesando..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "Ignorar",
@@ -8562,7 +8691,8 @@
"title_claim": "Reclamar beneficio",
"action": "Reclamar",
"empty-list": "No tienes ningún beneficio en este momento.",
- "powered_by": "Impulsado por"
+ "powered_by": "Impulsado por",
+ "available_count": "{{count}} disponible(s)"
},
"end_of_season_rewards": {
"confirm_label_default": "Confirmar",
@@ -8695,7 +8825,7 @@
"label_your_rank": "Tu rango",
"label_portfolio": "Portfolio",
"label_net_inflow": "Flujo neto de entrada",
- "label_total_inflow": "Entrada total",
+ "label_total_inflow": "Flujo total de entrada",
"label_outflow": "Flujo de salida",
"label_days_held": "Días de retención",
"qualified_title": "Estás calificado",
@@ -8880,9 +9010,11 @@
"musd_claim": "Reclamar mUSD",
"perps_deposit": "Agregar fondos",
"perps_withdraw": "Retiro",
+ "predict_withdraw": "Retirar {{sourceSymbol}} desde {{sourceChain}}",
"predict_deposit": "Agregar fondos",
"swap": "Canjear tokens",
- "swap_approval": "Aprobar tokens"
+ "swap_approval": "Aprobar tokens",
+ "fiat_purchase": "Comprar {{token}} con {{paymentMethod}}"
},
"perps_deposit_solution": "Ya tienes {{fiat}} de USDC en Arbitrum. Vuelve a intentar realizar tu depósito."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "De máximo a mínimo",
"low_to_high": "De mínimo a máximo",
"apply": "Aplicar",
- "search_placeholder": "Buscar tokens, sitios, URL",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "Cancelar",
"perps": "Perps",
"rwa_perps_section": "Perps",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "Perps",
"predictions": "Predicciones",
"no_results": "No se encontraron resultados",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "Populares",
"sites": "Sitios",
"popular_sites": "Sitios populares",
"search_sites": "Sitios de búsqueda",
"view_all": "Ver todo",
+ "view_more": "Ver más",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "Activar la funcionalidad básica",
"basic_functionality_disabled_title": "Explorar no está disponible",
"basic_functionality_disabled_description": "No podemos obtener los metadatos necesarios cuando la funcionalidad básica está deshabilitada.",
@@ -8995,6 +9131,14 @@
"crypto": "Cripto",
"sports": "Deportes",
"dapps": "Sitios"
+ },
+ "search_tabs": {
+ "all": "Todas",
+ "crypto": "Cryptos",
+ "perps": "Perps",
+ "stocks": "Acciones",
+ "predictions": "Predicciones",
+ "sites": "Sitios"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "No hay mUSD en esta red. Cambia de red para ver tus mUSD.",
"money_empty_state": {
"get_started": "Comenzar",
+ "earn": "Ganar",
"earn_apy": "Gana un {{percentage}} % de APY"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "Activos relacionados",
"perpetuals": "Contratos perpetuos",
"predictions": "Predicciones",
- "whats_happening": "Qué está pasando",
- "whats_happening_ai": "IA",
- "whats_happening_impact": {
- "bullish": "Al alza",
- "bearish": "A la baja",
- "neutral": "Neutral"
- },
"top_traders": "Traders principales",
- "whats_happening_categories": {
- "geopolitical": "Geopolítico",
- "macro": "Macro",
- "regulatory": "Regulatorio",
- "technical": "Técnico",
- "social": "Social",
- "other": "Otro"
- },
"defi": "DeFi",
"nfts": "NFT",
"trending_tokens": "Tendencias",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "Populares"
+ },
+ "whats_happening": {
+ "title": "Qué está pasando",
+ "ai": "IA",
+ "impact": {
+ "bullish": "Al alza",
+ "bearish": "A la baja",
+ "neutral": "Neutral"
+ },
+ "categories": {
+ "geopolitical": "Geopolítico",
+ "macro": "Macro",
+ "regulatory": "Regulatorio",
+ "technical": "Técnico",
+ "social": "Social",
+ "other": "Otro"
+ }
}
}
diff --git a/locales/languages/fr.json b/locales/languages/fr.json
index 536b363914d5..7eda6bf1e9bd 100644
--- a/locales/languages/fr.json
+++ b/locales/languages/fr.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "Aucun fonds disponible. Veuillez utiliser un autre compte."
},
+ "headless_buy_error": {
+ "title": "Échec du règlement de l’achat en monnaie fiduciaire",
+ "message": "Une erreur s’est produite lors du règlement de votre achat en monnaie fiduciaire. Veuillez réessayer."
+ },
"mmpay_hardware_account": {
"title": "Portefeuille non pris en charge",
"message": "Les portefeuilles matériels ne sont pas pris en charge.\nChangez de portefeuille pour continuer."
@@ -133,8 +137,8 @@
"message": "L’adresse du destinataire ne prend peut-être pas en charge les transferts directs de jetons, ce qui pourrait entraîner une perte de fonds. Ne poursuivez que si vous êtes certain que ce contrat est capable de recevoir votre transfert."
},
"gas_sponsorship_reserve_balance": {
- "message": "Le parrainage de gaz n’est pas disponible pour cette transaction. Vous devrez conserver au moins %{minBalance} %{nativeTokenSymbol} sur votre compte.",
- "title": "Parrainage de gaz non disponible"
+ "message": "Ce réseau spécifique exige que vous mainteniez une réserve de %{minBalance} %{nativeTokenSymbol} sur votre compte.",
+ "title": "Un solde de réserve est requis"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "Vous envoyez des jetons à l’adresse du contrat du jeton. Cela peut entraîner la perte de ces jetons.",
"smart_contract_address": "Adresse du contrat intelligent",
"smart_contract_address_warning": "L’adresse du destinataire ne prend peut-être pas en charge les transferts directs de jetons, ce qui pourrait entraîner une perte de fonds. Ne poursuivez que si vous êtes certain que ce contrat est capable de recevoir votre transfert.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "Mettre à jour",
"i_understand": "Je comprends",
"cancel": "Annuler",
"new_address_title": "Nouvelle adresse",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "Valeur",
"pnl_percent": "Résultat %",
- "recent": "Recent"
+ "recent": "Récents"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "Tradez un contrat à terme perpétuel sur {{symbol}}",
"subtitle": "Multipliez vos pertes et profits jusqu’à {{leverage}} fois"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "Contacter le service d’assistance"
+ },
"today": "Aujourd’hui",
"yesterday": "Hier",
"unrealized_pnl": "Profits et pertes non réalisés",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC transféré vers votre portefeuille",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} transféré(s) vers votre portefeuille",
"toast_error_title": "Quelque chose a mal tourné",
- "toast_error_description": "Échec du retrait"
+ "toast_error_description": "Échec du retrait",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Réessayez"
},
"quote": {
"network_fee": "Frais de réseau",
@@ -1767,10 +1781,10 @@
"oracle_price": "Prix de l’oracle",
"order_book": "Carnet d’ordres",
"countdown": "Compte à rebours",
- "long": "Position longue",
- "short": "Position courte",
- "long_lowercase": "position longue",
- "short_lowercase": "position courte",
+ "long": "Long",
+ "short": "Courte",
+ "long_lowercase": "long",
+ "short_lowercase": "courte",
"modify": "Modifier",
"close_long": "Fermer la position longue",
"close_short": "Fermer la position courte",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "Prédictions MetaMask",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "Coupe du monde",
+ "banner_title": "Coupe du monde 2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
+ "all": "Tous",
"props": "Props",
- "live": "Live"
+ "live": "Active"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
- "final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_stage": "Phase de groupes",
+ "round_of_32": "Seizièmes de finale",
+ "round_of_16": "Huitièmes de finale",
+ "quarterfinals": "Quarts de finale",
+ "semifinals": "Demi-finales",
+ "third_place": "Troisième place",
+ "final": "Finale",
+ "group_a": "Groupe A",
+ "group_b": "Groupe B",
+ "group_c": "Groupe C",
+ "group_d": "Groupe D",
+ "group_e": "Groupe E",
+ "group_f": "Groupe F",
+ "group_g": "Groupe G",
+ "group_h": "Groupe H",
+ "group_i": "Groupe I",
+ "group_j": "Groupe J",
+ "group_k": "Groupe K",
+ "group_l": "Groupe L",
+ "third_place_match": "Match pour la troisième place"
}
},
"prediction_markets": "Marchés prédictifs",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "Retrait effectué",
"withdraw_completed_subtitle": "{{amount}} USDC transféré vers votre portefeuille",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}} transféré(s) vers votre portefeuille",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "Retraits temporairement indisponibles",
+ "unavailable_description": "Pour une assistance urgente, veuillez contacter le service client.",
"unavailable_got_it": "J’ai compris",
"error_title": "Quelque chose a mal tourné",
"error_description": "Échec du retrait",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "Accepter et fermer",
"pna25_open_settings_button": "Ouvrir les paramètres"
},
+ "onboarding_interest_questionnaire": {
+ "title": "Que souhaitez-vous faire avec MetaMask ?",
+ "description": "Sélectionnez toutes les réponses pertinentes.",
+ "option_buy_and_sell_crypto": "Acheter et vendre des crypto-monnaies",
+ "option_consolidate_wallets": "Consolider vos portefeuilles",
+ "option_advanced_trades": "Effectuer du trading avancé",
+ "option_predict_sports_events": "Parier sur des événements sportifs, politiques, etc.",
+ "option_crypto_as_money": "Utiliser les crypto-monnaies comme moyen de paiement",
+ "option_connect_apps_sites": "Vous connecter à des applications ou des sites",
+ "continue": "Continuer"
+ },
"template_confirmation": {
"ok": "OK",
"cancel": "Annuler"
@@ -3261,8 +3286,27 @@
"notifications_desc": "Gérez vos notifications",
"allow_notifications": "Autoriser les notifications",
"enable_push_notifications": "Activer les notifications push",
- "allow_notifications_desc": "Restez au courant de ce qui se passe dans votre portefeuille grâce aux notifications. Pour activer les notifications, nous utilisons un profil pour synchroniser certains paramètres entre vos appareils.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "Off",
+ "select_all": "Tout sélectionner",
+ "deselect_all": "Désélectionner tout",
+ "select_accounts_title": "Comptes",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "Personnalisez vos notifications",
"customize_session_desc": "Activez les types de notifications que vous souhaitez recevoir :",
"account_session_title": "Activité du compte",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snaps",
"snaps_desc": "Nouvelles fonctionnalités et mises à jour",
"products_announcements_title": "Annonces de produits",
- "products_announcements_desc": "Nouveaux produits et nouvelles fonctionnalités",
- "perps_title": "Trading de contrats à terme perpétuels (perps)"
+ "products_announcements_desc": "Nouveaux produits et nouvelles fonctionnalités"
},
"contacts_title": "Contacts",
"contacts_desc": "Ajoutez, modifiez, supprimez et gérez vos contacts.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "Card",
"reset_onboarding_description": "Réinitialiser l’état d’intégration de « Card » pour recommencer le processus d’intégration depuis le début.",
- "reset_onboarding_button": "Réinitialiser l’état d’intégration"
+ "reset_onboarding_button": "Réinitialiser l’état d’intégration",
+ "unlink_money_account_description": "Révoquer la limite de dépenses en USDC qui autorise Card à effectuer des paiements en débitant votre compte Money. Cela soumet une transaction « approve(0) » et marque le compte Money comme non délégué dans le back-end de Card.",
+ "unlink_money_account_button": "Dissocier le compte Money de Card",
+ "unlink_money_account_disabled_hint": "Aucun lien actif à supprimer."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "Retour haptique",
@@ -3842,8 +3893,8 @@
"predict_button": "Prédictions",
"add_collectible_button": "Ajouter",
"info": "Infos",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "Vente par lot",
+ "batch_sell_new_label": "Nouveau",
"swap": "Échanger",
"convert": "Convertir",
"bridge": "Passerelle",
@@ -3883,7 +3934,7 @@
"troubleshoot": "Résoudre le problème",
"deposit_description": "Virement bancaire ou par carte à frais réduits",
"buy_description": "Convient pour acheter un jeton spécifique",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "Vendez jusqu’à 5 jetons contre un stablecoin",
"sell_description": "Vendre des cryptomonnaies",
"swap_description": "Échange de jetons",
"bridge_description": "Transférer des jetons entre différents réseaux",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "Paramètres > Notifications.",
"cancel": "Annuler",
"cta": "Activer"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "Ne manquez jamais une opportunité",
+ "body": "Recevez des alertes en temps réel lorsque les prix atteignent vos objectifs, que vos opérations de Bourse sont confirmées et que votre portefeuille évolue. Vous recevrez également des mises à jour sur les produits et des récompenses. Nous vous informerons des dernières mises à jour en fonction de vos interactions avec MetaMask.",
+ "button_yes": "Oui",
+ "button_not_now": "Pas maintenant",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "maintenant",
+ "title": "L’ETH affiche aujourd’hui une hausse de 4,2 %",
+ "message": "Cote 2 668,51 $ actuellement — au-dessus de votre alerte de prix"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "Il y a 1 heure",
+ "title": "0,25 ETH reçus",
+ "message": "Provenant de 0x9a21…4f8c · 640,29 $"
+ }
+ },
+ "existing_user": {
+ "title": "Présentation des alertes personnalisées",
+ "body": "Recevez des notifications adaptées à votre style de trading que vous pouvez modifier à tout moment.",
+ "card_title": "Ce que vous obtiendrez",
+ "card_description": "Des alertes et des mises à jour personnalisées, adaptées à votre activité de trading.",
+ "button_confirm": "Confirmer",
+ "button_not_now": "Pas maintenant"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "Payer avec",
"buying_via": "Achat via {{providerName}}.",
"change_provider": "Changer de fournisseur.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "Un problème est survenu, veuillez réessayer.",
"no_payment_methods_available": "Aucun moyen de paiement n’est disponible.",
"error_fetching_quotes": "Un problème est survenu, veuillez réessayer.",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "Convertir en mUSD",
"get_a_percentage_musd_bonus": "Obtenir {{percentage}} % de bonus en mUSD",
"convert": "Convertir",
+ "confirm": "Confirmer",
+ "convert_tooltip_description": "Convertissez vos stablecoins en mUSD et gagnez jusqu’à {{percentage}} % de bonus annualisé que vous pouvez réclamer quotidiennement. Proposé par Relay.",
"fetching_quote": "Récupération de la cotation...",
"you_convert": "Vous convertissez",
"network_fee": "Frais de réseau",
@@ -6590,14 +6672,14 @@
"money": {
"title": "Money",
"your_balance": "Votre solde",
- "apy_label": "Taux de rendement annuel de {{percentage}} %",
+ "apy_label": "APY {{percentage}} %",
"apy_currency_suffix": " • mUSD",
"apy_info_label": "Informations sur le taux de rendement annuel",
"onboarding": {
"step_progress": "Étape {{current}} sur {{total}}",
"title": "Ajouter de l’argent",
"description": "Approvisionnez votre compte et commencez à percevoir un rendement annuel.",
- "add": "Ajouter",
+ "add": "Ajouter des fonds",
"step2_title": "Obtenez votre carte MetaMask Card",
"step2_description": "Dépensez l’argent que vous avez déposé sur votre compte Money tout en le faisant fructifier, partout où la carte Mastercard est acceptée.",
"step2_cta": "Obtenir une carte",
@@ -6605,6 +6687,19 @@
"link_card_description": "Dépensez votre argent tout en le faisant fructifier, partout où la carte Mastercard est acceptée.",
"link_card_cta": "Associer une carte"
},
+ "rive_onboarding": {
+ "step1_title": "Les comptes Money sont disponibles",
+ "step1_body": "Gagnez jusqu’à {{percentage}} % de rendement annuel sur votre solde, disponible sur l’ensemble de votre portefeuille.",
+ "step1_footer_text": "Le taux de rendement annuel est variable et peut changer à tout moment.",
+ "step2_title": "Générez des revenus automatiquement",
+ "step2_body": "Transférez des stablecoins sans frais de change. Vos fonds commencent à générer des revenus immédiatement.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "Dépensez partout",
+ "step3_body": "Bénéficiez d’un cashback allant jusqu’à {{percentage}} % sur vos achats en associant votre compte Money à une carte MetaMask.",
+ "step4_title": "Tradez et générez des revenus sur la même plateforme",
+ "step4_body": "Utilisez le solde de votre compte Money pour trader sur MetaMask tout en continuant à générer des revenus.",
+ "continue": "Continuer"
+ },
"action": {
"add": "Ajouter",
"transfer": "Transférer",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "Comment ça marche ",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "Déposez des mUSD sur votre compte Money et gagnez jusqu’à",
+ "description_suffix": ". Votre solde est adossé au dollar et vous pouvez l’utiliser à tout moment pour effectuer des achats, des transferts ou des opérations de Bourse."
},
"musd_row": {
"add": "Ajouter"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Solde du compte Money",
"add": "Ajouter",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Solde du compte Money",
+ "info_sheet_body": "Votre solde qui est adossé au dollar et que vous pouvez utiliser à tout moment pour effectuer des achats, des transferts ou des opérations de Bourse n’est pas comptabilisé dans le solde total de votre compte.\n\nLes retraits sont traités immédiatement, sous réserve des délais de confirmation habituels du réseau de la blockchain utilisée. Des retards peuvent survenir en cas de manque de liquidités."
},
"potential_earnings": {
"title": "Gagnez de l’argent grâce à vos crypto-monnaies",
"description": "Découvrez comment votre argent peut fructifier au fil du temps en convertissant vos crypto-monnaies en mUSD.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "Convertissez vos {{total}} en actifs et vous pourriez gagner jusqu’à",
+ "description_with_amounts_suffix": "en un an.",
"convert": "Convertir",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "Convertissez vos crypto-monnaies",
"no_fee": "Pas de frais MetaMask",
"view_all": "Tout afficher",
"view_potential_earnings": "Afficher les gains potentiels"
@@ -6649,41 +6744,44 @@
"cashback": "{{percentage}} % de cashback en mUSD",
"get_now": "Obtenir maintenant",
"link_title": "Associer une carte MetaMask Card",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "Utilisez le solde de votre compte pour effectuer des achats et gagnez de l’argent. De plus, vous bénéficiez d’un taux de rendement annuel pouvant atteindre les {{apy}} % sur votre solde.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "Recevez {{percentage}} % de cashback en mUSD",
+ "link_bullet_apy": "Obtenez jusqu’à {{apy}} % de rendement annuel",
"link_card": "Associer une carte",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "Dépensez\net générez des revenus",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "Associer une carte",
+ "manage_card": "Gérer",
+ "avail_balance": "Solde disponible"
},
"what_you_get": {
"title": "Ce que vous obtenez",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "Gagnez automatiquement jusqu'à",
"benefit_dollar_backed": "Sécurisez vos fonds en achetant des mUSD, un stablecoin adossé au dollar à un ratio de 1:1",
"benefit_liquidity": "Profitez d’une liquidité totale sans période de blocage, vous pourrez ainsi trader ou retirer des fonds à tout moment",
"benefit_spend_prefix": "Effectuez vos achats chez plus de 150 millions de commerçants avec la MetaMask Card et bénéficiez de ",
"benefit_spend_cashback": "1 à 3 % de cashback en mUSD",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "Effectuez des transferts vers n’importe lequel de vos portefeuilles MetaMask",
+ "benefit_global": "Envoyez et recevez des fonds partout dans le monde",
"learn_more": "En savoir plus"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "Ajouter des fonds"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "Ajouter des fonds",
"convert_crypto": "Convertir des crypto-monnaies",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "Depuis n’importe quel compte",
"deposit_funds": "Déposer des fonds",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "Depuis une carte de débit ou un compte bancaire",
+ "move_musd": "Transférez vos {{amount}} mUSD",
+ "move_musd_no_amount": "Transférez vos mUSD",
+ "move_musd_description": "Depuis votre solde",
"receive_external": "Recevoir depuis un portefeuille externe",
"coming_soon": "Bientôt disponible"
},
@@ -6694,7 +6792,7 @@
"contact_support": "Contacter le service d’assistance"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "Transférez des fonds",
"between_accounts": "Entre comptes",
"perps_account": "Compte PERPS",
"predictions_account": "Compte « Prédictions »",
@@ -6712,8 +6810,8 @@
"body": "Estimation du montant que vous pourriez gagner sur une période donnée, basée sur votre solde actuel et le taux de rendement annuel applicable aujourd’hui. Les estimations ne constituent pas des rendements garantis et sont susceptibles de changer."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "Gagnez de l’argent grâce à vos crypto-monnaies",
+ "body": "L’illustration suppose que le taux de rendement annuel de {{percentage}} % restera inchangé pendant un an. Le taux de rendement annuel est variable et peut changer pour diverses raisons. Il n’y a aucune garantie de rendement."
},
"activity": {
"title": "Activité",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "Comment ça marche ",
- "how_it_works_subtitle": "Découvrez comment votre argent travaille pour vous",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "En savoir plus sur le mUSD",
"what_you_get_title": "Ce que vous obtenez",
@@ -6738,7 +6836,8 @@
"sent": "Envoyé(s)",
"transferred": "Transféré",
"card_transaction": "Transaction Card",
- "converted": "Converti"
+ "converted": "Converti",
+ "failed": "Échec"
},
"convert_stablecoins": {
"title": "Convertissez vos stablecoins",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "Money",
"section_title": "Comment ça marche ",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "Déposez des mUSD sur votre compte Money et bénéficiez automatiquement d’un taux de rendement annuel (variable) pouvant atteindre les {{percentage}} %. Les fonds sont placés dans un coffre-fort DeFi qui génère des rendements sur des marchés de prêt audités — sans staking, sans réclamation, sans gel de fonds.",
"description_2": "Le solde de votre compte Money correspond à votre solde disponible. Associez votre carte MetaMask Card pour effectuer des achats auprès de plus de 150 millions de commerçants dans le monde entier. Votre argent continue de générer des revenus jusqu’à ce que vous le dépensiez.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "Foire aux questions",
"faq_placeholder_answer": "Bientôt disponible.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "Comment fonctionne le taux de rendement annuel de {{percentage}} % ?",
"faq_q2": "Qu’est-ce que le mUSD ?",
"faq_q3": "D’où provient le rendement ?",
"faq_q4": "Mon argent est-il bloqué ? Puis-je effectuer un retrait à tout moment ?",
@@ -7030,11 +7129,12 @@
"confirm": "Confirmer",
"pay_with_bottom_sheet": {
"title": "Payer avec",
- "last_used": "Last used",
+ "last_used": "Dernière utilisation",
+ "bank_and_card": "Banque et carte",
"crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "available_balance": "{{balance}} disponibles",
+ "other_assets": "Autres actifs",
+ "other_assets_description": "Sélectionnez parmi vos jetons"
},
"staking_footer": {
"part1": "En continuant, vous acceptez nos ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "Les frais de conversion en mUSD comprennent les coûts de réseau et peuvent inclure des frais de fournisseur. Aucuns frais MetaMask ne s’appliquent lorsque vous effectuez un échange contre des mUSD."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMask échangera vos mUSD contre le jeton de votre choix. Les prestataires de services d’échange peuvent facturer des frais, mais MetaMask ne le fera pas."
},
"title": {
"transaction_fee": "Frais"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "Ajouter des fonds",
"deposit_edit_amount_predict_withdraw": "Retirer",
"deposit_edit_amount_musd_conversion": "Convertir en mUSD",
- "preparing_order": "Preparing order"
+ "preparing_order": "Préparation de l’ordre"
},
"change_in_simulation_modal": {
"title": "Les résultats ont changé",
@@ -7250,20 +7350,33 @@
"confirm_swap": "Échanger",
"terms_and_conditions": "Conditions générales",
"select_token": "Sélectionnez un jeton",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "Sélectionnez jusqu’à 5 jetons",
+ "batch_sell_select_subtitle": "Tous les jetons doivent être sur le même réseau.",
+ "batch_sell_empty_state_title": "Vous n’avez pas de jetons ? Pas de problème.",
+ "batch_sell_empty_state_description": "Vous n’avez pas de jetons ? Pas de problème. Découvrez et achetez des jetons pour les vendre par lot.",
+ "batch_sell_continue_with_one_token": "Continuer avec (1) jeton",
+ "batch_sell_continue_with_tokens": "Continuer avec ({{tokenCount}}) jetons",
+ "batch_sell_max_tokens_allowed": "5 jetons au maximum",
+ "batch_sell_single_token_dialog_title": "Alerte de taux élevé",
+ "batch_sell_single_token_dialog_description": "La vente par lot d’un jeton pourrait entraîner un taux plus élevé. Souhaitez-vous effectuer un échange à la place ?",
+ "batch_sell_swap_instead": "Oui, procéder à l’échange",
+ "batch_sell_review_title": "Examen de la vente par lot",
+ "batch_sell_select_stablecoin": "Sélectionnez un stablecoin",
+ "batch_sell_total_received": "Total reçu",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "Examiner",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "Personnaliser {{tokenSymbol}}",
+ "batch_sell_remove_token": "Supprimer {{tokenSymbol}}",
+ "batch_sell_checkbox_label": "Vendre le jeton par lot",
+ "sort_balance": "Solde",
+ "next": "Suivant",
+ "explore_tokens": "Découvrir les jetons",
"no_tokens_found": "Aucun jeton trouvé",
"no_tokens_found_description": "Nous n’avons trouvé aucun jeton portant ce nom. Veuillez essayer un autre critère de recherche.",
"select_network": "Sélectionnez le réseau",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "Vous perdrez environ {{priceImpact}} de la valeur de vos jetons lors de cet échange. Essayez de réduire le montant ou de choisir une voie plus liquide.",
"proceed": "Continuer",
"cancel": "Annuler",
+ "close": "Fermer",
"slippage_info_title": "Slippage/effet de glissement",
"slippage_info_description": "Le pourcentage de variation de prix que vous êtes prêt à accepter avant que votre transaction ne soit annulée.",
"blockaid_error_title": "Cette transaction sera annulée",
@@ -8107,7 +8221,14 @@
"retry": "Réessayez",
"on_linea": "sur Linea",
"account_label": "Compte",
- "token_label": "Jeton"
+ "token_label": "Jeton",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "Cashback en mUSD",
@@ -8335,8 +8456,8 @@
},
"main_title": "Récompenses",
"vip": {
- "bps_unit": "bps",
- "swaps_label": "Swaps",
+ "bps_unit": "points de base",
+ "swaps_label": "Échanges",
"perps_label": "Perps",
"error_title": "We couldn’t load your VIP dashboard",
"error_description": "Check your connection and try again.",
@@ -8445,6 +8566,7 @@
"show_less": "Afficher moins",
"linking_progress": "Ajout des comptes… ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} inscrit(s)",
+ "accounts_added": "Accounts added",
"add_all_accounts": "Ajouter tous les comptes",
"environment_selector": "Environnement",
"environment_cancel": "Annuler",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "Supprimer la progression Rewards",
- "description": "Cela supprimera vos comptes du programme de récompenses et effacera votre progression. Cette action est irréversible.",
- "confirm": "Supprimer",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "En êtes-vous sûr(e) ?",
- "confirmation_description": "Cette opération supprimera toute votre progression et ne pourra pas être annulée. Si vous rejoignez le programme « Rewards » ultérieurement, vous repartirez de zéro.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "Annuler",
"confirm": "Confirmer",
+ "error_title": "Quelque chose a mal tourné",
"error_message": "Échec de la désinscription du programme « Rewards ». Veuillez réessayer.",
"processing": "Traitement en cours..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "Ignorer",
@@ -8562,7 +8691,8 @@
"title_claim": "Réclamer l’avantage",
"action": "Réclamer",
"empty-list": "Vous ne disposez d’aucun avantage pour le moment.",
- "powered_by": "Fourni par"
+ "powered_by": "Fourni par",
+ "available_count": "{{count}} disponible(s)"
},
"end_of_season_rewards": {
"confirm_label_default": "Confirmer",
@@ -8695,7 +8825,7 @@
"label_your_rank": "Votre classement",
"label_portfolio": "Portfolio",
"label_net_inflow": "Flux net entrant",
- "label_total_inflow": "Entrées totales",
+ "label_total_inflow": "Apport total",
"label_outflow": "Flux sortant",
"label_days_held": "Jours de détention",
"qualified_title": "Vous êtes qualifié",
@@ -8880,9 +9010,11 @@
"musd_claim": "Réclamer mes mUSD",
"perps_deposit": "Ajouter des fonds",
"perps_withdraw": "Retrait",
+ "predict_withdraw": "Retirer {{sourceSymbol}} de {{sourceChain}}",
"predict_deposit": "Ajouter des fonds",
"swap": "Échanger des jetons",
- "swap_approval": "Approuver les jetons"
+ "swap_approval": "Approuver les jetons",
+ "fiat_purchase": "Acheter des{{token}} avec {{paymentMethod}}"
},
"perps_deposit_solution": "Vous disposez désormais de {{fiat}} d’USDC sur Arbitrum. Réessayez d’effectuer un dépôt."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "Du plus élevé au plus bas",
"low_to_high": "Du plus bas au plus élevé",
"apply": "Appliquer",
- "search_placeholder": "Rechercher des jetons, des sites, des URL",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "Annuler",
"perps": "Perps",
"rwa_perps_section": "Perps",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "Perps",
"predictions": "Prédictions",
"no_results": "Aucun résultat trouvé",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "Populaire",
"sites": "Sites",
"popular_sites": "Sites populaires",
"search_sites": "Rechercher des sites",
"view_all": "Tout afficher",
+ "view_more": "Afficher plus",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "Activer les fonctionnalités de base",
"basic_functionality_disabled_title": "La section « Explorer » n’est pas disponible",
"basic_functionality_disabled_description": "Nous ne pouvons pas récupérer les métadonnées requises lorsque les fonctionnalités de base sont désactivées.",
@@ -8995,6 +9131,14 @@
"crypto": "Crypto",
"sports": "Sports",
"dapps": "Sites"
+ },
+ "search_tabs": {
+ "all": "Tous",
+ "crypto": "Cryptos",
+ "perps": "Perps",
+ "stocks": "Actions",
+ "predictions": "Prédictions",
+ "sites": "Sites"
}
},
"ota_update_modal": {
@@ -9132,33 +9276,19 @@
"money_empty_description_network_filter": "Pas de mUSD sur ce réseau. Changez de réseau pour consulter votre solde de mUSD.",
"money_empty_state": {
"get_started": "Commencer",
+ "earn": "Gagner",
"earn_apy": "Obtenez un taux de rendement annuel de {{percentage}} %"
},
"money_filled_state": {
"add": "Ajouter",
- "apy": "Taux de rendement annuel de {{percentage}} %"
+ "apy": "APY {{percentage}} %"
},
"tokens": "Jetons",
"perps": "Perps",
"related_assets": "Actifs associés",
"perpetuals": "Contrats perpétuels",
"predictions": "Prédictions",
- "whats_happening": "Actualités",
- "whats_happening_ai": "IA",
- "whats_happening_impact": {
- "bullish": "Haussier",
- "bearish": "Baissier",
- "neutral": "Neutre"
- },
"top_traders": "Meilleurs traders",
- "whats_happening_categories": {
- "geopolitical": "Géopolitique",
- "macro": "Macroéconomie",
- "regulatory": "Réglementation",
- "technical": "Technique",
- "social": "Réseaux sociaux",
- "other": "Autre"
- },
"defi": "DeFi",
"nfts": "NFT",
"trending_tokens": "Tendance",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "Populaire"
+ },
+ "whats_happening": {
+ "title": "Actualités",
+ "ai": "IA",
+ "impact": {
+ "bullish": "Haussier",
+ "bearish": "Baissier",
+ "neutral": "Neutre"
+ },
+ "categories": {
+ "geopolitical": "Géopolitique",
+ "macro": "Macroéconomie",
+ "regulatory": "Réglementation",
+ "technical": "Technique",
+ "social": "Réseaux sociaux",
+ "other": "Autre"
+ }
}
}
diff --git a/locales/languages/hi.json b/locales/languages/hi.json
index e5434929124a..1279c328a27b 100644
--- a/locales/languages/hi.json
+++ b/locales/languages/hi.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "कोई फंड उपलब्ध नहीं है। कृपया किसी अन्य अकाउंट का इस्तेमाल करें।"
},
+ "headless_buy_error": {
+ "title": "फिएट की खरीद नहीं हो पाई",
+ "message": "आपकी फिएट खरीदारी में कुछ गड़बड़ हो गई है। कृपया फिर से कोशिश करें।"
+ },
"mmpay_hardware_account": {
"title": "वॉलेट सपोर्टेड नहीं है",
"message": "हार्डवेयर वॉलेट सपोर्टेड नहीं हैं।\nजारी रखने के लिए वॉलेट बदलें।"
@@ -133,8 +137,8 @@
"message": "हो सकता है कि पाने वाले का पता सीधे टोकन ट्रांसफर को सपोर्ट न करे, जिससे फंड का नुकसान हो सकता है। तभी आगे बढ़ें जब आपको पक्का हो कि यह कॉन्ट्रैक्ट आपका ट्रांसफर प्राप्त कर सकता है।"
},
"gas_sponsorship_reserve_balance": {
- "message": "इस ट्रांसेक्शन के लिए गैस स्पॉन्सरशिप उपलब्ध नहीं है। आपको अपने अकाउंट में कम से कम %{minBalance} %{nativeTokenSymbol} रखना होगा।",
- "title": "गैस स्पॉन्सरशिप उपलब्ध नहीं है"
+ "message": "इस खास नेटवर्क को आपके अकाउंट में %{minBalance} %{nativeTokenSymbol} का रिज़र्व बनाए रखना ज़रूरी है।",
+ "title": "रिज़र्व बैलेंस ज़रूरी है"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "आप टोकन को टोकन के कॉन्ट्रैक्ट एड्रेस पर भेज रहे हैं। इससे इन टोकन को खोने की संभावना है।",
"smart_contract_address": "स्मार्ट कॉन्ट्रैक्ट एड्रेस",
"smart_contract_address_warning": "हो सकता है कि पाने वाले का पता सीधे टोकन ट्रांसफर को सपोर्ट न करे, जिससे फंड का नुकसान हो सकता है। तभी आगे बढ़ें जब आपको पक्का हो कि यह कॉन्ट्रैक्ट आपका ट्रांसफर प्राप्त कर सकता है।",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "अपडेट करें",
"i_understand": "मैं समझता हूं",
"cancel": "कैंसिल करें",
"new_address_title": "नया एड्रेस",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "वैल्यू",
"pnl_percent": "पी एंड एल %",
- "recent": "Recent"
+ "recent": "हालिया"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "{{symbol}} पर्प ट्रेड करें",
"subtitle": "अपने P&L को {{leverage}} तक कई गुना बढ़ाएँ"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "सपोर्ट टीम से कॉन्टेक्ट करें"
+ },
"today": "आज",
"yesterday": "कल",
"unrealized_pnl": "अनरियलाइज्ड P&L",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC आपके वॉलेट में भेज दिया गया",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} आपके वॉलेट में भेजा गया",
"toast_error_title": "कुछ गलत हो गया",
- "toast_error_description": "विदड्रॉवल नहीं हो पाया"
+ "toast_error_description": "विदड्रॉवल नहीं हो पाया",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "फिर से प्रयास करें"
},
"quote": {
"network_fee": "नेटवर्क फीस",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "MetaMask प्रिडिक्शन्स",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "वर्ल्ड कप",
+ "banner_title": "वर्ल्ड कप 2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
- "props": "Props",
- "live": "Live"
+ "all": "सभी",
+ "props": "प्रॉप्स",
+ "live": "लाइव"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
- "final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_stage": "ग्रुप स्टेज",
+ "round_of_32": "राउंड ऑफ़ 32",
+ "round_of_16": "राउंड ऑफ़ 16",
+ "quarterfinals": "क्वार्टरफ़ाइनल्स",
+ "semifinals": "सेमीफ़ाइनल्स",
+ "third_place": "थर्ड प्लेस",
+ "final": "फाइनल",
+ "group_a": "ग्रुप ए",
+ "group_b": "ग्रुप बी",
+ "group_c": "ग्रुप सी",
+ "group_d": "ग्रुप डी",
+ "group_e": "ग्रुप ई",
+ "group_f": "ग्रुप एफ",
+ "group_g": "ग्रुप जी",
+ "group_h": "ग्रुप एच",
+ "group_i": "ग्रुप आई",
+ "group_j": "ग्रुप जे",
+ "group_k": "ग्रुप के",
+ "group_l": "ग्रुप एल",
+ "third_place_match": "थर्ड प्लेस मैच"
}
},
"prediction_markets": "प्रेडिक्शन मार्केट",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "विदड्रॉवल पूरा हुआ",
"withdraw_completed_subtitle": "{{amount}} USDC आपके वॉलेट में भेज दिया गया",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}} आपके वॉलेट में भेजा गया",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "अभी विदड्रॉवल उपलब्ध नहीं है",
+ "unavailable_description": "अर्जेंट मदद के लिए, कृपया कस्टमर सर्विस से संपर्क करें।",
"unavailable_got_it": "समझ गए",
"error_title": "कुछ गलत हो गया",
"error_description": "निकालना जारी नहीं हो पाया",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "स्वीकार करें और बंद करें",
"pna25_open_settings_button": "सेटिंग्स खोलें"
},
+ "onboarding_interest_questionnaire": {
+ "title": "आप MetaMask के साथ क्या करना चाहते हैं?",
+ "description": "जो भी लागू हो, उसे चुनें।",
+ "option_buy_and_sell_crypto": "क्रिप्टो खरीदें और बेचें",
+ "option_consolidate_wallets": "अपने वॉलेट को कंसोलिडेट करें",
+ "option_advanced_trades": "एडवांस्ड ट्रेड करें",
+ "option_predict_sports_events": "स्पोर्ट्स और इवेंट्स प्रेडिक्ट करें",
+ "option_crypto_as_money": "क्रिप्टो को पैसे की तरह इस्तेमाल करें",
+ "option_connect_apps_sites": "ऐप्स या साइट से कनेक्ट करें",
+ "continue": "जारी रखें"
+ },
"template_confirmation": {
"ok": "ठीक है",
"cancel": "कैंसिल करें"
@@ -3261,8 +3286,27 @@
"notifications_desc": "अपने नोटिफिकेशंस मैनेज करें",
"allow_notifications": "नोटिफिकेशंस की अनुमति दें",
"enable_push_notifications": "पुश नोटिफिकेशंस को चालू करें",
- "allow_notifications_desc": "नोटिफिकेशंस के साथ आपके वॉलेट में क्या हो रहा है, इसकी जानकारी रखें। नोटिफिकेशंस का उपयोग करने के लिए, हम आपके डिवाइस में कुछ सेटिंग्स को सिंक करने के लिए एक प्रोफ़ाइल का उपयोग करते हैं।",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "बंद",
+ "select_all": "सभी को चुनें",
+ "deselect_all": "सभी को अचयनित करें",
+ "select_accounts_title": "एकाउंट्स",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "अपने नोटिफिकेशंस कस्टमाइज़ करें",
"customize_session_desc": "आप जिस प्रकार के नोटिफिकेशंस प्राप्त करना चाहते हैं, उन्हें चालू करें:",
"account_session_title": "अकाउंट एक्टिविटी",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snaps",
"snaps_desc": "नए फीचर्स और अपडेट",
"products_announcements_title": "प्रॉडक्ट घोषणाएं",
- "products_announcements_desc": "नए प्रॉडक्ट और फीचर्स",
- "perps_title": "पर्प्स ट्रेडिंग"
+ "products_announcements_desc": "नए प्रॉडक्ट और फीचर्स"
},
"contacts_title": "संपर्क",
"contacts_desc": "अपने अकाउंट को बदलें, हटाएं, और मैनेज करें।",
@@ -3640,7 +3683,15 @@
"card": {
"title": "कार्ड",
"reset_onboarding_description": "ऑनबोर्डिंग प्रक्रिया को शुरुआत से दोबारा शुरू करने के लिए कार्ड की ऑनबोर्डिंग स्टेट रीसेट करें।",
- "reset_onboarding_button": "ऑनबोर्डिंग स्टेट रीसेट करें"
+ "reset_onboarding_button": "ऑनबोर्डिंग स्टेट रीसेट करें",
+ "unlink_money_account_description": "यूएसडीसी खर्च-लिमिट अलाउंस को रद्द करें जो कार्ड को आपके मनी अकाउंट से खर्च करने की अनुमति देता है। यह एक अप्रूव(0) ट्रांसेक्शन सबमिट करता है और मनी अकाउंट को कार्ड बैकएंड पर नॉट-डेलीगेटेड के रूप में मार्क करता है।",
+ "unlink_money_account_button": "कार्ड से मनी अकाउंट को अनलिंक करें",
+ "unlink_money_account_disabled_hint": "हटाने के लिए कोई एक्टिव लिंकेज नहीं है।"
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "हैप्टिक्स",
@@ -3842,8 +3893,8 @@
"predict_button": "प्रेडिक्शंस",
"add_collectible_button": "जोड़ें",
"info": "जानकारी",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "बैच सेल",
+ "batch_sell_new_label": "नया",
"swap": "स्वैप",
"convert": "कन्वर्ट करें",
"bridge": "ब्रिज",
@@ -3883,7 +3934,7 @@
"troubleshoot": "ट्रबलशूट",
"deposit_description": "कम शुल्क वाले बैंक या कार्ड ट्रांसफर",
"buy_description": "एक विशिष्ट टोकन खरीदने के लिए अच्छा है",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "एक स्टेबलकॉइन के लिए 5 टोकन तक बेचें",
"sell_description": "कैश के लिए क्रिप्टो बेचें",
"swap_description": "टोकन के बीच विनिमय",
"bridge_description": "अलग-अलग नेटवर्कों के बीच टोकन ट्रांसफर करें",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "सेटिंग्स > नोटीफिकेशंस।",
"cancel": "कैंसिल करें",
"cta": "चालू करें"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "कभी भी कोई मूव मिस न करें",
+ "body": "जब कीमतें आपके टारगेट तक पहुँचें, आपके ट्रेड कन्फर्म हों, और आपका पोर्टफ़ोलियो मूव करे तो रियल-टाइम अलर्ट पाएँ। साथ ही, प्रोडक्ट अपडेट और रिवॉर्ड भी मिलते रहेंगे। हम MetaMask के साथ आपके इंटरैक्शन के आधार पर अपडेट शेयर करेंगे।",
+ "button_yes": "हां",
+ "button_not_now": "अभी नहीं",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "अब",
+ "title": "ETH आज 4.2% बढ़ा",
+ "message": "अब $2,668.51 पर — आपके प्राइस अलर्ट से ऊपर"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "1 घंटा पहले",
+ "title": "0.25 ETH मिला",
+ "message": "0x9a21…4f8c · $640.29 से"
+ }
+ },
+ "existing_user": {
+ "title": "पेश है पर्सनलाइज़्ड अलर्ट",
+ "body": "अपने ट्रेड करने के तरीके से मैच करने वाले नोटिफ़िकेशन पाएं। कभी भी अपडेट करें।",
+ "card_title": "आपको क्या मिलता है",
+ "card_description": "आपकी ट्रेडिंग एक्टिविटी के हिसाब से पर्सनलाइज़्ड अलर्ट और अपडेट।",
+ "button_confirm": "कन्फर्म करें",
+ "button_not_now": "अभी नहीं"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "के साथ भुगतान करें",
"buying_via": "{{providerName}} के ज़रिए खरीद रहे हैं।",
"change_provider": "प्रोवाइडर बदलें।",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "कुछ गलत हो गया। कृपया फिर से कोशिश करें।",
"no_payment_methods_available": "पेमेंट का कोई तरीका उपलब्ध नहीं है।",
"error_fetching_quotes": "कुछ गलत हो गया। कृपया फिर से कोशिश करें।",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "mUSD में कन्वर्ट करें",
"get_a_percentage_musd_bonus": "{{percentage}}% mUSD बोनस पाएं",
"convert": "कन्वर्ट करें",
+ "confirm": "कन्फर्म करें",
+ "convert_tooltip_description": "अपने स्टेबलकॉइन्स को mUSD में बदलें और {{percentage}}% तक वार्षिक बोनस कमाएं, जिसे आप रोज़ाना क्लेम कर सकते हैं। रिले द्वारा पावर्ड।",
"fetching_quote": "कोटेशन लाया जा रहा है...",
"you_convert": "आप बदलते हैं",
"network_fee": "नेटवर्क फीस",
@@ -6597,7 +6679,7 @@
"step_progress": "{{total}} का स्टेप {{current}}",
"title": "धन जोड़ें",
"description": "अपना अकाउंट फंड करें और APY कमाना शुरू करें।",
- "add": "जोड़ें",
+ "add": "फंड जोड़ें",
"step2_title": "अपना MetaMask कार्ड पाएं",
"step2_description": "अपना Money बैलेंस पाने के साथ उसे खर्च करें, जहां भी मास्टरकार्ड स्वीकार किया जाता हो।",
"step2_cta": "कार्ड पाएं",
@@ -6605,6 +6687,19 @@
"link_card_description": "अपना बैलेंस पाने के साथ उसे खर्च करें, जहां भी मास्टरकार्ड स्वीकार किया जाता हो।",
"link_card_cta": "कार्ड लिंक करें"
},
+ "rive_onboarding": {
+ "step1_title": "मनी अकाउंट यहां हैं",
+ "step1_body": "अपने बैलेंस पर {{percentage}}% APY तक कमाएं, जो आपके पूरे वॉलेट में उपलब्ध है।",
+ "step1_footer_text": "APY वेरिएबल है और कभी भी बदल सकता है।",
+ "step2_title": "ऑटोमैटिकली कमाएं",
+ "step2_body": "बिना किसी एक्सचेंज फ़ीस के स्टेबलकॉइन मूव करें। फ़ंड तुरंत कमाना शुरू कर देते हैं।",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "कहीं भी खर्च करें",
+ "step3_body": "अपने मनी अकाउंट को MetaMask कार्ड से लिंक करके खरीदारी पर {{percentage}}% तक वापस पाएं।",
+ "step4_title": "एक ही जगह पर ट्रेड करें और कमाएं",
+ "step4_body": "अपने मनी बैलेंस का इस्तेमाल MetaMask पर ट्रेड करने के लिए करें और साथ ही यील्ड भी कमाएं।",
+ "continue": "जारी रखें"
+ },
"action": {
"add": "जोड़ें",
"transfer": "स्थानांतरण",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "ये कैसे काम करता है",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "अपने मनी अकाउंट में mUSD डिपॉज़िट करें और कमाएं",
+ "description_suffix": ". आपका बैलेंस डॉलर-बैक्ड है और कभी भी खर्च करने, ट्रेड करने या भेजने के लिए तैयार है।"
},
"musd_row": {
"add": "जोड़ें"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Money बैलेंस",
"add": "जोड़ें",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Money बैलेंस",
+ "info_sheet_body": "आपका डॉलर-बैक्ड mUSD बैलेंस जो खर्च करने, भेजने या कभी भी ट्रेड करने के लिए हमेशा उपलब्ध रहता है। हम इसे आपके टोटल अकाउंट बैलेंस में कैलकुलेट नहीं करते हैं।\n\nविदड्रॉवल तुरंत प्रोसेस होता है, जो संबंधित ब्लॉकचेन पर स्टैंडर्ड नेटवर्क कन्फर्मेशन टाइम पर निर्भर करता है। अगर लिक्विडिटी टाइट है, तो कुछ समय के लिए देरी हो सकती है।"
},
"potential_earnings": {
"title": "अपने क्रिप्टो पर कमाएं",
"description": "देखें कि अपने क्रिप्टो को mUSD में बदलकर आपका पैसा समय के साथ कैसे बढ़ सकता है।",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "अपने {{total}} को एसेट्स में कन्वर्ट करें और आप कमा सकते हैं",
+ "description_with_amounts_suffix": "एक साल में।",
"convert": "कन्वर्ट करें",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "अपने क्रिप्टो को कन्वर्ट करें",
"no_fee": "कोई MetaMask फीस नहीं",
"view_all": "सभी देखें",
"view_potential_earnings": "संभावित कमाई देखें"
@@ -6649,41 +6744,44 @@
"cashback": "{{percentage}}% mUSD वापस",
"get_now": "अभी पाएं",
"link_title": "MetaMask कार्ड लिंक करें",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "अपना मनी बैलेंस खर्च करें और खरीदारी पर कमाएं। साथ ही, आपके बैलेंस पर {{apy}}% APY तक।",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "{{percentage}}% mUSD वापस पाएं",
+ "link_bullet_apy": "{{apy}}% APY तक कमाएं",
"link_card": "कार्ड लिंक करें",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "खर्च करें और कमाएँ",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "कार्ड लिंक करें",
+ "manage_card": "मैनेज करें",
+ "avail_balance": "उपलब्ध बैलेंस"
},
"what_you_get": {
"title": "आपको क्या मिलता है",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "तक ऑटो-अर्न करें",
"benefit_dollar_backed": "अपना पैसा mUSD में रखें, जो एक 1:1 डॉलर-बैक्ड स्टेबलकॉइन है",
"benefit_liquidity": "बिना किसी लॉकअप के पूरी लिक्विडिटी पाएं, इसलिए आप कभी भी ट्रेड कर सकते हैं या निकाल सकते हैं",
"benefit_spend_prefix": "MetaMask कार्ड के साथ 150M+ मर्चेंट पर खर्च करें और कमाएँ ",
"benefit_spend_cashback": "1-3% mUSD वापस",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "MetaMask पर अपने किसी भी वॉलेट में ट्रांसफर करें",
+ "benefit_global": "दुनिया भर में पैसे भेजें और प्राप्त करें",
"learn_more": "ज़्यादा जानें"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "फंड जोड़ें"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "फंड जोड़ें",
"convert_crypto": "क्रिप्टो को कन्वर्ट करें",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "किसी भी अकाउंट से",
"deposit_funds": "फंड्स डिपॉज़िट करें",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "डेबिट कार्ड या बैंक से",
+ "move_musd": "अपना {{amount}} mUSD ट्रांसफर करें",
+ "move_musd_no_amount": "अपना mUSD ट्रांसफर करें",
+ "move_musd_description": "आपके बैलेंस से",
"receive_external": "बाहरी वॉलेट से प्राप्त करें",
"coming_soon": "जल्द आ रहा है"
},
@@ -6694,7 +6792,7 @@
"contact_support": "सपोर्ट टीम से कॉन्टेक्ट करें"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "पैसा ट्रांसफर करें",
"between_accounts": "एकाउंट्स के बीच",
"perps_account": "पर्प्स अकाउंट",
"predictions_account": "प्रेडिक्शंस अकाउंट",
@@ -6712,8 +6810,8 @@
"body": "आपके वर्तमान बैलेंस और आज के APY के आधार पर किसी अवधि में आपकी संभावित कमाई का अनुमान। यह अनुमानित रिटर्न की गारंटी नहीं है और इसमें बदलाव हो सकता है।"
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "अपने क्रिप्टो पर कमाएं",
+ "body": "उदाहरण में माना गया है कि {{percentage}}% एनुअल परसेंटेज यील्ड (APY) एक साल तक नहीं बदलेगा। APY बदलता रहता है और कई वजहों से बदल सकता है। रिटर्न की कोई गारंटी नहीं है।"
},
"activity": {
"title": "गतिविधि",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "ये कैसे काम करता है",
- "how_it_works_subtitle": "देखें कि आपका पैसा आपके लिए कैसे काम करता है",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "mUSD के बारे में ज्यादा जानें",
"what_you_get_title": "आपको क्या मिलता है",
@@ -6738,7 +6836,8 @@
"sent": "भेजा गया",
"transferred": "ट्रांसफर किया गया",
"card_transaction": "कार्ड ट्रांसेक्शन",
- "converted": "कन्वर्ट किया गया"
+ "converted": "कन्वर्ट किया गया",
+ "failed": "नहीं हो पाया"
},
"convert_stablecoins": {
"title": "अपने स्टेबलकॉइन कन्वर्ट करें",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "वित्त",
"section_title": "ये कैसे काम करता है",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "अपने मनी अकाउंट में mUSD जमा करें और ऑटोमेटिकली {{percentage}}% APY (बदलने वाला) कमाएँ। फंड एक DeFi वॉल्ट में जाते हैं जो ऑडिट किए गए लेंडिंग मार्केट में रिटर्न देता है—कोई स्टेकिंग नहीं, कोई क्लेमिंग नहीं, कोई लॉक-अप नहीं।",
"description_2": "आपका Money बैलेंस आपका खर्च करने का बैलेंस है। दुनिया भर में 150M+ मर्चेंट पर खर्च करने के लिए अपना MetaMask कार्ड लिंक करें। आपका पैसा तब तक कमाता रहता है जब तक आप इसका इस्तेमाल नहीं करते।",
- "faq_title": "Frequently asked questions",
+ "faq_title": "अक्सर पूछे जाने वाले सवाल",
"faq_placeholder_answer": "जल्द आ रहा है।",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "{{percentage}}% APY कैसे काम करता है?",
"faq_q2": "mUSD क्या है?",
"faq_q3": "यील्ड कहाँ से आती है?",
"faq_q4": "क्या मेरा पैसा लॉक हो गया है? क्या मैं कभी भी निकाल सकता हूँ?",
@@ -7030,11 +7129,12 @@
"confirm": "कन्फर्म करें",
"pay_with_bottom_sheet": {
"title": "के साथ भुगतान करें",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "पिछली बार इस्तेमाल किया गया",
+ "bank_and_card": "बैंक और कार्ड",
+ "crypto": "क्रिप्टो",
+ "available_balance": "{{balance}} उपलब्ध है",
+ "other_assets": "अन्य एसेट्स",
+ "other_assets_description": "अपने टोकन में से चुनें"
},
"staking_footer": {
"part1": "जारी रखकर, आप हमारी ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "mUSD कन्वर्शन शुल्क में नेटवर्क लागत शामिल होती है और इसमें प्रदाता शुल्क भी शामिल हो सकता है। जब आप mUSD में कन्वर्ट करते हैं, तो कोई MetaMask शुल्क लागू नहीं होता।"
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMask आपके mUSD को आपके मनचाहे टोकन से स्वैप करेगा। स्वैप प्रोवाइडर फीस ले सकते हैं, लेकिन MetaMask नहीं लेगा।"
},
"title": {
"transaction_fee": "फीस"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "फंड जोड़ें",
"deposit_edit_amount_predict_withdraw": "निकालें",
"deposit_edit_amount_musd_conversion": "mUSD में कन्वर्ट करें",
- "preparing_order": "Preparing order"
+ "preparing_order": "ऑर्डर तैयार किया जा रहा है"
},
"change_in_simulation_modal": {
"title": "परिणाम बदल गए हैं",
@@ -7250,20 +7350,33 @@
"confirm_swap": "स्वैप करें",
"terms_and_conditions": "नियम और शर्त",
"select_token": "टोकन चुनें",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "5 टोकन तक चुनें",
+ "batch_sell_select_subtitle": "सभी टोकन एक ही नेटवर्क पर होने चाहिए।",
+ "batch_sell_empty_state_title": "कोई टोकन नहीं। कोई दिक्कत नहीं।",
+ "batch_sell_empty_state_description": "कोई टोकन नहीं। कोई प्रॉब्लम नहीं। बैच सेल के लिए टोकन एक्सप्लोर करें और खरीदें।",
+ "batch_sell_continue_with_one_token": "(1) टोकन के साथ जारी रखें",
+ "batch_sell_continue_with_tokens": "({{tokenCount}}) टोकन के साथ जारी रखें",
+ "batch_sell_max_tokens_allowed": "ज़्यादा से ज़्यादा 5 टोकन अलाउड हैं",
+ "batch_sell_single_token_dialog_title": "हाई रेट अलर्ट",
+ "batch_sell_single_token_dialog_description": "एक टोकन बैच में बेचने पर रेट ज़्यादा हो सकता है। क्या आप इसके बजाय स्वैप करना चाहते हैं?",
+ "batch_sell_swap_instead": "हाँ, स्वैप करें",
+ "batch_sell_review_title": "बैच सेल रिव्यू",
+ "batch_sell_select_stablecoin": "एक स्टेबलकॉइन चुनें",
+ "batch_sell_total_received": "टोटल प्राप्त हुआ",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "समीक्षा करें",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "{{tokenSymbol}} कस्टमाइज़ करें",
+ "batch_sell_remove_token": "{{tokenSymbol}} हटाएं",
+ "batch_sell_checkbox_label": "बैच सेल टोकन",
+ "sort_balance": "बैलेंस",
+ "next": "आगे",
+ "explore_tokens": "टोकन एक्सप्लोर करें",
"no_tokens_found": "कोई टोकन नहीं मिला",
"no_tokens_found_description": "हमें इस नाम का कोई टोकन नहीं मिला। कोई एक अलग सर्च से कोशिश करें।",
"select_network": "नेटवर्क चुनें",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "इस स्वैप में आप अपने टोकन के मूल्य का लगभग {{priceImpact}} खो देंगे। राशि कम करने की कोशिश करें या अधिक लिक्विड रूट चुनें।",
"proceed": "आगे बढ़ें",
"cancel": "कैंसिल करें",
+ "close": "बंद करें",
"slippage_info_title": "स्लिपेज (slippage)",
"slippage_info_description": "ट्रांसेक्शन कैंसिल होने से पहले आप जो प्राइस में % परिवर्तन स्वीकार करने के लिए तैयार हैं।",
"blockaid_error_title": "इस ट्रांसेक्शन को वापस किया जाएगा",
@@ -8107,7 +8221,14 @@
"retry": "फिर से प्रयास करें",
"on_linea": "Linea पर",
"account_label": "अकाउंट",
- "token_label": "टोकन"
+ "token_label": "टोकन",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "mUSD वापस",
@@ -8445,6 +8566,7 @@
"show_less": "कम दिखाएं",
"linking_progress": "एकाउंट्स जोड़ा जा रहा है... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} नामांकन किया गया",
+ "accounts_added": "Accounts added",
"add_all_accounts": "सभी एकाउंट जोड़ें",
"environment_selector": "वातावरण",
"environment_cancel": "कैंसिल करें",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "रिवॉर्ड्स प्रगति हटाएँ",
- "description": "यह क्रिया आपके खाते को रिवॉर्ड्स प्रोग्राम से हटा देगी और आपकी प्रोग्रेस को मिटा देगी। यह कार्रवाई वापस नहीं की जा सकती।",
- "confirm": "हटाएँ",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "क्या आप सुनिश्चित हैं?",
- "confirmation_description": "इससे आपकी सभी प्रगति हट जाएगी और इसे वापस नहीं लाया जा सकता। यदि आप बाद में रिवॉर्ड्स प्रोग्राम में फिर से शामिल होते हैं, तो आपकी शुरुआत फिर से 0 से होगी।",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "कैंसिल करें",
"confirm": "कन्फर्म करें",
+ "error_title": "कुछ गलत हो गया",
"error_message": "रिवॉर्ड्स से ऑप्ट आउट नहीं हो पाया। कृपया फिर से प्रयास करें।",
"processing": "प्रोसेस हो रहा है..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "खारिज करें",
@@ -8562,7 +8691,8 @@
"title_claim": "फ़ायदे क्लेम करें",
"action": "क्लेम करें",
"empty-list": "अभी आपको कोई फ़ायदा नहीं मिल रहा है।",
- "powered_by": "के द्वारा पावर्ड"
+ "powered_by": "के द्वारा पावर्ड",
+ "available_count": "{{count}} उपलब्ध है"
},
"end_of_season_rewards": {
"confirm_label_default": "कन्फर्म करें",
@@ -8695,7 +8825,7 @@
"label_your_rank": "आपकी रैंक",
"label_portfolio": "Portfolio",
"label_net_inflow": "नेट इनफ्लो",
- "label_total_inflow": "कुल इनफ्लो",
+ "label_total_inflow": "टोटल इनफ्लो",
"label_outflow": "आउटफ्लो",
"label_days_held": "दिनों तक होल्ड किया",
"qualified_title": "आप क्वालिफ़ाई कर चुके हैं",
@@ -8880,9 +9010,11 @@
"musd_claim": "mUSD क्लेम करें",
"perps_deposit": "फंड जोड़ें",
"perps_withdraw": "विदड्रॉवल",
+ "predict_withdraw": "{{sourceSymbol}} को {{sourceChain}} से विदड्रॉ करें",
"predict_deposit": "फंड जोड़ें",
"swap": "टोकन स्वैप करें",
- "swap_approval": "टोकन एप्रूव करें"
+ "swap_approval": "टोकन एप्रूव करें",
+ "fiat_purchase": "{{paymentMethod}} के साथ {{token}} खरीदें"
},
"perps_deposit_solution": "अब आपके पास Arbitrum पर USDC का {{fiat}} है। कृपया अपना डिपॉज़िट फिर से प्रयास करें।"
},
@@ -8958,7 +9090,7 @@
"high_to_low": "ज़्यादा से कम",
"low_to_high": "कम से ज़्यादा",
"apply": "लागू करें",
- "search_placeholder": "टोकन, साइट, URL ढूंढें",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "कैंसिल करें",
"perps": "पर्प्स",
"rwa_perps_section": "पर्प्स",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "पर्प्स",
"predictions": "प्रेडिक्शंस",
"no_results": "कोई रिज़ल्ट नहीं मिला",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "लोकप्रिय",
"sites": "साइट्स",
"popular_sites": "पॉपुलर साइटें",
"search_sites": "साइट ढूंढें",
"view_all": "सभी देखें",
+ "view_more": "अधिक देखें",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "बेसिक फंक्शनलिटी को चालू करें",
"basic_functionality_disabled_title": "एक्सप्लोर उपलब्ध नहीं है",
"basic_functionality_disabled_description": "बेसिक फंक्शनलिटी बंद होने पर हम ज़रूरी मेटाडेटा नहीं ला सकते।",
@@ -8995,6 +9131,14 @@
"crypto": "क्रिप्टो",
"sports": "स्पोर्ट्स",
"dapps": "साइट्स"
+ },
+ "search_tabs": {
+ "all": "सभी",
+ "crypto": "Cryptos",
+ "perps": "पर्प्स",
+ "stocks": "स्टॉक्स",
+ "predictions": "प्रेडिक्शंस",
+ "sites": "साइट्स"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "इस नेटवर्क पर कोई mUSD नहीं है। अपना mUSD देखने के लिए नेटवर्क बदलें।",
"money_empty_state": {
"get_started": "शुरू करें",
+ "earn": "कमाएं",
"earn_apy": "{{percentage}}% APY कमाएं"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "संबंधित एसेट्स",
"perpetuals": "परपेचुअल्स",
"predictions": "प्रेडिक्शंस",
- "whats_happening": "क्या हो रहा है",
- "whats_happening_ai": "AI",
- "whats_happening_impact": {
- "bullish": "बुलिश",
- "bearish": "बेयरिश",
- "neutral": "न्यूट्रल"
- },
"top_traders": "टॉप ट्रेडर्स",
- "whats_happening_categories": {
- "geopolitical": "जियोपॉलिटिकल",
- "macro": "मैक्रो",
- "regulatory": "रेगुलेटरी",
- "technical": "टेक्निकल",
- "social": "सोशल",
- "other": "अन्य"
- },
"defi": "DeFi",
"nfts": "NFTs",
"trending_tokens": "ट्रेंडिंग",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "लोकप्रिय"
+ },
+ "whats_happening": {
+ "title": "क्या हो रहा है",
+ "ai": "AI",
+ "impact": {
+ "bullish": "बुलिश",
+ "bearish": "बेयरिश",
+ "neutral": "न्यूट्रल"
+ },
+ "categories": {
+ "geopolitical": "जियोपॉलिटिकल",
+ "macro": "मैक्रो",
+ "regulatory": "रेगुलेटरी",
+ "technical": "टेक्निकल",
+ "social": "सोशल",
+ "other": "अन्य"
+ }
}
}
diff --git a/locales/languages/id.json b/locales/languages/id.json
index 0d8e0ae595d2..e9634ac26b23 100644
--- a/locales/languages/id.json
+++ b/locales/languages/id.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "Dana tidak tersedia. Gunakan akun lain."
},
+ "headless_buy_error": {
+ "title": "Pembelian fiat gagal",
+ "message": "Terjadi kesalahan pada pembelian fiat. Coba lagi."
+ },
"mmpay_hardware_account": {
"title": "Dompet tidak didukung",
"message": "Dompet perangkat keras tidak didukung.\nGanti dompet untuk melanjutkan."
@@ -133,8 +137,8 @@
"message": "Alamat penerima mungkin tidak mendukung transfer token langsung, yang dapat mengakibatkan hilangnya dana. Lanjutkan hanya jika Anda yakin kontrak ini dapat menerima transfer."
},
"gas_sponsorship_reserve_balance": {
- "message": "Sponsor gas tidak tersedia untuk transaksi ini. Anda perlu mempertahankan saldo minimal %{minBalance} %{nativeTokenSymbol} di akun Anda.",
- "title": "Sponsor gas tidak tersedia"
+ "message": "Jaringan khusus ini mengharuskan Anda untuk mempertahankan saldo cadangan sebesar %{minBalance} %{nativeTokenSymbol} di akun Anda.",
+ "title": "Saldo cadangan diperlukan"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "Anda mengirimkan token ke alamat kontrak token. Hal ini dapat mengakibatkan hilangnya token tersebut.",
"smart_contract_address": "Alamat kontrak cerdas",
"smart_contract_address_warning": "Alamat penerima mungkin tidak mendukung transfer token langsung, yang dapat mengakibatkan hilangnya dana. Lanjutkan hanya jika Anda yakin kontrak ini dapat menerima transfer.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "Perbarui",
"i_understand": "Saya mengerti",
"cancel": "Batal",
"new_address_title": "Alamat baru",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "Nilai",
"pnl_percent": "P&L %",
- "recent": "Recent"
+ "recent": "Terbaru"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "Perdagangkan {{symbol}} perp",
"subtitle": "Gandakan P&L Anda hingga {{leverage}}"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "Hubungi dukungan"
+ },
"today": "Hari ini",
"yesterday": "Kemarin",
"unrealized_pnl": "P&L Belum Terealisasi",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC dipindahkan ke dompet Anda",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} dipindahkan ke dompet Anda",
"toast_error_title": "Terjadi kesalahan",
- "toast_error_description": "Gagal memproses penarikan"
+ "toast_error_description": "Gagal memproses penarikan",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Coba lagi"
},
"quote": {
"network_fee": "Biaya jaringan",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "MetaMask Predictions",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "Piala Dunia",
+ "banner_title": "Piala Dunia 2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
- "props": "Props",
- "live": "Live"
+ "all": "Semua",
+ "props": "Prop",
+ "live": "Langsung"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
+ "group_stage": "Babak Penyisihan Grup",
+ "round_of_32": "Babak 32 besar",
+ "round_of_16": "Babak 16 besar",
+ "quarterfinals": "Perempat final",
+ "semifinals": "Semifinal",
+ "third_place": "Posisi Ketiga",
"final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_a": "Grup A",
+ "group_b": "Grup B",
+ "group_c": "Grup C",
+ "group_d": "Grup D",
+ "group_e": "Grup E",
+ "group_f": "Grup F",
+ "group_g": "Grup G",
+ "group_h": "Grup H",
+ "group_i": "Grup I",
+ "group_j": "Grup J",
+ "group_k": "Grup K",
+ "group_l": "Grup L",
+ "third_place_match": "Pertandingan Perebutan Posisi Ketiga"
}
},
"prediction_markets": "Pasar prediksi",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "Penarikan selesai",
"withdraw_completed_subtitle": "{{amount}} USDC dipindahkan ke dompet Anda",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}} dipindahkan ke dompet Anda",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "Penarikan dana sementara tidak tersedia",
+ "unavailable_description": "Untuk bantuan mendesak, hubungi Layanan Pelanggan.",
"unavailable_got_it": "Mengerti",
"error_title": "Terjadi kesalahan",
"error_description": "Gagal melanjutkan penarikan",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "Terima dan tutup",
"pna25_open_settings_button": "Buka Pengaturan"
},
+ "onboarding_interest_questionnaire": {
+ "title": "Apa yang ingin Anda lakukan dengan MetaMask?",
+ "description": "Pilih semua yang sesuai.",
+ "option_buy_and_sell_crypto": "Membeli dan menjual kripto",
+ "option_consolidate_wallets": "Menggabungkan dompet Anda",
+ "option_advanced_trades": "Melakukan perdagangan lanjutan",
+ "option_predict_sports_events": "Memprediksi olahraga dan peristiwa",
+ "option_crypto_as_money": "Menggunakan kripto sebagai uang",
+ "option_connect_apps_sites": "Terhubung ke aplikasi atau situs",
+ "continue": "Lanjutkan"
+ },
"template_confirmation": {
"ok": "Oke",
"cancel": "Batal"
@@ -3261,8 +3286,27 @@
"notifications_desc": "Kelola notifikasi",
"allow_notifications": "Izinkan notifikasi",
"enable_push_notifications": "Aktifkan notifikasi push",
- "allow_notifications_desc": "Pantau terus segala yang terjadi di dompet Anda dengan notifikasi. Untuk menggunakan notifikasi, kami menggunakan profil untuk menyinkronkan beberapa pengaturan di seluruh perangkat Anda.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "Mati",
+ "select_all": "Pilih semua",
+ "deselect_all": "Batal pilih semua",
+ "select_accounts_title": "Akun",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "Sesuaikan notifikasi",
"customize_session_desc": "Aktifkan jenis notifikasi yang ingin diterima:",
"account_session_title": "Aktivitas akun",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snap",
"snaps_desc": "Fitur dan pembaruan terkini",
"products_announcements_title": "Pengumuman produk",
- "products_announcements_desc": "Produk dan fitur baru",
- "perps_title": "Perdagangan Perp"
+ "products_announcements_desc": "Produk dan fitur baru"
},
"contacts_title": "Kontak",
"contacts_desc": "Tambahkan, edit, hapus, dan kelola akun Anda.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "Kartu",
"reset_onboarding_description": "Reset status pendaftaran Kartu untuk memulai alur pendaftaran dari awal.",
- "reset_onboarding_button": "Reset Status Pendaftaran"
+ "reset_onboarding_button": "Reset Status Pendaftaran",
+ "unlink_money_account_description": "Cabut izin batas penggunaan USDC yang mengizinkan Kartu untuk menggunakan saldo dari Akun Dana milik Anda. Ini mengirimkan transaksi persetujuan(0) dan menandai Akun Dana sebagai tidak didelegasikan di backend Kartu.",
+ "unlink_money_account_button": "Batalkan penautan Akun Dana dari kartu",
+ "unlink_money_account_disabled_hint": "Tidak ada tautan aktif yang perlu dihapus."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "Haptic",
@@ -3842,8 +3893,8 @@
"predict_button": "Prediksi",
"add_collectible_button": "Tambahkan",
"info": "Informasi",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "Penjualan Massal",
+ "batch_sell_new_label": "Baru",
"swap": "Tukar",
"convert": "Konversikan",
"bridge": "Bridge",
@@ -3883,7 +3934,7 @@
"troubleshoot": "Troubleshoot",
"deposit_description": "Transfer bank atau kartu dengan biaya rendah",
"buy_description": "Cocok untuk membeli token tertentu",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "Jual maksimal 5 token untuk mendapatkan stablecoin",
"sell_description": "Jual kripto untuk dapat uang tunai",
"swap_description": "Pertukaran antar token",
"bridge_description": "Transfer token antar jaringan",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "Pengaturan > Notifikasi.",
"cancel": "Batal",
"cta": "Aktifkan"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "Jangan pernah melewatkan apa pun",
+ "body": "Dapatkan peringatan waktu nyata saat harga mencapai target Anda, perdagangan Anda dikonfirmasi, dan portofolio Anda bergerak. Plus pembaruan produk dan reward di sepanjang perjalanannya. Kami akan membagikan pembaruan berdasarkan interaksi Anda dengan MetaMask.",
+ "button_yes": "Ya",
+ "button_not_now": "Tidak sekarang",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "sekarang",
+ "title": "ETH naik 4,2% hari ini",
+ "message": "Sekarang seharga $2.668,51 — di atas peringatan harga Anda"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "1 jam lalu",
+ "title": "Menerima 0,25 ETH",
+ "message": "Dari 0x9a21…4f8c · $640,29"
+ }
+ },
+ "existing_user": {
+ "title": "Memperkenalkan peringatan yang dipersonalisasi",
+ "body": "Dapatkan notifikasi yang sesuai dengan cara Anda berdagang. Perbarui setiap saat.",
+ "card_title": "Yang akan Anda dapatkan",
+ "card_description": "Peringatan dan pembaruan yang dipersonalisasi dan disesuaikan dengan aktivitas perdagangan Anda.",
+ "button_confirm": "Konfirmasikan",
+ "button_not_now": "Tidak sekarang"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "Bayar melalui",
"buying_via": "Membeli melalui {{providerName}}.",
"change_provider": "Ubah penyedia.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "Terjadi kesalahan. Coba lagi.",
"no_payment_methods_available": "Tidak ada metode pembayaran yang tersedia.",
"error_fetching_quotes": "Terjadi kesalahan. Coba lagi.",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "Konversikan ke mUSD",
"get_a_percentage_musd_bonus": "Dapatkan bonus {{percentage}}% mUSD",
"convert": "Konversikan",
+ "confirm": "Konfirmasikan",
+ "convert_tooltip_description": "Konversikan stablecoin Anda ke mUSD dan dapatkan bonus tahunan hingga {{percentage}}% yang dapat diklaim setiap hari. Didukung oleh Relay.",
"fetching_quote": "Mengambil kuotasi...",
"you_convert": "Anda mengonversi",
"network_fee": "Biaya jaringan",
@@ -6597,7 +6679,7 @@
"step_progress": "Langkah {{current}} dari {{total}}",
"title": "Tambahkan uang",
"description": "Danai akun Anda dan mulailah mendapatkan APY.",
- "add": "Tambah",
+ "add": "Tambahkan dana",
"step2_title": "Dapatkan Kartu MetaMask",
"step2_description": "Gunakan saldo Dana selagi masih menghasilkan, di mana pun Mastercard diterima.",
"step2_cta": "Dapatkan kartu",
@@ -6605,6 +6687,19 @@
"link_card_description": "Gunakan saldo Anda selagi masih menghasilkan, di mana pun Mastercard diterima.",
"link_card_cta": "Tautkan kartu"
},
+ "rive_onboarding": {
+ "step1_title": "Akun Dana telah hadir",
+ "step1_body": "Dapatkan hingga {{percentage}}% APY dari saldo Anda, tersedia di seluruh dompet Anda.",
+ "step1_footer_text": "APY bersifat variabel dan dapat berubah sewaktu-waktu.",
+ "step2_title": "Dapatkan secara otomatis",
+ "step2_body": "Pindahkan stablecoin tanpa biaya pertukaran. Dana mulai menghasilkan keuntungan segera.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "Gunakan di mana pun",
+ "step3_body": "Dapatkan hingga {{percentage}}% pengembalian dari pembelian dengan menghubungkan akun Dana milik Anda ke kartu MetaMask.",
+ "step4_title": "Berdagang dan hasilkan keuntungan di satu tempat",
+ "step4_body": "Gunakan saldo Dana untuk bertransaksi di MetaMask sambil tetap mendapatkan keuntungan.",
+ "continue": "Lanjutkan"
+ },
"action": {
"add": "Tambah",
"transfer": "Transfer",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "Cara kerjanya",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "Depositkan mUSD ke akun Dana dan dapatkan hingga",
+ "description_suffix": ". Saldo Anda didukung dolar dan siap untuk digunakan, diperdagangkan, atau dikirim setiap saat."
},
"musd_row": {
"add": "Tambah"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Saldo dana",
"add": "Tambah",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Saldo dana",
+ "info_sheet_body": "Saldo mUSD yang didukung dolar selalu tersedia untuk digunakan, dikirim, atau diperdagangkan setiap saat. Kami tidak menghitung ini ke dalam total saldo akun Anda.\n\nPenarikan diproses dengan segera, tergantung waktu konfirmasi jaringan standar pada blockchain yang relevan. Jika likuiditas ketat, penundaan sementara dapat terjadi."
},
"potential_earnings": {
"title": "Raih pendapatan dari kripto",
"description": "Lihat bagaimana uang Anda dapat bertambah nilainya dari waktu ke waktu dengan mengonversikan kripto menjadi mUSD.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "Konversikan {{total}} menjadi aset dan Anda bisa mendapatkan hingga",
+ "description_with_amounts_suffix": "dalam satu tahun.",
"convert": "Konversikan",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "Konversikan kripto Anda",
"no_fee": "Tidak ada biaya MetaMask",
"view_all": "Lihat semua",
"view_potential_earnings": "Lihat potensi penghasilan"
@@ -6649,41 +6744,44 @@
"cashback": "pengembalian {{percentage}}% mUSD",
"get_now": "Dapatkan sekarang",
"link_title": "Tautkan Kartu MetaMask",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "Gunakan saldo Dana dan dapatkan keuntungan dari pembelian. Plus, hingga {{apy}}% APY dari saldo Anda.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "Dapatkan pengembalian {{percentage}}% mUSD",
+ "link_bullet_apy": "Dapatkan hingga {{apy}}% APY",
"link_card": "Tautkan kartu",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "Gunakan dan dapatkan",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "Tautkan kartu",
+ "manage_card": "Kelola",
+ "avail_balance": "Saldo tersedia"
},
"what_you_get": {
"title": "Yang Anda dapatkan",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "Dapatkan otomatis hingga",
"benefit_dollar_backed": "Amankan uang Anda di mUSD, stablecoin yang didukung dolar dengan rasio 1:1",
"benefit_liquidity": "Dapatkan likuiditas penuh tanpa penguncian, sehingga Anda dapat melakukan perdagangan atau penarikan setiap saat",
"benefit_spend_prefix": "Gunakan di lebih dari 150 juta merchant dengan Kartu MetaMask dan dapatkan ",
"benefit_spend_cashback": "Pengembalian 1-3% mUSD",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "Transfer ke dompet mana pun di MetaMask",
+ "benefit_global": "Kirim dan terima dana secara global",
"learn_more": "Pelajari selengkapnya"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "Tambahkan dana"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "Tambahkan dana",
"convert_crypto": "Konversikan kripto",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "Dari akun mana pun",
"deposit_funds": "Depositkan dana",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "Dari kartu debit atau bank",
+ "move_musd": "Transfer {{amount}} mUSD",
+ "move_musd_no_amount": "Transfer mUSD",
+ "move_musd_description": "Dari saldo Anda",
"receive_external": "Terima dari dompet eksternal",
"coming_soon": "Segera hadir"
},
@@ -6694,7 +6792,7 @@
"contact_support": "Hubungi dukungan"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "Transfer dana",
"between_accounts": "Antar akun",
"perps_account": "Akun perp",
"predictions_account": "Akun Predictions",
@@ -6712,8 +6810,8 @@
"body": "Estimasi berapa banyak yang dapat Anda peroleh selama periode tertentu berdasarkan saldo saat ini dan APY (Persentase Hasil Tahunan) hari ini. Estimasi ini bukan jaminan pengembalian dan dapat berubah sewaktu-waktu."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "Raih pendapatan dari kripto",
+ "body": "Ilustrasi ini mengasumsikan Persentase Hasil Tahunan (APY) sebesar {{percentage}}% tetap tidak berubah selama satu tahun. APY bersifat variabel dan dapat berubah karena berbagai faktor. Tidak ada jaminan pengembalian."
},
"activity": {
"title": "Aktivitas",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "Cara kerjanya",
- "how_it_works_subtitle": "Lihat cara kerja dana Anda",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "Pelajari lebih lanjut seputar mUSD",
"what_you_get_title": "Yang Anda dapatkan",
@@ -6738,7 +6836,8 @@
"sent": "Mengirim",
"transferred": "Ditransfer",
"card_transaction": "Transaksi kartu",
- "converted": "Dikonversi"
+ "converted": "Dikonversi",
+ "failed": "Gagal"
},
"convert_stablecoins": {
"title": "Konversikan stablecoin Anda",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "Dana",
"section_title": "Cara kerjanya",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "Depositkan mUSD ke akun Dana dan dapatkan hingga {{percentage}}% APY (variabel) secara otomatis. Dana masuk ke brankas DeFi yang menghasilkan keuntungan di pasar pinjaman yang diaudit—tanpa stake, tanpa klaim, tanpa penguncian.",
"description_2": "Saldo Dana merupakan saldo penggunaan Anda. Hubungkan Kartu MetaMask untuk digunakan di lebih dari 150 juta merchant di seluruh dunia. Dana Anda terus menghasilkan keuntungan hingga saat Anda menggunakannya.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "Pertanyaan umum",
"faq_placeholder_answer": "Segera hadir.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "Bagaimana cara kerja {{percentage}}% APY?",
"faq_q2": "Apa itu mUSD?",
"faq_q3": "Dari mana hasil tersebut berasal?",
"faq_q4": "Apakah dana saya terkunci? Bisakah saya menarik dana setiap saat?",
@@ -7030,11 +7129,12 @@
"confirm": "Konfirmasikan",
"pay_with_bottom_sheet": {
"title": "Bayar melalui",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "Terakhir digunakan",
+ "bank_and_card": "Bank dan kartu",
+ "crypto": "Kripto",
+ "available_balance": "{{balance}} tersedia",
+ "other_assets": "Aset lainnya",
+ "other_assets_description": "Pilih dari token Anda"
},
"staking_footer": {
"part1": "Dengan melanjutkan, Anda menyetujui ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "Biaya konversi mUSD mencakup biaya jaringan dan mungkin termasuk biaya penyedia. MetaMask tidak mengenakan biaya saat Anda mengonversi ke mUSD."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMask akan menukar mUSD dengan token yang Anda inginkan. Penyedia layanan swap dapat mengenakan biaya, tetapi MetaMask tidak akan mengenakan biaya."
},
"title": {
"transaction_fee": "Biaya"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "Tambahkan dana",
"deposit_edit_amount_predict_withdraw": "Tarik",
"deposit_edit_amount_musd_conversion": "Konversikan ke mUSD",
- "preparing_order": "Preparing order"
+ "preparing_order": "Mempersiapkan order"
},
"change_in_simulation_modal": {
"title": "Hasil telah berubah",
@@ -7250,20 +7350,33 @@
"confirm_swap": "Swap",
"terms_and_conditions": "Syarat & Ketentuan",
"select_token": "Pilih token",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "Pilih maksimal 5 token",
+ "batch_sell_select_subtitle": "Semua token harus berada di jaringan yang sama.",
+ "batch_sell_empty_state_title": "Tidak punya token. Tidak masalah.",
+ "batch_sell_empty_state_description": "Tidak punya token? Tidak masalah. Jelajahi dan beli token untuk dijual secara massal.",
+ "batch_sell_continue_with_one_token": "Lanjutkan dengan (1) token",
+ "batch_sell_continue_with_tokens": "Lanjutkan dengan ({{tokenCount}}) token",
+ "batch_sell_max_tokens_allowed": "Maksimal 5 token diperbolehkan",
+ "batch_sell_single_token_dialog_title": "Peringatan harga tinggi",
+ "batch_sell_single_token_dialog_description": "Menjual satu token secara massal dapat menghasilkan harga yang lebih tinggi. Ingin melakukan swap saja?",
+ "batch_sell_swap_instead": "Ya, swap",
+ "batch_sell_review_title": "Ulasan Penjualan Massal",
+ "batch_sell_select_stablecoin": "Pilih stablecoin",
+ "batch_sell_total_received": "Total yang diterima",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "Tinjau",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "Sesuaikan {{tokenSymbol}}",
+ "batch_sell_remove_token": "Hapus {{tokenSymbol}}",
+ "batch_sell_checkbox_label": "Penjualan Massal token",
+ "sort_balance": "Saldo",
+ "next": "Berikutnya",
+ "explore_tokens": "Jelajahi token",
"no_tokens_found": "Tidak ada token yang ditemukan",
"no_tokens_found_description": "Kami tidak dapat menemukan token dengan nama ini. Coba pencarian lain.",
"select_network": "Pilih jaringan",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "Anda akan kehilangan sekitar {{priceImpact}} dari nilai token Anda pada swap ini. Cobalah untuk mengurangi jumlahnya atau pilih rute yang lebih likuid.",
"proceed": "Lanjutkan",
"cancel": "Batal",
+ "close": "Tutup",
"slippage_info_title": "Selip",
"slippage_info_description": "% perubahan harga yang Anda bersedia izinkan sebelum transaksi dibatalkan.",
"blockaid_error_title": "Transaksi ini akan dikembalikan",
@@ -8107,7 +8221,14 @@
"retry": "Coba lagi",
"on_linea": "di Linea",
"account_label": "Akun",
- "token_label": "Token"
+ "token_label": "Token",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "Pengembalian mUSD",
@@ -8445,6 +8566,7 @@
"show_less": "Ciutkan",
"linking_progress": "Menambahkan akun... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} terdaftar",
+ "accounts_added": "Accounts added",
"add_all_accounts": "Tambahkan semua akun",
"environment_selector": "Lingkungan",
"environment_cancel": "Batal",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "Hapus progres Rewards",
- "description": "Tindakan ini akan menghapus akun Anda dari program Reward dan menghapus progres Anda. Tindakan ini tidak dapat dibatalkan.",
- "confirm": "Hapus",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "Anda yakin?",
- "confirmation_description": "Tindakan ini akan menghapus semua progres Anda dan tidak dapat dibatalkan. Jika bergabung kembali dengan program Reward nanti, Anda akan memulai dari 0.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "Batal",
"confirm": "Konfirmasikan",
+ "error_title": "Terjadi kesalahan",
"error_message": "Gagal keluar dari Reward. Coba lagi.",
"processing": "Memproses..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "Lewatkan",
@@ -8562,7 +8691,8 @@
"title_claim": "Klaim Manfaat",
"action": "Klaim",
"empty-list": "Saat ini Anda tidak memiliki manfaat apa pun.",
- "powered_by": "Didukung oleh"
+ "powered_by": "Didukung oleh",
+ "available_count": "{{count}} tersedia"
},
"end_of_season_rewards": {
"confirm_label_default": "Konfirmasikan",
@@ -8880,9 +9010,11 @@
"musd_claim": "Klaim mUSD",
"perps_deposit": "Tambahkan dana",
"perps_withdraw": "Penarikan",
+ "predict_withdraw": "Tarik {{sourceSymbol}} dari {{sourceChain}}",
"predict_deposit": "Tambahkan dana",
"swap": "Tukar token",
- "swap_approval": "Setujui token"
+ "swap_approval": "Setujui token",
+ "fiat_purchase": "Beli {{token}} dengan {{paymentMethod}}"
},
"perps_deposit_solution": "Saat ini Anda memiliki {{fiat}} USDC di Arbitrum. Coba deposit lagi."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "Tinggi ke rendah",
"low_to_high": "Rendah ke tinggi",
"apply": "Terapkan",
- "search_placeholder": "Cari token, situs, URL",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "Batal",
"perps": "Perps",
"rwa_perps_section": "Perp",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "Perp",
"predictions": "Prediksi",
"no_results": "Hasil tidak ditemukan",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "Populer",
"sites": "Situs",
"popular_sites": "Situs populer",
"search_sites": "Cari situs",
"view_all": "Lihat semua",
+ "view_more": "Lihat selengkapnya",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "Aktifkan fungsi dasar",
"basic_functionality_disabled_title": "Fitur Jelajahi tidak tersedia",
"basic_functionality_disabled_description": "Kami tidak dapat mengakses metadata yang diperlukan saat fungsi dasar dinonaktifkan.",
@@ -8995,6 +9131,14 @@
"crypto": "Kripto",
"sports": "Olahraga",
"dapps": "Situs"
+ },
+ "search_tabs": {
+ "all": "Semua",
+ "crypto": "Cryptos",
+ "perps": "Perp",
+ "stocks": "Saham",
+ "predictions": "Prediksi",
+ "sites": "Situs"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "Tidak ada mUSD di jaringan ini. Ganti jaringan untuk melihat mUSD milik Anda.",
"money_empty_state": {
"get_started": "Mulai",
+ "earn": "Dapatkan",
"earn_apy": "Dapatkan APY {{percentage}}%"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "Aset Terkait",
"perpetuals": "Abadi",
"predictions": "Prediksi",
- "whats_happening": "Apa yang sedang terjadi",
- "whats_happening_ai": "AI",
- "whats_happening_impact": {
- "bullish": "Bullish",
- "bearish": "Bearish",
- "neutral": "Netral"
- },
"top_traders": "Trader Top",
- "whats_happening_categories": {
- "geopolitical": "Geopolitik",
- "macro": "Makro",
- "regulatory": "Peraturan",
- "technical": "Teknis",
- "social": "Sosial",
- "other": "Lainnya"
- },
"defi": "DeFi",
"nfts": "NFT",
"trending_tokens": "Sedang tren",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "Populer"
+ },
+ "whats_happening": {
+ "title": "Apa yang sedang terjadi",
+ "ai": "AI",
+ "impact": {
+ "bullish": "Bullish",
+ "bearish": "Bearish",
+ "neutral": "Netral"
+ },
+ "categories": {
+ "geopolitical": "Geopolitik",
+ "macro": "Makro",
+ "regulatory": "Peraturan",
+ "technical": "Teknis",
+ "social": "Sosial",
+ "other": "Lainnya"
+ }
}
}
diff --git a/locales/languages/ja.json b/locales/languages/ja.json
index 9a9808e0a9f9..f448aaac5828 100644
--- a/locales/languages/ja.json
+++ b/locales/languages/ja.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "利用可能な資金がありません。別のアカウントをお試しください。"
},
+ "headless_buy_error": {
+ "title": "法定通貨の購入に失敗しました",
+ "message": "法定通貨の購入で問題が発生しました。もう一度お試しください。"
+ },
"mmpay_hardware_account": {
"title": "未対応のウォレットです",
"message": "ハードウェアウォレットには対応していません。\n続行するにはウォレットを切り替えてください。"
@@ -133,8 +137,8 @@
"message": "この受取人のアドレスはトークンの直接送金に対応していない可能性があり、資金を失うおそれがあります。これが送金を受け取れるコントラクトであることを確信している場合のみ、続行してください。"
},
"gas_sponsorship_reserve_balance": {
- "message": "この取引でガススポンサーシップはご利用いただけません。アカウントに%{minBalance} %{nativeTokenSymbol}以上の残高が必要です。",
- "title": "ガススポンサーシップは利用できません"
+ "message": "このネットワークでは、アカウントに%{minBalance} %{nativeTokenSymbol}の準備残高が必要です。",
+ "title": "準備残高が必要です"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "トークンのコントラクトアドレスにトークンを送金しようとしています。これにより、当該トークンが失われる可能性があります。",
"smart_contract_address": "スマートコントラクトアドレス",
"smart_contract_address_warning": "この受取人のアドレスはトークンの直接送金に対応していない可能性があり、資金を失うおそれがあります。これが送金を受け取れるコントラクトであることを確信している場合のみ、続行してください。",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "更新",
"i_understand": "理解しています",
"cancel": "キャンセル",
"new_address_title": "新しいアドレス",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "価値",
"pnl_percent": "損益額 %",
- "recent": "Recent"
+ "recent": "最近"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "{{symbol}}のパーペチュアルを取引",
"subtitle": "損益を最大{{leverage}}増加"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "サポートへのお問い合わせ"
+ },
"today": "今日",
"yesterday": "昨日",
"unrealized_pnl": "含み損益",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDCがウォレットに移動されました",
"toast_completed_any_token_subtitle": "{{amount}} {{token}}がウォレットに移動されました",
"toast_error_title": "問題が発生しました",
- "toast_error_description": "出金に失敗しました"
+ "toast_error_description": "出金に失敗しました",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "もう一度お試しください"
},
"quote": {
"network_fee": "ネットワーク手数料",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "MetaMask 予測",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "ワールドカップ",
+ "banner_title": "ワールドカップ2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
- "props": "Props",
- "live": "Live"
+ "all": "すべて",
+ "props": "プロップベット",
+ "live": "ライブ"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
- "final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_stage": "グループステージ",
+ "round_of_32": "ラウンド32",
+ "round_of_16": "ラウンド16",
+ "quarterfinals": "準々決勝",
+ "semifinals": "準決勝",
+ "third_place": "3位",
+ "final": "決勝",
+ "group_a": "グループA",
+ "group_b": "グループB",
+ "group_c": "グループC",
+ "group_d": "グループD",
+ "group_e": "グループE",
+ "group_f": "グループF",
+ "group_g": "グループG",
+ "group_h": "グループH",
+ "group_i": "グループI",
+ "group_j": "グループJ",
+ "group_k": "グループK",
+ "group_l": "グループL",
+ "third_place_match": "3位決定戦"
}
},
"prediction_markets": "予測市場",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "出金完了",
"withdraw_completed_subtitle": "{{amount}} USDCがウォレットに移動されました",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}}がウォレットに移動されました",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "一時的に出金ができません",
+ "unavailable_description": "緊急でサポートが必要な場合は、カスタマーサービスまでお問い合わせください。",
"unavailable_got_it": "了解",
"error_title": "問題が発生しました",
"error_description": "出金に失敗しました",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "承認して閉じる",
"pna25_open_settings_button": "設定を開く"
},
+ "onboarding_interest_questionnaire": {
+ "title": "MetaMaskで何がしたいですか?",
+ "description": "該当するものをすべて選択してください。",
+ "option_buy_and_sell_crypto": "仮想通貨の売買",
+ "option_consolidate_wallets": "複数あるウォレットを1つにまとめたい",
+ "option_advanced_trades": "高度な取引がしたい",
+ "option_predict_sports_events": "スポーツや出来事を予測したい",
+ "option_crypto_as_money": "仮想通貨をお金として使いたい",
+ "option_connect_apps_sites": "アプリやサイトに接続したい",
+ "continue": "続行"
+ },
"template_confirmation": {
"ok": "OK",
"cancel": "キャンセル"
@@ -3261,8 +3286,27 @@
"notifications_desc": "通知の管理",
"allow_notifications": "通知を許可する",
"enable_push_notifications": "プッシュ通知を有効にする",
- "allow_notifications_desc": "通知を使えば、ウォレットで何が起きているか常に把握できます。通知を使用するために、プロファイルを使用してデバイス間で一部の設定が同期されます。",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "オフ",
+ "select_all": "すべて選択",
+ "deselect_all": "すべて選択解除",
+ "select_accounts_title": "アカウント",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "通知のカスタマイズ",
"customize_session_desc": "受け取る通知の種類をオンにします",
"account_session_title": "アカウントのアクティビティ",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snap",
"snaps_desc": "新機能や最新情報",
"products_announcements_title": "製品に関するお知らせ",
- "products_announcements_desc": "新しい製品や機能",
- "perps_title": "パーペチュアル取引"
+ "products_announcements_desc": "新しい製品や機能"
},
"contacts_title": "連絡先",
"contacts_desc": "アカウントを追加、編集、削除、管理します.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "カード",
"reset_onboarding_description": "オンボーディングフローをやり直すには、カードのオンボーディングステータスをリセットしてください。",
- "reset_onboarding_button": "オンボーディングステータスのリセット"
+ "reset_onboarding_button": "オンボーディングステータスのリセット",
+ "unlink_money_account_description": "カードがマネーカウントから支出できるUSDCの利用限度額を取り消します。これにより、approve(0)のトランザクションが送信され、カードのバックエンド側でマネーアカウントが委任解除済み (not-delegated) に変更されます。",
+ "unlink_money_account_button": "マネーアカウントとカードのリンクを解除",
+ "unlink_money_account_disabled_hint": "削除対象となる有効なリンクがありません。"
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "ハプティクス",
@@ -3842,8 +3893,8 @@
"predict_button": "予測",
"add_collectible_button": "追加",
"info": "情報",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "一括売却",
+ "batch_sell_new_label": "新登場",
"swap": "スワップ",
"convert": "変換",
"bridge": "ブリッジ",
@@ -3883,7 +3934,7 @@
"troubleshoot": "トラブルシューティング",
"deposit_description": "手数料の少ない銀行またはカード送金",
"buy_description": "特定のトークンの購入に適しています",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "最大5つのトークンを売却してステーブルコインに交換できます",
"sell_description": "仮想通貨を売って現金化します",
"swap_description": "トークン同士を交換します",
"bridge_description": "ネットワーク間でトークンを転送します",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "「設定」>「通知」でいつでもオフにできます。",
"cancel": "キャンセル",
"cta": "オンにする"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "価格変動を見逃さない",
+ "body": "目標価格への到達、取引の確定、ポートフォリオの動きをリアルタイムのアラートでお知らせします。また、製品の最新情報や特典に関する情報もお届けします。お届けする最新情報は、MetaMaskのご利用状況によって決まります。",
+ "button_yes": "はい",
+ "button_not_now": "後で",
+ "preview_card_1": {
+ "eyebrow": "MetaMask",
+ "time": "現在",
+ "title": "ETHは今日4.2%値上がりしています",
+ "message": "現在$2,668.51 — 価格アラートの金額を超えています"
+ },
+ "preview_card_2": {
+ "eyebrow": "MetaMask",
+ "time": "1時間前",
+ "title": "0.25 ETHを受け取りました",
+ "message": "送金元: 0x9a21…4f8c · $640.29"
+ }
+ },
+ "existing_user": {
+ "title": "個別アラートのご紹介",
+ "body": "取引スタイルに合った通知を受け取りましょう。いつでも更新可能です。",
+ "card_title": "得られる情報",
+ "card_description": "取引状況に合わせた個別のアラートと最新情報をお届けします。",
+ "button_confirm": "確定",
+ "button_not_now": "後で"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "支払方法:",
"buying_via": "{{providerName}}で購入しようとしています。",
"change_provider": "プロバイダーを変更します。",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "問題が発生しました。もう一度お試しください。",
"no_payment_methods_available": "使用可能な支払方法がありません。",
"error_fetching_quotes": "問題が発生しました。もう一度お試しください。",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "mUSDに変換",
"get_a_percentage_musd_bonus": "{{percentage}}%のmUSDボーナスを獲得",
"convert": "変換",
+ "confirm": "確定",
+ "convert_tooltip_description": "お持ちのステーブルコインをmUSDに換えて、年換算で最大{{percentage}}%のボーナスを獲得しましょう。ボーナスは毎日請求できます。このサービスはRelayの技術によって支えられています。",
"fetching_quote": "クォートを取得中...",
"you_convert": "変換するトークン",
"network_fee": "ネットワーク手数料",
@@ -6597,7 +6679,7 @@
"step_progress": "ステップ{{current}}/{{total}}",
"title": "お金を追加",
"description": "アカウントに入金して、毎年複利で利息を得ましょう。",
- "add": "追加",
+ "add": "資金を追加",
"step2_title": "MetaMaskカードを取得",
"step2_description": "Mastercardの取扱店であればどこでも、収益を得ると同時にマネー残高を使用できます。",
"step2_cta": "カードを取得",
@@ -6605,6 +6687,19 @@
"link_card_description": "Mastercardの取扱店であればどこでも、収益を得ると同時に残高を使えます。",
"link_card_cta": "カードをリンク"
},
+ "rive_onboarding": {
+ "step1_title": "マネーアカウントが新登場",
+ "step1_body": "残高に対して最大{{percentage}}%のAPYが得られ、ウォレット内で自由に利用できます。",
+ "step1_footer_text": "APYは変動制で、いつでも変わる可能性があります。",
+ "step2_title": "自動で報酬がもらえる",
+ "step2_body": "取引所手数料なしでステーブルコインを移動できます。報酬の獲得はすぐに開始されます。",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "どこでも使える",
+ "step3_body": "マネーアカウントをMetaMaskカードとリンクさせることで、購入額の最大{{percentage}}%が還元されます。",
+ "step4_title": "取引も報酬の獲得も1か所で",
+ "step4_body": "MetaMaskでの取引にマネー残高を利用しながら、引き続き利回りが得られます。",
+ "continue": "続行"
+ },
"action": {
"add": "追加",
"transfer": "送金",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "報酬獲得の仕組み",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "マネーアカウントにmUSDを入金すると、得られる利回りは最大",
+ "description_suffix": "。残高はドルの裏付けがあり、いつでもショッピング、取引、送金に利用できます。"
},
"musd_row": {
"add": "追加"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "マネー残高",
"add": "追加",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "マネー残高",
+ "info_sheet_body": "ドルの裏付けのあるmUSD残高で、いつでもショッピング、送金、取引に利用できます。この残高は、アカウントの合計残高には算入されません。\n\n出金は即時処理され、対象となるブロックチェーンネットワークによる標準的な承認時間がかかります。流動性が低下している場合は、一時的に遅延が発生することがあります。"
},
"potential_earnings": {
"title": "仮想通貨を収益化",
"description": "仮想通貨をmUSDに換えることで、時間の経過とともにお金がどのように増えるかをご覧ください。",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "お持ちの資産{{total}}を変換すると、1年間で最大",
+ "description_with_amounts_suffix": "の報酬を得られる可能性があります。",
"convert": "変換",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "仮想通貨を変換",
"no_fee": "MetaMaskの手数料なし",
"view_all": "すべて表示",
"view_potential_earnings": "獲得可能な収益を表示"
@@ -6649,41 +6744,44 @@
"cashback": "{{percentage}}%のmUSDを還元",
"get_now": "今すぐ入手",
"link_title": "MetaMaskカードをリンク",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "マネー残高で支払えば、購入額に対して報酬を獲得できます。さらに、残高に対して最大{{apy}}%のAPYが得られます。",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "{{percentage}}%のmUSDを還元",
+ "link_bullet_apy": "最大{{apy}}%APYを獲得",
"link_card": "カードをリンク",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "使えば貯まる",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "カードをリンク",
+ "manage_card": "管理",
+ "avail_balance": "利用可能残高"
},
"what_you_get": {
"title": "得られるもの",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "自動で得られる報酬は最大",
"benefit_dollar_backed": "ドルと1対1で裏付けられたステーブルコイン、mUSDで、資金を安全に守れます",
"benefit_liquidity": "ロックアップなしで完全な流動性が確保され、いつでも取引や引き出しが可能です",
"benefit_spend_prefix": "1億5000万店舗を超える加盟店でMetaMaskカードを利用して、",
"benefit_spend_cashback": "1~3%のmUSDを還元",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "MetaMaskに登録した任意のウォレットに送金可能",
+ "benefit_global": "世界中と資金をやり取りできます",
"learn_more": "詳細"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "資金を追加"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "資金を追加",
"convert_crypto": "仮想通貨を変換",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "任意のアカウントから",
"deposit_funds": "資金を入金",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "デビットカードまたは銀行から",
+ "move_musd": "{{amount}} mUSDを送金",
+ "move_musd_no_amount": "mUSDを送金",
+ "move_musd_description": "残高から",
"receive_external": "外部ウォレットから受け取る",
"coming_soon": "近日追加予定"
},
@@ -6694,7 +6792,7 @@
"contact_support": "サポートへのお問い合わせ"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "送金",
"between_accounts": "アカウント間",
"perps_account": "パーペチュアルアカウント",
"predictions_account": "予測市場アカウント",
@@ -6712,8 +6810,8 @@
"body": "現在の残高および本日のAPYから算出された、一定期間内に獲得可能な金額の見積もり。見積もりはリターンを保証するものではなく、変更される可能性があります。"
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "仮想通貨を収益化",
+ "body": "図では、{{percentage}}%の年換算利率 (APY) が1年間変わらないと仮定しています。APYは変動制で、さまざまな要因によって変わる可能性があります。利益が保証されるものではありません。"
},
"activity": {
"title": "アクティビティ",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "報酬獲得の仕組み",
- "how_it_works_subtitle": "資金がどのように働いてくれるのか、ご覧ください",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "mUSDの詳細",
"what_you_get_title": "得られるもの",
@@ -6738,7 +6836,8 @@
"sent": "送信済み",
"transferred": "送金済み",
"card_transaction": "カードトランザクション",
- "converted": "換金済み"
+ "converted": "換金済み",
+ "failed": "失敗"
},
"convert_stablecoins": {
"title": "ステーブルコインの換金",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "マネー",
"section_title": "報酬獲得の仕組み",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "マネーアカウントにmUSDを入金すると、最大{{percentage}}%のAPY (変動式) が自動的に得られます。資金はDeFiヴォールトに入れられ、監査対象のレンディング市場で利益を生み出します。ステーキングや請求は不要で、ロックアップもありません。",
"description_2": "マネー残高は使用可能な残高です。MetaMaskカードをリンクすれば、世界中の1億5千万以上のマーチャントで使用できます。資金は使うまで収益を生み続けます。",
- "faq_title": "Frequently asked questions",
+ "faq_title": "よくある質問",
"faq_placeholder_answer": "近日追加予定.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "{{percentage}}%のAPYが得られる仕組みとは?",
"faq_q2": "mUSDとは?",
"faq_q3": "利益はどこから生まれるのですか?",
"faq_q4": "資金はロックされますか?いつでも引き出せますか?",
@@ -7030,11 +7129,12 @@
"confirm": "確定",
"pay_with_bottom_sheet": {
"title": "支払方法:",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "前回の利用",
+ "bank_and_card": "銀行とカード",
+ "crypto": "仮想通貨",
+ "available_balance": "{{balance}}が利用可能",
+ "other_assets": "その他の資産",
+ "other_assets_description": "お持ちのトークンからお選びください"
},
"staking_footer": {
"part1": "続行することで、",
@@ -7120,7 +7220,7 @@
"transaction_fee": "mUSD換算手数料にはネットワークコストが含まれるほか、プロバイダー手数料も含まれる場合があります。なお、mUSDへの換算時にMetaMaskの手数料は発生しません。"
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMaskはお持ちのmUSDをご希望のトークンに交換します。スワッププロバイダーは手数料を請求することがありますが、MetaMaskなら無料です。"
},
"title": {
"transaction_fee": "手数料"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "資金を追加",
"deposit_edit_amount_predict_withdraw": "出金",
"deposit_edit_amount_musd_conversion": "mUSDに変換",
- "preparing_order": "Preparing order"
+ "preparing_order": "注文を準備しています"
},
"change_in_simulation_modal": {
"title": "結果が変更になりました",
@@ -7250,20 +7350,33 @@
"confirm_swap": "スワップ",
"terms_and_conditions": "利用規約",
"select_token": "トークンを選択",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "トークンを最大5つまでお選びください",
+ "batch_sell_select_subtitle": "すべてのトークンが同一ネットワーク上にある必要があります。",
+ "batch_sell_empty_state_title": "トークンがありません。でも問題ありません。",
+ "batch_sell_empty_state_description": "トークンがありません。でも問題ありません。一括売却するトークンを検索して購入しましょう。",
+ "batch_sell_continue_with_one_token": "1つのトークンで続行",
+ "batch_sell_continue_with_tokens": "{{tokenCount}}つのトークンで続行",
+ "batch_sell_max_tokens_allowed": "最大で5つのトークンを売却できます",
+ "batch_sell_single_token_dialog_title": "割高レートに関するアラート",
+ "batch_sell_single_token_dialog_description": "一括売却するトークンが1種類の場合は、レートが高くなる可能性があります。代わりにスワップを行いますか?",
+ "batch_sell_swap_instead": "はい、スワップを行います",
+ "batch_sell_review_title": "一括売却の確認",
+ "batch_sell_select_stablecoin": "ステーブルコインを選択",
+ "batch_sell_total_received": "受取額合計",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "確認",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "{{tokenSymbol}}をカスタマイズ",
+ "batch_sell_remove_token": "{{tokenSymbol}}を削除",
+ "batch_sell_checkbox_label": "トークンを一括売却",
+ "sort_balance": "残高",
+ "next": "次へ",
+ "explore_tokens": "トークンの閲覧",
"no_tokens_found": "トークンが見つかりませんでした",
"no_tokens_found_description": "この名前のトークンが見つかりませんでした。別の検索をお試しください。",
"select_network": "ネットワークを選択",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "このスワップにより、トークンの価値の約{{priceImpact}}が失われます。金額を下げるか、より流動性の高いルートを選択してください。",
"proceed": "先に進む",
"cancel": "キャンセル",
+ "close": "閉じる",
"slippage_info_title": "スリッページ",
"slippage_info_description": "トランザクションが取り消される前に許容する価格の変動率(%)。",
"blockaid_error_title": "このトランザクションは取り消されます",
@@ -8107,7 +8221,14 @@
"retry": "再試行してください",
"on_linea": "Lineaで",
"account_label": "アカウント",
- "token_label": "トークン"
+ "token_label": "トークン",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "mUSDの還元",
@@ -8445,6 +8566,7 @@
"show_less": "表示を戻す",
"linking_progress": "アカウントを追加中… ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}}件を登録済み",
+ "accounts_added": "Accounts added",
"add_all_accounts": "すべてのアカウントを追加",
"environment_selector": "環境",
"environment_cancel": "キャンセル",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "リワードの進行状況を削除",
- "description": "この操作を行うと、アカウントがRewardsプログラムから削除され、進行状況が失われます。この操作は元に戻せません。",
- "confirm": "削除",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "よろしいですか?",
- "confirmation_description": "これにより、進行状況はすべて削除され、元には戻せません。リワードプログラムに後で再び参加した場合は、最初から始めることになります。",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "キャンセル",
"confirm": "選択すると、",
+ "error_title": "問題が発生しました",
"error_message": "リワードのオプトアウトに失敗しました。もう一度お試しください。",
"processing": "処理中..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "閉じる",
@@ -8562,7 +8691,8 @@
"title_claim": "特典を請求",
"action": "請求",
"empty-list": "現在特典がありません。",
- "powered_by": "提供元:"
+ "powered_by": "提供元:",
+ "available_count": "{{count}}件利用可能"
},
"end_of_season_rewards": {
"confirm_label_default": "確定",
@@ -8695,7 +8825,7 @@
"label_your_rank": "ランク",
"label_portfolio": "Portfolio",
"label_net_inflow": "純流入額",
- "label_total_inflow": "総流入額",
+ "label_total_inflow": "流入額合計",
"label_outflow": "流出額",
"label_days_held": "保有日数",
"qualified_title": "対象となります",
@@ -8880,9 +9010,11 @@
"musd_claim": "mUSDの請求",
"perps_deposit": "資金を追加",
"perps_withdraw": "出金",
+ "predict_withdraw": "{{sourceChain}}から{{sourceSymbol}}を出金",
"predict_deposit": "資金を追加",
"swap": "トークンをスワップ",
- "swap_approval": "トークンを承認"
+ "swap_approval": "トークンを承認",
+ "fiat_purchase": "{{paymentMethod}}で{{token}}を購入"
},
"perps_deposit_solution": "Arbitrumの残高が{{fiat}}のUSDCになりました。もう一度入金をお試しください。"
},
@@ -8958,7 +9090,7 @@
"high_to_low": "高い順",
"low_to_high": "低い順",
"apply": "適用",
- "search_placeholder": "トークン、サイト、URLを検索",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "キャンセル",
"perps": "パーペチュアル",
"rwa_perps_section": "パーペチュアル",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "パーペチュアル",
"predictions": "予測",
"no_results": "結果が見つかりませんでした",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "人気",
"sites": "サイト",
"popular_sites": "人気のサイト",
"search_sites": "サイトを検索",
"view_all": "すべて表示",
+ "view_more": "さらに表示",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "基本機能を有効にする",
"basic_functionality_disabled_title": "閲覧を利用できません",
"basic_functionality_disabled_description": "基本機能が無効になっていると、必要なメタデータを取得できません。",
@@ -8995,6 +9131,14 @@
"crypto": "仮想通貨",
"sports": "スポーツ",
"dapps": "サイト"
+ },
+ "search_tabs": {
+ "all": "すべて",
+ "crypto": "Cryptos",
+ "perps": "パーペチュアル",
+ "stocks": "株式",
+ "predictions": "予測",
+ "sites": "サイト"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "このネットワークにはmUSDがありません。ネットワークを切り替えてお持ちのmUSDをご確認ください。",
"money_empty_state": {
"get_started": "開始",
+ "earn": "獲得",
"earn_apy": "年換算利率 (APY) {{percentage}}%を獲得"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "関連する資産",
"perpetuals": "パーペチュアル",
"predictions": "予測",
- "whats_happening": "現在起きていること",
- "whats_happening_ai": "AI",
- "whats_happening_impact": {
- "bullish": "強気",
- "bearish": "弱気",
- "neutral": "中立"
- },
"top_traders": "トップトレーダー",
- "whats_happening_categories": {
- "geopolitical": "地政学",
- "macro": "マクロ",
- "regulatory": "規制",
- "technical": "技術",
- "social": "社会",
- "other": "その他"
- },
"defi": "DeFi",
"nfts": "NFT",
"trending_tokens": "人気上昇中",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "人気"
+ },
+ "whats_happening": {
+ "title": "現在起きていること",
+ "ai": "AI",
+ "impact": {
+ "bullish": "強気",
+ "bearish": "弱気",
+ "neutral": "中立"
+ },
+ "categories": {
+ "geopolitical": "地政学",
+ "macro": "マクロ",
+ "regulatory": "規制",
+ "technical": "技術",
+ "social": "社会",
+ "other": "その他"
+ }
}
}
diff --git a/locales/languages/ko.json b/locales/languages/ko.json
index 7cc2c186ef7b..049bfb3f590c 100644
--- a/locales/languages/ko.json
+++ b/locales/languages/ko.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "사용 가능한 자금이 없습니다. 다른 계정을 사용하세요."
},
+ "headless_buy_error": {
+ "title": "법정화폐 구매 실패",
+ "message": "법정화폐 구매 시 오류가 발생했습니다. 다시 시도하세요."
+ },
"mmpay_hardware_account": {
"title": "지원되지 않는 지갑",
"message": "하드웨어 지갑이 지원되지 않습니다. 다른 지갑으로 전환하여 계속하세요."
@@ -133,8 +137,8 @@
"message": "수신자 주소가 직접 토큰 전송을 지원하지 않을 수 있습니다. 이 경우 자금이 손실될 수 있습니다. 이 계약이 전송되는 토큰을 받을 수 있다는 확신이 있을 때만 계속하세요."
},
"gas_sponsorship_reserve_balance": {
- "message": "이 트랜잭션에는 가스 후원이 제공되지 않습니다. 계정에 최소 %{minBalance}의 %{nativeTokenSymbol} 토큰을 보유해야 합니다.",
- "title": "가스 후원 이용 불가"
+ "message": "이 네트워크에서는 계정에 최소 %{minBalance} %{nativeTokenSymbol}의 잔액을 유지해야 합니다.",
+ "title": "최소 잔액이 필요합니다"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "토큰의 계약 주소로 토큰을 보내고 있습니다. 이로 인해 해당 토큰이 손실될 수 있습니다.",
"smart_contract_address": "스마트 계약 주소",
"smart_contract_address_warning": "수신자 주소가 직접 토큰 전송을 지원하지 않을 수 있습니다. 이 경우 자금이 손실될 수 있습니다. 이 계약이 전송되는 토큰을 받을 수 있다는 확신이 있을 때만 계속하세요.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "업데이트",
"i_understand": "견적은 다음 기간 전에 만료됨을",
"cancel": "취소",
"new_address_title": "새 주소",
@@ -1067,8 +1074,8 @@
"twitter_link": "X(Twitter)에서 보기",
"sort": {
"value": "가격",
- "pnl_percent": "P&L %",
- "recent": "Recent"
+ "pnl_percent": "손익 %",
+ "recent": "최근"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "{{symbol}} 무기한 선물 거래",
"subtitle": "최대 {{leverage}}까지 손익 확대"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "지원팀에 문의"
+ },
"today": "오늘",
"yesterday": "어제",
"unrealized_pnl": "미실현 손익",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC가 지갑으로 전송되었습니다",
"toast_completed_any_token_subtitle": "{{amount}} {{token}}이(가) 지갑으로 이동했습니다",
"toast_error_title": "문제가 발생했습니다",
- "toast_error_description": "출금 실패"
+ "toast_error_description": "출금 실패",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "다시 시도"
},
"quote": {
"network_fee": "네트워크 수수료",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "MetaMask 예측",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "월드컵",
+ "banner_title": "월드컵 2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
- "props": "Props",
- "live": "Live"
+ "all": "모두",
+ "props": "프롭",
+ "live": "진행 중"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
- "final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_stage": "조별 예선",
+ "round_of_32": "32강",
+ "round_of_16": "16강",
+ "quarterfinals": "8강",
+ "semifinals": "준결승",
+ "third_place": "3위",
+ "final": "종료",
+ "group_a": "A조",
+ "group_b": "B조",
+ "group_c": "C조",
+ "group_d": "D조",
+ "group_e": "E조",
+ "group_f": "F조",
+ "group_g": "G조",
+ "group_h": "H조",
+ "group_i": "I조",
+ "group_j": "J조",
+ "group_k": "K조",
+ "group_l": "L조",
+ "third_place_match": "3위 결정전"
}
},
"prediction_markets": "예측 시장",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "출금 완료",
"withdraw_completed_subtitle": "{{amount}} USDC가 지갑으로 전송되었습니다",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}}이(가) 지갑으로 이동했습니다",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "출금 일시 불가",
+ "unavailable_description": "긴급한 지원이 필요한 경우 고객센터로 문의해 주세요.",
"unavailable_got_it": "컨펌",
"error_title": "문제가 발생했습니다",
"error_description": "출금에 실패했습니다",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "동의하고 종료하기",
"pna25_open_settings_button": "설정 열기"
},
+ "onboarding_interest_questionnaire": {
+ "title": "MetaMask에서 무엇을 하고 싶으세요?",
+ "description": "해당 사항을 모두 선택하세요.",
+ "option_buy_and_sell_crypto": "암호자산 매매",
+ "option_consolidate_wallets": "지갑 통합",
+ "option_advanced_trades": "고급 거래 기능 이용",
+ "option_predict_sports_events": "스포츠 및 이벤트 예측",
+ "option_crypto_as_money": "암호자산을 화폐로 사용",
+ "option_connect_apps_sites": "앱 또는 사이트에 연결",
+ "continue": "계속"
+ },
"template_confirmation": {
"ok": "확인",
"cancel": "취소"
@@ -3261,8 +3286,27 @@
"notifications_desc": "알림 관리",
"allow_notifications": "알림 허용",
"enable_push_notifications": "푸시 알림 활성화",
- "allow_notifications_desc": "알림을 통해 지갑에서 무슨 일이 일어나고 있는지 계속 확인하세요. 알림 기능을 사용하려면 프로필을 사용해 기기 간에 일부 설정을 동기화해야 합니다.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "비활성화",
+ "select_all": "전체 선택",
+ "deselect_all": "전체 선택 해제",
+ "select_accounts_title": "계정",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "알림 맞춤 설정",
"customize_session_desc": "수신하려는 알림 유형 켜기:",
"account_session_title": "계정 활동",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snap",
"snaps_desc": "새 기능 및 업데이트",
"products_announcements_title": "제품 공지",
- "products_announcements_desc": "새 제품 및 기능",
- "perps_title": "무기한 선물 거래"
+ "products_announcements_desc": "새 제품 및 기능"
},
"contacts_title": "연락처",
"contacts_desc": "계정 추가, 편집, 제거 및 관리.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "카드",
"reset_onboarding_description": "온보딩 흐름을 처음부터 다시 시작하려면 카드 온보딩 상태를 초기화하세요.",
- "reset_onboarding_button": "온보딩 상태 초기화"
+ "reset_onboarding_button": "온보딩 상태 초기화",
+ "unlink_money_account_description": "카드에 부여된 머니 계정의 USDC 지출 한도 승인을 취소합니다. 이 작업을 통해 approve(0) 트랜잭션을 제출하고 카드 백엔드에서 머니 계정을 위임되지 않은 상태로 표시합니다.",
+ "unlink_money_account_button": "머니 계정과 카드의 연결 해제",
+ "unlink_money_account_disabled_hint": "제거할 활성 연결이 없습니다."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "햅틱",
@@ -3842,8 +3893,8 @@
"predict_button": "예측",
"add_collectible_button": "추가",
"info": "정보",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "일괄 매도",
+ "batch_sell_new_label": "신규",
"swap": "스왑",
"convert": "전환",
"bridge": "브릿지",
@@ -3883,7 +3934,7 @@
"troubleshoot": "문제 해결",
"deposit_description": "수수료가 낮은 은행 또는 카드 이체",
"buy_description": "특정 토큰 매수에 적합",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "최대 5개 토큰을 매도하여 스테이블코인 수령",
"sell_description": "암호화폐를 현금에 매도",
"swap_description": "토큰 간 교환",
"bridge_description": "네트워크간 토큰 전송",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "설정 > 알림",
"cancel": "취소",
"cta": "켜기"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "매매 기회를 놓치지 마세요",
+ "body": "목표 가격 도달, 거래 체결, 포트폴리오 변동 시 실시간 알림을 받아보세요. 제품 업데이트와 보상 정보도 함께 확인할 수 있습니다. MetaMask 이용 내역을 기반으로 맞춤형 정보를 보내드립니다.",
+ "button_yes": "예",
+ "button_not_now": "나중에",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "지금",
+ "title": "오늘 ETH가 4.2% 상승했습니다",
+ "message": "현재 $2,668.51입니다. 설정한 가격 알림 기준을 넘어섰습니다"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "1시간 전",
+ "title": "0.25 ETH 받음",
+ "message": "보낸 주소: 0x9a21…4f8c · $640.29"
+ }
+ },
+ "existing_user": {
+ "title": "맞춤형 알림 소개",
+ "body": "거래 방식에 맞는 알림을 받으세요. 언제든지 설정을 변경할 수 있습니다.",
+ "card_title": "혜택",
+ "card_description": "맞춤형 알림과 거래 내역에 기반한 정보를 받을 수 있습니다.",
+ "button_confirm": "컨펌",
+ "button_not_now": "나중에"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "결제 수단:",
"buying_via": "{{providerName}}을(를) 통해 구매",
"change_provider": "공급자를 변경하세요.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "오류가 발생했습니다. 다시 시도해 주세요.",
"no_payment_methods_available": "사용 가능한 결제 방법이 없습니다.",
"error_fetching_quotes": "오류가 발생했습니다. 다시 시도해 주세요.",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "mUSD로 전환",
"get_a_percentage_musd_bonus": "{{percentage}}%의 mUSD 보너스 혜택",
"convert": "전환",
+ "confirm": "컨펌",
+ "convert_tooltip_description": "스테이블코인을 mUSD로 전환하고 연간 최대 {{percentage}}% 보너스를 받으세요. 보너스는 매일 수령할 수 있습니다. Relay에서 제공합니다.",
"fetching_quote": "견적 가져오기...",
"you_convert": "전환 전:",
"network_fee": "네트워크 수수료",
@@ -6597,7 +6679,7 @@
"step_progress": "{{current}} 단계/{{total}} 단계",
"title": "자금 추가",
"description": "계정에 입금하고 연 수익(APY)을 버세요.",
- "add": "추가",
+ "add": "자금 추가",
"step2_title": "MetaMask 카드 받기",
"step2_description": "Mastercard가 허용되는 모든 곳에서 수익이 쌓이는 동안 Money 잔액을 사용하세요.",
"step2_cta": "카드 받기",
@@ -6605,6 +6687,19 @@
"link_card_description": "Mastercard가 허용되는 모든 곳에서 수익이 쌓이는 동안 잔액을 사용하세요.",
"link_card_cta": "카드 연결"
},
+ "rive_onboarding": {
+ "step1_title": "머니 계정을 소개합니다",
+ "step1_body": "지갑 전체에서 사용할 수 있는 잔액에 대해 최대 {{percentage}}% APY의 수익을 얻을 수 있습니다.",
+ "step1_footer_text": "APY는 변동형이므로 언제든지 변경될 수 있습니다.",
+ "step2_title": "자동으로 수익 얻기",
+ "step2_body": "환전 수수료 없이 스테이블코인을 이동하세요. 자금에 대한 수익 창출이 바로 시작됩니다.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "어디서든 결제 가능",
+ "step3_body": "머니 계정을 MetaMask 카드에 연결하면 구매 시 최대 {{percentage}}%의 캐시백을 받을 수 있습니다.",
+ "step4_title": "한곳에서 거래하고 수익을 적립하세요",
+ "step4_body": "MetaMask에서 머니 계정 잔액으로 거래하면서 수익을 계속 적립할 수 있습니다.",
+ "continue": "계속"
+ },
"action": {
"add": "추가",
"transfer": "송금",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "작동 방식",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "머니 계정에 mUSD를 입금하면 적립할 수 있는 최대 수익은 다음과 같습니다",
+ "description_suffix": ". 잔액은 달러 연동 방식으로 유지되며 언제든지 결제, 거래, 송금에 사용될 수 있습니다."
},
"musd_row": {
"add": "추가"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "머니 잔액",
"add": "추가",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "머니 잔액",
+ "info_sheet_body": "언제든지 결제, 송금, 거래에 사용할 수 있는 달러 연동 방식의 mUSD 잔액입니다. 이 금액은 총 계정 잔액 계산 시 포함되지 않습니다.\n\n출금은 해당 블록체인의 일반적인 네트워크 컨펌 시간을 기준으로 즉시 처리됩니다. 다만 유동성이 부족한 경우 일시적으로 지연될 수 있습니다."
},
"potential_earnings": {
"title": "암호화폐로 수익 얻기",
"description": "암호화폐를 mUSD로 전환했을 때 자금이 시간에 따라 어떻게 늘어날 수 있는지 확인해 보세요.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "자산 중 {{total}}을(를) 전환하여 적립할 수 있는 최대 수익은 다음과 같습니다",
+ "description_with_amounts_suffix": "1년 후.",
"convert": "전환",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "암호자산 전환",
"no_fee": "MetaMask 수수료 없음",
"view_all": "모두 보기",
"view_potential_earnings": "예상 수익 보기"
@@ -6649,41 +6744,44 @@
"cashback": "{{percentage}}% mUSD 캐시백",
"get_now": "지금 받기",
"link_title": "MetaMask 카드 연결",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "머니 계정 잔액 사용 시 구매 금액에 대해 캐시백을 받을 수 있습니다. 또한 잔액에 대해 {{apy}}% APY의 수익이 적립됩니다.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "{{percentage}}% mUSD 캐시백 적립",
+ "link_bullet_apy": "최대 {{apy}}% APY 수익 적립",
"link_card": "카드 연결",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "지출하면서 수익을 올리세요",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "카드 연결",
+ "manage_card": "관리",
+ "avail_balance": "사용 가능한 잔액"
},
"what_you_get": {
"title": "혜택",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "자동으로 적립될 수 있는 최대 수익",
"benefit_dollar_backed": "1:1 달러 연동 스테이블코인인 mUSD로 자금을 안전하게 보관하세요",
"benefit_liquidity": "락업 없이 완전한 유동성을 제공하므로 언제든지 거래하거나 출금할 수 있습니다",
"benefit_spend_prefix": "MetaMask Card로 1억 5천만 개 이상의 가맹점에서 결제하고 ",
"benefit_spend_cashback": "1-3% mUSD 캐시백",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "MetaMask의 모든 지갑으로 전송할 수 있습니다",
+ "benefit_global": "전 세계 어디서나 자금을 보내고 받을 수 있습니다",
"learn_more": "더 보기"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "자금 추가"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "자금 추가",
"convert_crypto": "암호자산 전환",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "모든 계정에서",
"deposit_funds": "자금 예치",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "직불카드 또는 은행에서",
+ "move_musd": "{{amount}} mUSD 전송",
+ "move_musd_no_amount": "mUSD 전송",
+ "move_musd_description": "잔액에서",
"receive_external": "외부 지갑에서 받기",
"coming_soon": "곧 추가 예정"
},
@@ -6694,7 +6792,7 @@
"contact_support": "지원팀에 문의"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "자금 전송",
"between_accounts": "계좌 간",
"perps_account": "무기한 선물 계정",
"predictions_account": "예측 계정",
@@ -6712,8 +6810,8 @@
"body": "현재 잔액과 오늘의 연간수익률을 기준으로 계산한 일정 기간의 예상 수익입니다. 예상 수익은 보장되지 않으며 변동될 수 있습니다."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "암호화폐로 수익 얻기",
+ "body": "본 예시는 {{percentage}}% 연이율(APY)이 1년 동안 유지된다는 가정하에 계산되었습니다. APY는 변동형이므로 다양한 요인에 따라 변경될 수 있습니다. 수익은 보장되지 않습니다."
},
"activity": {
"title": "활동",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "작동 방식",
- "how_it_works_subtitle": "자금이 어떻게 수익을 내는지 확인하세요",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "mUSD 자세히 알아보기",
"what_you_get_title": "혜택",
@@ -6738,7 +6836,8 @@
"sent": "보낸",
"transferred": "송금됨",
"card_transaction": "카드 거래",
- "converted": "전환됨"
+ "converted": "전환됨",
+ "failed": "실패"
},
"convert_stablecoins": {
"title": "스테이블코인 전환하기",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "머니",
"section_title": "작동 방식",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "mUSD를 머니 계정에 입금하면 최대 {{percentage}}% APY(변동형)의 수익을 자동으로 적립할 수 있습니다. 자금은 검증된 대출 시장 전반에서 수익을 창출하는 디파이 볼트에 예치되며, 스테이킹, 수령, 락업 등의 과정을 거칠 필요 없이 이용할 수 있습니다.",
"description_2": "Money 잔액은 결제에 사용할 수 있는 잔액입니다. MetaMask 카드를 연결하면 전 세계 1억 5천만 개 이상의 가맹점에서 사용할 수 있습니다. 자금은 사용하는 순간까지 계속 수익을 냅니다.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "자주 묻는 질문",
"faq_placeholder_answer": "곧 추가 예정.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "{{percentage}}% APY는 어떻게 적용되나요?",
"faq_q2": "mUSD란 무엇인가요?",
"faq_q3": "수익은 어디에서 발생하나요?",
"faq_q4": "자금은 묶이나요? 언제든지 인출할 수 있나요?",
@@ -7030,11 +7129,12 @@
"confirm": "컨펌",
"pay_with_bottom_sheet": {
"title": "결제 수단:",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "마지막 사용",
+ "bank_and_card": "은행 및 카드",
+ "crypto": "암호화폐",
+ "available_balance": "{{balance}} 사용 가능",
+ "other_assets": "기타 자산",
+ "other_assets_description": "보유한 토큰 중에서 선택"
},
"staking_footer": {
"part1": "계속 진행하면 당사의 ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "mUSD 환전 수수료에는 네트워크 비용이 포함되며 제공업체 수수료가 포함될 수 있습니다. mUSD로 환전할 때 MetaMask 수수료는 적용되지 않습니다."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMask가 mUSD를 원하는 토큰으로 스왑해 드립니다. 스왑 제공업체에서 수수료를 부과할 수 있지만, MetaMask는 수수료가 없습니다."
},
"title": {
"transaction_fee": "수수료"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "자금 추가",
"deposit_edit_amount_predict_withdraw": "출금",
"deposit_edit_amount_musd_conversion": "mUSD로 전환",
- "preparing_order": "Preparing order"
+ "preparing_order": "주문 준비"
},
"change_in_simulation_modal": {
"title": "결과가 변경되었습니다",
@@ -7250,20 +7350,33 @@
"confirm_swap": "스왑",
"terms_and_conditions": "이용약관",
"select_token": "토큰 선택",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "최대 5개 토큰 선택",
+ "batch_sell_select_subtitle": "모든 토큰은 동일한 네트워크에 있어야 합니다.",
+ "batch_sell_empty_state_title": "토큰이 없습니다. 그래도 문제없습니다.",
+ "batch_sell_empty_state_description": "토큰이 없습니다. 그래도 문제없습니다. 둘러보고 토큰을 매입한 후 일괄 매도하세요.",
+ "batch_sell_continue_with_one_token": "(1)개 토큰으로 계속하기",
+ "batch_sell_continue_with_tokens": "{{tokenCount}}개 토큰으로 계속하기",
+ "batch_sell_max_tokens_allowed": "최대 5개 토큰 허용됨",
+ "batch_sell_single_token_dialog_title": "높은 환율 알림",
+ "batch_sell_single_token_dialog_description": "하나의 토큰을 일괄 매도하면 환율이 높아질 수 있습니다. 그 대신 스왑을 하시겠습니까?",
+ "batch_sell_swap_instead": "예, 스왑하겠습니다",
+ "batch_sell_review_title": "일괄 매도 검토",
+ "batch_sell_select_stablecoin": "스테이블코인 선택",
+ "batch_sell_total_received": " 총 수령량",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "검토",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "{{tokenSymbol}} 맞춤 설정",
+ "batch_sell_remove_token": "{{tokenSymbol}} 제거",
+ "batch_sell_checkbox_label": "토큰 일괄 매도",
+ "sort_balance": "잔액",
+ "next": "다음",
+ "explore_tokens": "토큰 탐색",
"no_tokens_found": "토큰을 찾을 수 없습니다",
"no_tokens_found_description": "해당 이름의 토큰을 찾을 수 없습니다. 다른 검색어를 입력해 보세요.",
"select_network": "네트워크 선택",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "이 스왑으로 토큰 가치의 약 {{priceImpact}}을(를) 잃게 됩니다. 금액을 낮추거나 유동성이 더 많은 경로를 선택해 보세요.",
"proceed": "진행",
"cancel": "취소",
+ "close": "닫기",
"slippage_info_title": "슬리피지",
"slippage_info_description": "사용자가 허용하는 가격 변동률(%)로, 가격이 이보다 크게 변동하면 트랜잭션이 취소됩니다.",
"blockaid_error_title": "이 트랜잭션은 취소됩니다",
@@ -8107,7 +8221,14 @@
"retry": "다시 시도",
"on_linea": "Linea 네트워크에서",
"account_label": "계정",
- "token_label": "토큰"
+ "token_label": "토큰",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "mUSD 캐시백",
@@ -8445,6 +8566,7 @@
"show_less": "요약 보기",
"linking_progress": "계정 추가 중...({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} 등록됨",
+ "accounts_added": "Accounts added",
"add_all_accounts": "모든 계정 추가",
"environment_selector": "환경",
"environment_cancel": "취소",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "보상 프로그램 영구 탈퇴",
- "description": "이 작업을 수행하면 보상 프로그램에서 계정이 지워지며 진행도도 삭제됩니다. 이 작업은 되돌릴 수 없습니다.",
- "confirm": "영구 탈퇴",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "진행하시겠습니까?",
- "confirmation_description": "이 작업을 수행하면 모든 진행 상황이 제거되며 복원할 수 없습니다. 보상 프로그램에 다시 참여할 경우 처음부터 다시 시작해야 합니다.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "취소",
"confirm": "컨펌",
+ "error_title": "문제가 발생했습니다",
"error_message": "보상 프로그램을 탈퇴할 수 없습니다. 다시 시도해 보세요.",
"processing": "처리 중..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "닫기",
@@ -8562,7 +8691,8 @@
"title_claim": "혜택 수령하기",
"action": "청구",
"empty-list": "현재 보유한 혜택이 없습니다.",
- "powered_by": "제공자:"
+ "powered_by": "제공자:",
+ "available_count": "{{count}}개 사용 가능"
},
"end_of_season_rewards": {
"confirm_label_default": "컨펌",
@@ -8695,7 +8825,7 @@
"label_your_rank": "내 등급",
"label_portfolio": "Portfolio",
"label_net_inflow": "순유입",
- "label_total_inflow": "총 유입",
+ "label_total_inflow": "총 유입액",
"label_outflow": "순유출",
"label_days_held": "보유 일수",
"qualified_title": "자격 충족",
@@ -8880,9 +9010,11 @@
"musd_claim": "mUSD 받기",
"perps_deposit": "자금 추가",
"perps_withdraw": "출금",
+ "predict_withdraw": "{{sourceChain}}에서 {{sourceSymbol}} 인출",
"predict_deposit": "자금 추가",
"swap": "토큰 스왑",
- "swap_approval": "토큰 승인"
+ "swap_approval": "토큰 승인",
+ "fiat_purchase": "{{paymentMethod}}(으)로 {{token}} 매입"
},
"perps_deposit_solution": "이제 Arbitrum에 {{fiat}} USDC가 있습니다. 다시 입금해 보세요."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "높은 순",
"low_to_high": "낮은 순",
"apply": "적용",
- "search_placeholder": "토큰, 사이트 및 URL 검색",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "취소",
"perps": "무기한 선물",
"rwa_perps_section": "무기한 선물",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "무기한 선물",
"predictions": "예측",
"no_results": "검색 결과가 없습니다",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "인기",
"sites": "사이트",
"popular_sites": "인기 사이트",
"search_sites": "사이트 검색",
"view_all": "모두 보기",
+ "view_more": "더 보기",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "기본 기능 활성화",
"basic_functionality_disabled_title": "탐색 기능을 사용할 수 없습니다",
"basic_functionality_disabled_description": "기본 기능이 비활성화되어 있어 필요한 메타데이터를 가져올 수 없습니다.",
@@ -8995,6 +9131,14 @@
"crypto": "암호화폐",
"sports": "스포츠",
"dapps": "사이트"
+ },
+ "search_tabs": {
+ "all": "모두",
+ "crypto": "Cryptos",
+ "perps": "무기한 선물",
+ "stocks": "주식",
+ "predictions": "예측",
+ "sites": "사이트"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "이 네트워크에는 mUSD가 없습니다. mUSD를 확인하려면 네트워크를 전환하세요.",
"money_empty_state": {
"get_started": "지금 시작하세요",
+ "earn": "수익 창출",
"earn_apy": "{{percentage}}% APY 수익 얻기"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "관련 자산",
"perpetuals": "영구계약",
"predictions": "예측",
- "whats_happening": "주요 동향",
- "whats_happening_ai": "AI",
- "whats_happening_impact": {
- "bullish": "강세",
- "bearish": "약세",
- "neutral": "중립"
- },
"top_traders": "상위 트레이더",
- "whats_happening_categories": {
- "geopolitical": "지정학",
- "macro": "거시 경제",
- "regulatory": "규제",
- "technical": "기술",
- "social": "사회",
- "other": "기타"
- },
"defi": "디파이",
"nfts": "NFT",
"trending_tokens": "인기",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "인기"
+ },
+ "whats_happening": {
+ "title": "주요 동향",
+ "ai": "AI",
+ "impact": {
+ "bullish": "강세",
+ "bearish": "약세",
+ "neutral": "중립"
+ },
+ "categories": {
+ "geopolitical": "지정학",
+ "macro": "거시 경제",
+ "regulatory": "규제",
+ "technical": "기술",
+ "social": "사회",
+ "other": "기타"
+ }
}
}
diff --git a/locales/languages/pt.json b/locales/languages/pt.json
index d6203af5e32b..9ed340e9f12b 100644
--- a/locales/languages/pt.json
+++ b/locales/languages/pt.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "Não há fundos disponíveis. Use outra conta."
},
+ "headless_buy_error": {
+ "title": "A compra de moeda fiduciária falhou",
+ "message": "Ocorreu um erro em sua compra de moeda fiduciária. Tente novamente."
+ },
"mmpay_hardware_account": {
"title": "Não há suporte para esta carteira",
"message": "Não oferecemos suporte para carteiras de hardware.\nTroque de carteira para continuar."
@@ -133,8 +137,8 @@
"message": "O endereço do destinatário pode não aceitar transferências diretas de tokens, o que pode resultar na perda de fundos. Prossiga apenas se tiver certeza de que este contrato pode receber sua transferência."
},
"gas_sponsorship_reserve_balance": {
- "message": "O patrocínio de gas não está disponível para esta transação. Você precisará manter pelo menos %{minBalance} %{nativeTokenSymbol} em sua conta.",
- "title": "Patrocínio de gas não disponível"
+ "message": "Esta rede específica requer que você mantenha uma reserva de %{minBalance} %{nativeTokenSymbol} em sua conta.",
+ "title": "É obrigatório ter saldo de reserva"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "Você está enviando tokens para o endereço do contrato do token. Isso pode levar à perda desses tokens.",
"smart_contract_address": "Endereço de contrato inteligente",
"smart_contract_address_warning": "O endereço do destinatário pode não aceitar transferências diretas de tokens, o que pode resultar na perda de fundos. Prossiga apenas se tiver certeza de que este contrato pode receber sua transferência.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "Atualizar",
"i_understand": "Eu compreendo",
"cancel": "Cancelar",
"new_address_title": "Novo endereço",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "Valor",
"pnl_percent": "P&L %",
- "recent": "Recent"
+ "recent": "Recente"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "Negociar perp de {{symbol}}",
"subtitle": "Multiplique seu P&L até {{leverage}}"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "Falar com o suporte"
+ },
"today": "Hoje",
"yesterday": "Ontem",
"unrealized_pnl": "P&L não realizados",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC transferidos para sua carteira",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} transferidos para sua carteira",
"toast_error_title": "Ocorreu algum erro",
- "toast_error_description": "Não foi possível prosseguir com o saque"
+ "toast_error_description": "Não foi possível prosseguir com o saque",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Tentar novamente"
},
"quote": {
"network_fee": "Taxa de rede",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "Previsões da MetaMask",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "Copa do Mundo",
+ "banner_title": "Copa do Mundo 2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
+ "all": "Tudo",
"props": "Props",
- "live": "Live"
+ "live": "Em tempo real"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
+ "group_stage": "Fase de grupos",
+ "round_of_32": "Primeira fase eliminatória",
+ "round_of_16": "Oitavas de final",
+ "quarterfinals": "Quartas de final",
+ "semifinals": "Semifinais",
+ "third_place": "Terceiro lugar",
"final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_a": "Grupo A",
+ "group_b": "Grupo B",
+ "group_c": "Grupo C",
+ "group_d": "Grupo D",
+ "group_e": "Grupo E",
+ "group_f": "Grupo F",
+ "group_g": "Grupo G",
+ "group_h": "Grupo H",
+ "group_i": "Grupo I",
+ "group_j": "Grupo J",
+ "group_k": "Grupo K",
+ "group_l": "Grupo L",
+ "third_place_match": "Disputa pelo terceiro lugar"
}
},
"prediction_markets": "Mercados de previsão",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "Saque concluído",
"withdraw_completed_subtitle": "{{amount}} USDC transferidos para sua carteira",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}} transferidos para sua carteira",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "Saques temporariamente indisponíveis",
+ "unavailable_description": "Para assistência urgente, entre em contato com o Serviço de Atendimento ao Cliente.",
"unavailable_got_it": "Entendi",
"error_title": "Ocorreu algum erro",
"error_description": "Falha ao continuar com a retirada",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "Aceitar e fechar",
"pna25_open_settings_button": "Abrir Configurações"
},
+ "onboarding_interest_questionnaire": {
+ "title": "O que você deseja fazer com a MetaMask?",
+ "description": "Selecione todas as opções aplicáveis.",
+ "option_buy_and_sell_crypto": "Comprar e vender criptomoedas",
+ "option_consolidate_wallets": "Consolidar suas carteiras",
+ "option_advanced_trades": "Fazer negociações avançadas",
+ "option_predict_sports_events": "Prever esportes e eventos",
+ "option_crypto_as_money": "Usar criptomoedas como dinheiro",
+ "option_connect_apps_sites": "Conectar-se a aplicativos ou sites",
+ "continue": "Continuar"
+ },
"template_confirmation": {
"ok": "OK",
"cancel": "Cancelar"
@@ -3261,8 +3286,27 @@
"notifications_desc": "Gerencie suas notificações",
"allow_notifications": "Permitir notificações",
"enable_push_notifications": "Ativar notificações push",
- "allow_notifications_desc": "Fique por dentro do que acontece na sua carteira com as notificações. Para fazer isso, usamos um perfil para sincronizar algumas configurações entre seus dispositivos.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "Off",
+ "select_all": "Marcar tudo",
+ "deselect_all": "Desmarcar tudo",
+ "select_accounts_title": "Contas",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "Personalize suas notificações",
"customize_session_desc": "Ative os tipos de notificações que você deseja receber:",
"account_session_title": "Atividade da conta",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snaps",
"snaps_desc": "Novos recursos e atualizações",
"products_announcements_title": "Comunicados sobre produtos",
- "products_announcements_desc": "Novos produtos e recursos",
- "perps_title": "Negociação de perps"
+ "products_announcements_desc": "Novos produtos e recursos"
},
"contacts_title": "Contatos",
"contacts_desc": "Adicione, edite, remova e gerencie suas contas.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "Cartão",
"reset_onboarding_description": "Redefina o estado de integração do Cartão para iniciar o fluxo de integração desde o início.",
- "reset_onboarding_button": "Redefinir estado de integração"
+ "reset_onboarding_button": "Redefinir estado de integração",
+ "unlink_money_account_description": "Revogar a autorização de limite de gasto de USDC que permite ao Cartão gastar a partir da sua Conta Money. Isso envia uma transação de aprovar(0) e marca a Conta Money como não delegada no sistema interno do Cartão.",
+ "unlink_money_account_button": "Desvincular conta Money do Cartão",
+ "unlink_money_account_disabled_hint": "Nenhum vínculo ativo para remover."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "Háptica",
@@ -3842,8 +3893,8 @@
"predict_button": "Previsões",
"add_collectible_button": "Adicionar",
"info": "Informações",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "Venda em lote",
+ "batch_sell_new_label": "Nova",
"swap": "Troca",
"convert": "Converter",
"bridge": "Ponte",
@@ -3883,7 +3934,7 @@
"troubleshoot": "Solução de problemas",
"deposit_description": "Transferência bancária ou com cartão com taxas baixas",
"buy_description": "Bom para comprar um token específico",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "Venda até 5 tokens por uma moeda estável",
"sell_description": "Venda criptoativos por dinheiro",
"swap_description": "Faça câmbio entre tokens",
"bridge_description": "Transferir tokens entre redes",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "Configurações > Notificações.",
"cancel": "Cancelar",
"cta": "Ativar"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "Não perca nenhum movimento",
+ "body": "Receba alertas em tempo real quando os preços atingirem suas metas, suas negociações forem confirmadas e seu portfólio for movimentado. Além disso, receba atualizações de produtos e recompensas ao longo do caminho. Compartilharemos atualizações com base em suas interações com a MetaMask.",
+ "button_yes": "Sim",
+ "button_not_now": "Agora não",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "agora",
+ "title": "O ETH subiu 4,2% hoje",
+ "message": "Agora está em $ 2.668,51 — acima do seu alerta de preço"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "1 h atrás",
+ "title": "0,25 ETH recebido",
+ "message": "De 0x9a21…4f8c · $ 640,29"
+ }
+ },
+ "existing_user": {
+ "title": "Apresentando alertas personalizados",
+ "body": "Receba notificações que correspondem à forma como você negocia. Atualize a qualquer momento.",
+ "card_title": "O que você receberá",
+ "card_description": "Alertas e atualizações personalizados, adaptados à sua atividade de negociação.",
+ "button_confirm": "Confirmar",
+ "button_not_now": "Agora não"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "Pagar com",
"buying_via": "Comprando via {{providerName}}.",
"change_provider": "Alterar fornecedor.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "Algo deu errado. Tente novamente.",
"no_payment_methods_available": "Nenhum método de pagamento disponível.",
"error_fetching_quotes": "Algo deu errado. Tente novamente.",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "Converter para mUSD",
"get_a_percentage_musd_bonus": "Ganhe {{percentage}}% de bônus em mUSD",
"convert": "Converter",
+ "confirm": "Confirmar",
+ "convert_tooltip_description": "Converta suas moedas estáveis em mUSD e ganhe um bônus anualizado de até {{percentage}}% que pode ser resgatado diariamente. Desenvolvido por Relay.",
"fetching_quote": "Buscando cotação...",
"you_convert": "Você converte",
"network_fee": "Taxa de rede",
@@ -6597,7 +6679,7 @@
"step_progress": "Etapa {{current}} de {{total}}",
"title": "Adicionar dinheiro",
"description": "Deposite fundos em sua conta e comece a ganhar rendimento percentual anual (APY).",
- "add": "Adicionar",
+ "add": "Adicionar fundos",
"step2_title": "Peça o seu Cartão MetaMask",
"step2_description": "Use o saldo do seu cartão Money enquanto ele rende, em qualquer lugar que aceite o Mastercard.",
"step2_cta": "Peça o cartão",
@@ -6605,6 +6687,19 @@
"link_card_description": "Use seu saldo enquanto ele rende, em qualquer lugar que aceite o Mastercard.",
"link_card_cta": "Vincular cartão"
},
+ "rive_onboarding": {
+ "step1_title": "As contas Money chegaram",
+ "step1_body": "Ganhe até {{percentage}}% de APY sobre o seu saldo, disponível em toda a sua carteira.",
+ "step1_footer_text": "A APY é variável e pode ser alterada a qualquer momento.",
+ "step2_title": "Ganhe automaticamente",
+ "step2_body": "Transfira moedas estáveis sem taxas de câmbio. Seus fundos começam a render imediatamente.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "Gaste em qualquer lugar",
+ "step3_body": "Receba até {{percentage}}% de cashback em compras ao vincular sua conta Money a um cartão MetaMask.",
+ "step4_title": "Negocie e ganhe em um só lugar",
+ "step4_body": "Use o saldo de sua conta Money para negociar na MetaMask e ainda obter rendimento.",
+ "continue": "Continuar"
+ },
"action": {
"add": "Adicionar",
"transfer": "Transferir",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "Como funciona",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "Deposite mUSD em sua conta Money e ganhe até",
+ "description_suffix": ". Seu saldo é lastreado em dólares e está pronto para ser gasto, negociado ou enviado a qualquer hora."
},
"musd_row": {
"add": "Adicionar"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Saldo monetário",
"add": "Adicionar",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Saldo monetário",
+ "info_sheet_body": "Seu saldo em mUSD lastreado em dólares, sempre disponível para gastar, enviar ou negociar a qualquer hora. Não incluímos esse valor no saldo total da sua conta.\n\nSaques são processados imediatamente, sujeitos aos períodos de confirmação padrão da rede na blockchain pertinente. Se a liquidez estiver baixa, podem ocorrer atrasos temporários."
},
"potential_earnings": {
"title": "Ganhe com suas criptomoedas",
"description": "Veja como seu dinheiro pode render ao longo do tempo convertendo suas criptomoedas em mUSD.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "Converta seu {{total}} em ativos e você poderá ganhar até",
+ "description_with_amounts_suffix": "em um ano.",
"convert": "Converter",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "Converta suas criptomoedas",
"no_fee": "Sem taxas da MetaMask",
"view_all": "Exibir tudo",
"view_potential_earnings": "Visualizar ganhos potenciais"
@@ -6649,41 +6744,44 @@
"cashback": "{{percentage}}% de cashback em mUSD",
"get_now": "Adquira já",
"link_title": "Vincular cartão MetaMask",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "Use o saldo da sua conta Money e ganhe em compras. Além disso, até {{apy}}% de APY sobre o seu saldo.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "Receba {{percentage}}% de mUSD em cashback",
+ "link_bullet_apy": "Ganhe até {{apy}}% de APY",
"link_card": "Vincular cartão",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "Gaste e ganhe",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "Vincular cartão",
+ "manage_card": "Gerenciar",
+ "avail_balance": "Saldo disponível"
},
"what_you_get": {
"title": "O que você recebe",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "Ganhe automaticamente até",
"benefit_dollar_backed": "Mantenha seu dinheiro seguro em mUSD, uma stablecoin lastreada em dólar na proporção de 1:1",
"benefit_liquidity": "Obtenha liquidez total sem períodos de bloqueio, permitindo que você possa negociar ou sacar quando quiser",
"benefit_spend_prefix": "Gaste em mais de 150 milhões de estabelecimentos comerciais com o cartão MetaMask e ganhe",
"benefit_spend_cashback": "1-3% de cashback em mUSD",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "Transfira para qualquer uma de suas carteiras na MetaMask",
+ "benefit_global": "Envie e receba fundos globalmente",
"learn_more": "Saiba mais"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "Adicionar fundos"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "Adicionar fundos",
"convert_crypto": "Converter criptomoedas",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "A partir de qualquer conta",
"deposit_funds": "Depositar fundos",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "De cartão de débito ou conta bancária",
+ "move_musd": "Transfira seus {{amount}} mUSD",
+ "move_musd_no_amount": "Transfira seus mUSD",
+ "move_musd_description": "Do seu saldo",
"receive_external": "Receber de carteira externa",
"coming_soon": "Em breve"
},
@@ -6694,7 +6792,7 @@
"contact_support": "Falar com o suporte"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "Transferir fundos",
"between_accounts": "Entre contas",
"perps_account": "Conta de perps",
"predictions_account": "Conta de previsões",
@@ -6712,8 +6810,8 @@
"body": "Uma estimativa de quanto você poderia ganhar ao longo de um determinado período, com base no seu saldo atual e na APY de hoje. Estimativas não são garantias de retorno e estão sujeitas a alterações."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "Ganhe com suas criptomoedas",
+ "body": "A ilustração pressupõe que {{percentage}}% da APY permaneça inalterada por um ano. A APY é variável e pode mudar devido a diversos fatores. Não há garantia de retorno."
},
"activity": {
"title": "Atividade",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "Como funciona",
- "how_it_works_subtitle": "Veja como seu dinheiro trabalha para você",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "USD MetaMask",
"musd_subtitle": "Saiba mais sobre mUSD",
"what_you_get_title": "O que você recebe",
@@ -6738,7 +6836,8 @@
"sent": "Enviados",
"transferred": "Transferido",
"card_transaction": "Transação com cartão",
- "converted": "Convertido"
+ "converted": "Convertido",
+ "failed": "Falha"
},
"convert_stablecoins": {
"title": "Converta suas stablecoins",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "Money",
"section_title": "Como funciona",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "Deposite mUSD em sua conta Money e ganhe até {{percentage}}% de APY (variável) automaticamente. Os fundos são colocados em um cofre DeFi que gera rendimentos em mercados de empréstimo auditados — sem staking, sem necessidade de resgate, sem bloqueios.",
"description_2": "O saldo da sua conta Money é o seu saldo disponível para gastos. Vincule seu cartão MetaMask para gastar em mais de 150 milhões de estabelecimentos comerciais em todo o mundo. Seu dinheiro continua rendendo até o momento em que você o utiliza.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "Perguntas frequentes (FAQ)",
"faq_placeholder_answer": "Em breve.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "Como funciona a {{percentage}}% de APY?",
"faq_q2": "O que é mUSD?",
"faq_q3": "De onde vem o rendimento?",
"faq_q4": "Meu dinheiro está bloqueado? Posso sacar a qualquer hora?",
@@ -7030,11 +7129,12 @@
"confirm": "Confirmar",
"pay_with_bottom_sheet": {
"title": "Pagar com",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "Usado pela última vez",
+ "bank_and_card": "Banco e cartão",
+ "crypto": "Cripto",
+ "available_balance": "{{balance}} disponíveis",
+ "other_assets": "Outros ativos",
+ "other_assets_description": "Selecione um de seus tokens"
},
"staking_footer": {
"part1": "Ao continuar, você concorda com os nossos ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "As taxas de conversão de mUSD incluem custos de rede e podem incluir taxas do provedor. A MetaMask não aplica taxas quando você converte para mUSD."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "A MetaMask troca seus mUSD pelo token desejado. Provedores de troca podem cobrar uma taxa, mas a MetaMask não cobra."
},
"title": {
"transaction_fee": "Taxas"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "Adicionar fundos",
"deposit_edit_amount_predict_withdraw": "Sacar",
"deposit_edit_amount_musd_conversion": "Converter para mUSD",
- "preparing_order": "Preparing order"
+ "preparing_order": "Preparando ordem"
},
"change_in_simulation_modal": {
"title": "Os resultados mudaram",
@@ -7250,20 +7350,33 @@
"confirm_swap": "Swap",
"terms_and_conditions": "Termos e condições",
"select_token": "Selecionar token",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "Selecione até 5 tokens",
+ "batch_sell_select_subtitle": "É necessário que todos os tokens estejam na mesma rede.",
+ "batch_sell_empty_state_title": "Sem tokens. Sem problemas.",
+ "batch_sell_empty_state_description": "Sem tokens? Sem problema. Explore e compre tokens para vender em lote.",
+ "batch_sell_continue_with_one_token": "Continuar com (1) token",
+ "batch_sell_continue_with_tokens": "Continuar com ({{tokenCount}}) tokens",
+ "batch_sell_max_tokens_allowed": "Máximo de 5 tokens permitidos",
+ "batch_sell_single_token_dialog_title": "Alerta de taxa alta",
+ "batch_sell_single_token_dialog_description": "A venda em lote de apenas um token pode resultar em uma taxa mais alta. Em vez disso, quer fazer uma troca?",
+ "batch_sell_swap_instead": "Sim, trocar",
+ "batch_sell_review_title": "Análise de vendas em lote",
+ "batch_sell_select_stablecoin": "Selecione uma moeda estável",
+ "batch_sell_total_received": "Total recebido",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "Revisar",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "Personalizar {{tokenSymbol}}",
+ "batch_sell_remove_token": "Remover {{tokenSymbol}}",
+ "batch_sell_checkbox_label": "Venda de token em lote",
+ "sort_balance": "Saldo",
+ "next": "Avançar",
+ "explore_tokens": "Explorar tokens",
"no_tokens_found": "Nenhum token encontrado",
"no_tokens_found_description": "Não encontramos tokens com este nome. Tente outro termo de pesquisa.",
"select_network": "Selecionar rede",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "Você perderá aproximadamente {{priceImpact}} do valor do seu token nesta troca. Tente reduzir o valor ou escolher uma rota com mais liquidez.",
"proceed": "Prosseguir",
"cancel": "Cancelar",
+ "close": "Fechar",
"slippage_info_title": "Slippage",
"slippage_info_description": "A % de alteração no preço que você está disposto a permitir antes que sua transação seja cancelada.",
"blockaid_error_title": "Esta transação será estornada",
@@ -8107,7 +8221,14 @@
"retry": "Tentar novamente",
"on_linea": "na Linea",
"account_label": "Conta",
- "token_label": "Token"
+ "token_label": "Token",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "Cashback em mUSD",
@@ -8336,7 +8457,7 @@
"main_title": "Recompensas",
"vip": {
"bps_unit": "bps",
- "swaps_label": "Swaps",
+ "swaps_label": "Trocas",
"perps_label": "Perps",
"error_title": "We couldn’t load your VIP dashboard",
"error_description": "Check your connection and try again.",
@@ -8445,6 +8566,7 @@
"show_less": "Exibir menos",
"linking_progress": "Adicionando contas... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} inscrita(s)",
+ "accounts_added": "Accounts added",
"add_all_accounts": "Adicionar todas as contas",
"environment_selector": "Ambiente",
"environment_cancel": "Cancelar",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "Sair do programa de recompensas",
- "description": "Isso removerá suas contas do programa Recompensas e apagará seu progresso. Essa opção não poderá ser desfeita.",
- "confirm": "Cancelar participação",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "Tem certeza?",
- "confirmation_description": "Essa ação removerá todo o seu progresso e não poderá ser revertida. Se você voltar a participar do programa de recompensas mais tarde, começará do zero.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "Cancelar",
"confirm": "Confirmar",
+ "error_title": "Ocorreu algum erro",
"error_message": "Falha ao cancelar a participação no programa de recompensas. Tente novamente.",
"processing": "Em processamento..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "Ignorar",
@@ -8562,7 +8691,8 @@
"title_claim": "Resgatar benefício",
"action": "Resgatar",
"empty-list": "Você não tem nenhum benefício no momento.",
- "powered_by": "Com tecnologia da"
+ "powered_by": "Com tecnologia da",
+ "available_count": "{{count}} disponíveis"
},
"end_of_season_rewards": {
"confirm_label_default": "Confirmar",
@@ -8695,7 +8825,7 @@
"label_your_rank": "Sua classificação",
"label_portfolio": "Portfolio",
"label_net_inflow": "Fluxo de entrada líquido",
- "label_total_inflow": "Fluxo de entrada total",
+ "label_total_inflow": "Total de fluxo de entrada",
"label_outflow": "Fluxo de saída",
"label_days_held": "Dias mantidos",
"qualified_title": "Você se qualifica",
@@ -8880,9 +9010,11 @@
"musd_claim": "Reivindicar mUSD",
"perps_deposit": "Adicionar fundos",
"perps_withdraw": "Sacar",
+ "predict_withdraw": "Sacar {{sourceSymbol}} de {{sourceChain}}",
"predict_deposit": "Adicionar fundos",
"swap": "Trocar tokens",
- "swap_approval": "Aprovar tokens"
+ "swap_approval": "Aprovar tokens",
+ "fiat_purchase": "Comprar {{token}} com {{paymentMethod}}"
},
"perps_deposit_solution": "Agora você possui {{fiat}} de USDC na Arbitrum. Tente depositar novamente."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "Alta para baixa",
"low_to_high": "Baixa para alta",
"apply": "Aplicar",
- "search_placeholder": "Pesquisar tokens, sites, URLs",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "Cancelar",
"perps": "Perps",
"rwa_perps_section": "Perps",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "Perps",
"predictions": "Previsões",
"no_results": "Nenhum resultado encontrado",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "Populares",
"sites": "Sites",
"popular_sites": "Websites populares",
"search_sites": "Pesquisar websites",
"view_all": "Exibir tudo",
+ "view_more": "Ver mais",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "Ativar funcionalidade básica",
"basic_functionality_disabled_title": "A opção Explorar não está disponível",
"basic_functionality_disabled_description": "Não é possível obter os metadados necessários quando a funcionalidade básica está desativada.",
@@ -8995,6 +9131,14 @@
"crypto": "Cripto",
"sports": "Esportes",
"dapps": "Sites"
+ },
+ "search_tabs": {
+ "all": "Tudo",
+ "crypto": "Cryptos",
+ "perps": "Perps",
+ "stocks": "Ações",
+ "predictions": "Previsões",
+ "sites": "Sites"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "Não há mUSD nesta rede. Mude de rede para ver seus mUSD.",
"money_empty_state": {
"get_started": "Comece já",
+ "earn": "Ganhe",
"earn_apy": "Ganhe {{percentage}}% de APY"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "Ativos relacionados",
"perpetuals": "Perpétuos",
"predictions": "Previsões",
- "whats_happening": "O que está acontecendo",
- "whats_happening_ai": "IA",
- "whats_happening_impact": {
- "bullish": "Otimista",
- "bearish": "Pessimista",
- "neutral": "Neutro"
- },
"top_traders": "Principais traders",
- "whats_happening_categories": {
- "geopolitical": "Geopolítica",
- "macro": "Macro",
- "regulatory": "Regulatório",
- "technical": "Técnico",
- "social": "Redes sociais",
- "other": "Outro"
- },
"defi": "DeFi",
"nfts": "NFTs",
"trending_tokens": "Tendência",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "Populares"
+ },
+ "whats_happening": {
+ "title": "O que está acontecendo",
+ "ai": "IA",
+ "impact": {
+ "bullish": "Otimista",
+ "bearish": "Pessimista",
+ "neutral": "Neutro"
+ },
+ "categories": {
+ "geopolitical": "Geopolítica",
+ "macro": "Macro",
+ "regulatory": "Regulatório",
+ "technical": "Técnico",
+ "social": "Redes sociais",
+ "other": "Outro"
+ }
}
}
diff --git a/locales/languages/ru.json b/locales/languages/ru.json
index 4864e2f03fb3..4a63e4c26f97 100644
--- a/locales/languages/ru.json
+++ b/locales/languages/ru.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "Нет доступных средств. Используйте другой счет."
},
+ "headless_buy_error": {
+ "title": "Сбой покупки за фиатную фалюту",
+ "message": "Что-то пошло не так при покупке за фиатную валюту. Пожалуйста, попробуйте еще раз."
+ },
"mmpay_hardware_account": {
"title": "Кошелек не поддерживается",
"message": "Аппаратные кошельки не поддерживаются.\nЧтобы продолжить, смените кошелек."
@@ -133,8 +137,8 @@
"message": "Адрес получателя может не поддерживать прямые переводы токенов, что может привести к потере средств. Продолжайте только в том случае, если вы уверены, что этот контракт может получить ваш перевод."
},
"gas_sponsorship_reserve_balance": {
- "message": "Для этой транзакции недоступна спонсорская оплата газа. Вам необходимо постоянно иметь на счету не менее %{minBalance} %{nativeTokenSymbol}.",
- "title": "Спонсорская оплата газа недоступна"
+ "message": "Эта конкретная сеть требует поддержания резерва в размере %{minBalance} %{nativeTokenSymbol} на вашем счете.",
+ "title": "Требуется резервный баланс"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "Вы отправляете токены на адрес контракта токена. Это может привести к потере этих токенов.",
"smart_contract_address": "Адрес смарт-контракта",
"smart_contract_address_warning": "Адрес получателя может не поддерживать прямые переводы токенов, что может привести к потере средств. Продолжайте только в том случае, если вы уверены, что этот контракт может получить ваш перевод.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "Обновить",
"i_understand": "Я понимаю",
"cancel": "Отмена",
"new_address_title": "Новый адрес",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "Значение",
"pnl_percent": "П/У %",
- "recent": "Recent"
+ "recent": "Недавние"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "Торгуйте перами на {{symbol}}",
"subtitle": "Умножьте свои П/У на {{leverage}}"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "Связаться с поддержкой"
+ },
"today": "Сегодня",
"yesterday": "Вчера",
"unrealized_pnl": "Нереализованные П/У",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC переведены в ваш кошелек",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} переведены в ваш кошелек",
"toast_error_title": "Что-то пошло не так",
- "toast_error_description": "Не удалось продолжить вывод средств"
+ "toast_error_description": "Не удалось продолжить вывод средств",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Повторить попытку"
},
"quote": {
"network_fee": "Комиссия сети",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "Прогнозы MetaMask",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "Чемпионат мира",
+ "banner_title": "Чемпионат мира 2026 г.",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
- "props": "Props",
- "live": "Live"
+ "all": "Все",
+ "props": "Проп-ставки",
+ "live": "Идет сейчас"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
- "final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_stage": "Групповой этап",
+ "round_of_32": "1/16 финала",
+ "round_of_16": "1/8 финала",
+ "quarterfinals": "Четвертьфиналы",
+ "semifinals": "Полуфиналы",
+ "third_place": "Третье место",
+ "final": "Финал",
+ "group_a": "Группа A",
+ "group_b": "Группа B",
+ "group_c": "Группа C",
+ "group_d": "Группа D",
+ "group_e": "Группа E",
+ "group_f": "Группа F",
+ "group_g": "Группа G",
+ "group_h": "Группа H",
+ "group_i": "Группа I",
+ "group_j": "Группа J",
+ "group_k": "Группа K",
+ "group_l": "Группа L",
+ "third_place_match": "Матч за третье место"
}
},
"prediction_markets": "Рынки прогнозов",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "Вывод средств завершен",
"withdraw_completed_subtitle": "{{amount}} USDC переведены в ваш кошелек",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}} переведены в ваш кошелек",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "Вывод средств временно недоступен",
+ "unavailable_description": "Для получения срочной помощи обратитесь в службу поддержки.",
"unavailable_got_it": "Понятно",
"error_title": "Что-то пошло не так",
"error_description": "Не удалось продолжить вывод средств",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "Принять и закрыть",
"pna25_open_settings_button": "Открыть Настройки"
},
+ "onboarding_interest_questionnaire": {
+ "title": "Что вы хотите делать с помощью MetaMask?",
+ "description": "Выберите все подходящие варианты.",
+ "option_buy_and_sell_crypto": "Покупать и продавать криптовалюту",
+ "option_consolidate_wallets": "Объединить свои кошельки",
+ "option_advanced_trades": "Совершать продвинутые сделки",
+ "option_predict_sports_events": "Делать прогнозы на спорт и события",
+ "option_crypto_as_money": "Использовать криптовалюту как деньги",
+ "option_connect_apps_sites": "Подключаться к приложениям или сайтам",
+ "continue": "Продолжить"
+ },
"template_confirmation": {
"ok": "ОК",
"cancel": "Отмена"
@@ -3261,8 +3286,27 @@
"notifications_desc": "Управляйте своими уведомлениями",
"allow_notifications": "Разрешить уведомления",
"enable_push_notifications": "Включить push-уведомления",
- "allow_notifications_desc": "Будьте в курсе того, что происходит в вашем кошельке, с помощью уведомлений. Чтобы использовать уведомления, мы используем профиль для синхронизации некоторых настроек на ваших устройствах.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "Выкл.",
+ "select_all": "Выбрать все",
+ "deselect_all": "Отменить выбор всех",
+ "select_accounts_title": "Счета",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "Настройте свои уведомления",
"customize_session_desc": "Включите типы уведомлений, которые хотите получать:",
"account_session_title": "Активность счета",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snaps",
"snaps_desc": "Новые функции и обновления",
"products_announcements_title": "Объявления о продуктах",
- "products_announcements_desc": "Новые продукты и функции",
- "perps_title": "Торговля бессрочными фьючерсами"
+ "products_announcements_desc": "Новые продукты и функции"
},
"contacts_title": "Контакты",
"contacts_desc": "Добавляйте, изменяйте и удаляйте счета и управляйте ими.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "Карта",
"reset_onboarding_description": "Сбросьте состояние начальной регистрации карты, чтобы начать процесс регистрации с самого начала.",
- "reset_onboarding_button": "Сбросить состояние начальной регистрации"
+ "reset_onboarding_button": "Сбросить состояние начальной регистрации",
+ "unlink_money_account_description": "Отозвать разрешение на использование лимита трат USDC, которое позволяет карте расходовать средства с вашего денежного счета. Это отправляет транзакцию «одобрить (0)» и отмечает денежный счет как неделегированный в бэкенде карты.",
+ "unlink_money_account_button": "Отвязать денежный счет от карты",
+ "unlink_money_account_disabled_hint": "Нет активной привязки для удаления."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "Тактильный отклик",
@@ -3842,8 +3893,8 @@
"predict_button": "Прогнозы",
"add_collectible_button": "Добавить",
"info": "Информация",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "Пакетная продажа",
+ "batch_sell_new_label": "Новые",
"swap": "Обменять",
"convert": "Конвертировать",
"bridge": "Мост",
@@ -3883,7 +3934,7 @@
"troubleshoot": "Устранение неполадок",
"deposit_description": "Банковский перевод или перевод на карту с низкой комиссией",
"buy_description": "Подходит для покупки определенного токена",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "Продайте до 5 токенов за стейблкоин",
"sell_description": "Продать криптовалюту за наличные",
"swap_description": "Обменивайте одни токены на другие",
"bridge_description": "Передача токенов между сетями",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "Настройки > Уведомления.",
"cancel": "Отмена",
"cta": "Включить"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "Не упускайте ни одного движения",
+ "body": "Получайте оповещения в реальном времени, когда цены достигают ваших целей, сделки подтверждаются, а портфель меняется. Плюс обновления продукта и бонусы по пути. Мы будем делиться обновлениями на основе вашего взаимодействия с MetaMask.",
+ "button_yes": "Да",
+ "button_not_now": "Не сейчас",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "сейчас",
+ "title": "ETH вырос на 4,2% сегодня",
+ "message": "Сейчас на уровне 2668,51 $ — выше вашего ценового оповещения"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "1 ч назад",
+ "title": "Получено 0,25 ETH",
+ "message": "От 0x9a21…4f8c · $640.29"
+ }
+ },
+ "existing_user": {
+ "title": "Представляем персонализированные оповещения",
+ "body": "Получайте оповещения, которые соответствуют вашему стилю торговли. Обновляйте в любое время.",
+ "card_title": "Что вы получите",
+ "card_description": "Персонализированные оповещения и обновления, адаптированные к вашей торговой активности.",
+ "button_confirm": "Подтвердить",
+ "button_not_now": "Не сейчас"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "Оплатить с помощью",
"buying_via": "Покупка через {{providerName}}.",
"change_provider": "Изменить поставщика.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "Возникла какая-то проблема. Повторите попытку.",
"no_payment_methods_available": "Нет доступных способов оплаты.",
"error_fetching_quotes": "Возникла какая-то проблема. Повторите попытку.",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "Конвертировать в mUSD",
"get_a_percentage_musd_bonus": "Получите бонус в размере {{percentage}}% в mUSD",
"convert": "Конвертировать",
+ "confirm": "Подтвердить",
+ "convert_tooltip_description": "Конвертируйте свои стейблкоины в mUSD и получайте до {{percentage}}% годового бонуса, который можно запрашивать ежедневно. При поддержке Relay.",
"fetching_quote": "Получение котировки...",
"you_convert": "Вы конвертируете",
"network_fee": "Комиссия сети",
@@ -6597,7 +6679,7 @@
"step_progress": "Шаг {{current}} из {{total}}",
"title": "Пополнить",
"description": "Пополните свой счет и начните зарабатывать годовые.",
- "add": "Добавить",
+ "add": "Внести средства",
"step2_title": "Получите карту MetaMask Card",
"step2_description": "Тратьте свой баланс Money, пока он приносит доход, везде, где принимают Mastercard.",
"step2_cta": "Получить карту",
@@ -6605,6 +6687,19 @@
"link_card_description": "Тратьте свой баланс, пока он приносит доход, везде, где принимают Mastercard.",
"link_card_cta": "Привязать карту"
},
+ "rive_onboarding": {
+ "step1_title": "Денежные счета уже здесь",
+ "step1_body": "Получайте до {{percentage}}% годовых на свой баланс, доступный по всему вашему кошельку.",
+ "step1_footer_text": "Годовая процентная доходность является переменной величиной и может измениться в любое время.",
+ "step2_title": "Зарабатывайте автоматически",
+ "step2_body": "Переводите стейблкоины без комиссий за обмен. Средства начинают приносить доход сразу же.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "Тратьте где угодно",
+ "step3_body": "Получайте до {{percentage}}% кэшбэка за покупки, привязав свой денежный счет к карте MetaMask.",
+ "step4_title": "Торгуйте и зарабатывайте в одном месте",
+ "step4_body": "Используйте свой денежный баланс для торговли в MetaMask, продолжая получать доходность.",
+ "continue": "Продолжить"
+ },
"action": {
"add": "Добавить",
"transfer": "Перевести",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "Как это работает",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "Внесите mUSD на свой денежный счет и получайте до",
+ "description_suffix": ". Ваш баланс обеспечен долларами и готов к тратам, торговле или отправке в любое время."
},
"musd_row": {
"add": "Добавить"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Баланс Money",
"add": "Добавить",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Баланс Money",
+ "info_sheet_body": "Ваш обеспеченный долларами баланс mUSD, который всегда доступен для трат, отправки или торговли. Мы не учитываем его в общем балансе вашего счета. Вывод средств обрабатывается немедленно, в зависимости от стандартного времени подтверждения сети в соответствующем блокчейне. В случае нехватки ликвидности возможны временные задержки."
},
"potential_earnings": {
"title": "Зарабатывайте на своей криптовалюте",
"description": "Посмотрите, как сумма ваших средств может вырасти со временем, если конвертировать вашу криптовалюту в mUSD.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "Конвертируйте свои активы на сумму {{total}}, и вы сможете заработать до",
+ "description_with_amounts_suffix": "за один год.",
"convert": "Конвертировать",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "Конвертировать криптовалюту",
"no_fee": "Без комиссии MetaMask",
"view_all": "Смотреть все",
"view_potential_earnings": "Посмотреть потенциальный доход"
@@ -6649,41 +6744,44 @@
"cashback": "{{percentage}}% кэшбэка в mUSD",
"get_now": "Получить сейчас",
"link_title": "Привязать карту MetaMask",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "Тратьте свой денежный баланс и зарабатывайте на покупках. Плюс до {{apy}}% APY на ваш баланс.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "Получайте кэшбэк {{percentage}}% в mUSD",
+ "link_bullet_apy": "Получайте до {{apy}}% годовых",
"link_card": "Привязать карту",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "Тратьте и зарабатывайте",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "Привязать карту",
+ "manage_card": "Управление",
+ "avail_balance": "Доступ. баланс"
},
"what_you_get": {
"title": "Что вы получаете",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "Автоматически зарабатывайте до",
"benefit_dollar_backed": "Храните свои деньги в безопасности в mUSD, стейблкоине, обеспеченном долларом 1:1",
"benefit_liquidity": "Получите полную ликвидность без блокировок, чтобы вы могли торговать или выводить средства в любое время",
"benefit_spend_prefix": "Тратьте средства у более чем 150 млн продавцов с картой MetaMask и зарабатывайте ",
"benefit_spend_cashback": "1-3% кэшбэка в mUSD",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "Переводите на любые ваши кошельки в MetaMask",
+ "benefit_global": "Отправляйте и получайте средства по всему миру",
"learn_more": "Подробнее"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "Внести средства"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "Внести средства",
"convert_crypto": "Конвертировать крипту",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "С любого счета",
"deposit_funds": "Внести средства",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "С дебетовой карты или банковского счета",
+ "move_musd": "Перевести ваши {{amount}} mUSD",
+ "move_musd_no_amount": "Перевести ваши mUSD",
+ "move_musd_description": "С вашего баланса",
"receive_external": "Получить с внешнего кошелька",
"coming_soon": "Скоро появятся"
},
@@ -6694,7 +6792,7 @@
"contact_support": "Связаться с поддержкой"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "Перевести средства",
"between_accounts": "Между счетами",
"perps_account": "Счет перпов",
"predictions_account": "Счет прогнозов",
@@ -6712,8 +6810,8 @@
"body": "Оценка того, сколько вы могли бы заработать за период на основе вашего текущего баланса и сегодняшней APY. Оценки не гарантируют доходность и могут меняться."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "Зарабатывайте на своей криптовалюте",
+ "body": "В примере предполагается, что годовая процентная доходность или годовоые (APY) в размере {{percentage}}% останется неизменной в течение одного года. Годовая поцентная доходность является переменной величиной и может меняться под влиянием различных факторов. Гарантии возврата нет."
},
"activity": {
"title": "Деятельность",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "Как это работает",
- "how_it_works_subtitle": "Узнайте, как ваши деньги работают на вас",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "Узнать больше о mUSD",
"what_you_get_title": "Что вы получаете",
@@ -6738,7 +6836,8 @@
"sent": "Отправлено",
"transferred": "Переведено",
"card_transaction": "Транзакция по карте",
- "converted": "Конвертировано"
+ "converted": "Конвертировано",
+ "failed": "Ошибка"
},
"convert_stablecoins": {
"title": "Конвертируйте свои стейблкоины",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "Финансы",
"section_title": "Как это работает",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "Внесите mUSD на свой денежный счет и автоматически получайте до {{percentage}}% годовых (переменная ставка). Средства поступают в DeFi vault, который генерирует доход на проверенных рынках кредитования — без стейкинга, без клейминга, без блокировок.",
"description_2": "Ваш Денежный баланс — это ваш баланс для расходов. Привяжите свою карту MetaMask, чтобы расплачиваться в более чем 150 млн магазинов по всему миру. Ваши деньги продолжают приносить доход до того момента, пока вы их не потратите.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "Часто задаваемые вопросы",
"faq_placeholder_answer": "Скоро появятся.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "Как работает ставка {{percentage}}% годовых?",
"faq_q2": "Что такое mUSD?",
"faq_q3": "Откуда берется доходность?",
"faq_q4": "Заблокированы ли мои деньги? Могу ли я вывести их в любое время?",
@@ -7030,11 +7129,12 @@
"confirm": "Подтвердить",
"pay_with_bottom_sheet": {
"title": "Оплатить с помощью",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "Использовалось последним",
+ "bank_and_card": "Банковский счет и карта",
+ "crypto": "Крипто",
+ "available_balance": "Доступно {{balance}}",
+ "other_assets": "Другие активы",
+ "other_assets_description": "Выберите из ваших токенов"
},
"staking_footer": {
"part1": "Продолжая, вы соглашаетесь с нашими ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "В стоимость конвертации mUSD входят расходы сети, а также могут входить комиссии поставщика. При обмене на mUSD комиссия MetaMask не взимается."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMask выполнит своп ваших mUSD на нужный токен. Провайдеры свопов могут взимать комиссию, но MetaMask этого не делает."
},
"title": {
"transaction_fee": "Комиссии"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "Внести средства",
"deposit_edit_amount_predict_withdraw": "Вывести средства",
"deposit_edit_amount_musd_conversion": "Конвертировать в mUSD",
- "preparing_order": "Preparing order"
+ "preparing_order": "Подготовка ордера"
},
"change_in_simulation_modal": {
"title": "Результаты изменились",
@@ -7250,20 +7350,33 @@
"confirm_swap": "Обменять",
"terms_and_conditions": "Положения и условия",
"select_token": "Выберите токен",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "Выберите до 5 токенов",
+ "batch_sell_select_subtitle": "Все токены должны быть в одной сети.",
+ "batch_sell_empty_state_title": "Нет токенов. Нет проблем.",
+ "batch_sell_empty_state_description": "Нет токенов. Нет проблем. Ищите и покупайте токены для пакетной продажи.",
+ "batch_sell_continue_with_one_token": "Продолжить с (1) токеном",
+ "batch_sell_continue_with_tokens": "Продолжить с ({{tokenCount}}) токенами",
+ "batch_sell_max_tokens_allowed": "Разрешено не более 5 токенов",
+ "batch_sell_single_token_dialog_title": "Предупреждение о высоком курсе",
+ "batch_sell_single_token_dialog_description": "Пакетная продажа одного токена может привести к более высокому курсу. Хотите вместо этого выполнить своп?",
+ "batch_sell_swap_instead": "Да, выполнить своп",
+ "batch_sell_review_title": "Обзор пакетной продажи",
+ "batch_sell_select_stablecoin": "Выберите стейблкоин",
+ "batch_sell_total_received": "Всего получено",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "Проверить",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "Настроить {{tokenSymbol}}",
+ "batch_sell_remove_token": "Удалить {{tokenSymbol}}",
+ "batch_sell_checkbox_label": "Пакетная продажа токена",
+ "sort_balance": "Баланс",
+ "next": "Далее",
+ "explore_tokens": "Искать токены",
"no_tokens_found": "Токены не найдены",
"no_tokens_found_description": "Не удалось найти токены с этим именем. Попробуйте другой поисковый запрос.",
"select_network": "Выбрать сеть",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "В результате этого обмена вы потеряете примерно {{priceImpact}} от стоимости вашего токена. Попробуйте уменьшить сумму или выбрать более ликвидный вариант.",
"proceed": "Продолжить",
"cancel": "Отмена",
+ "close": "Закрыть",
"slippage_info_title": "Проскальзывание",
"slippage_info_description": "Процент изменения цены, который вы готовы допустить, прежде чем транзакция будет отменена.",
"blockaid_error_title": "Эта транзакция будет отменена",
@@ -8107,7 +8221,14 @@
"retry": "Повторить попытку",
"on_linea": "на Linea",
"account_label": "Счет",
- "token_label": "Токен"
+ "token_label": "Токен",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "Кэшбэк mUSD",
@@ -8445,6 +8566,7 @@
"show_less": "Показать меньше",
"linking_progress": "Добавляются счета... ({{current}} из {{total}})",
"accounts_linked_count": "{{linked}}/{{total}} зарегистрировано",
+ "accounts_added": "Accounts added",
"add_all_accounts": "Добавить все счета",
"environment_selector": "Среда",
"environment_cancel": "Отмена",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "Отказаться от участия в программе вознаграждений",
- "description": "Это приведет к удалению ваших счетов из программы вознаграждений, а также к потере прогресса. Отменить это действие будет невозможно.",
- "confirm": "Отказаться",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "Вы уверены?",
- "confirmation_description": "Это удалит весь ваш прогресс, и его нельзя будет восстановить. Если вы снова присоединитесь к Программе вознаграждений позже, вы начнете с нуля.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "Отмена",
"confirm": "Подтвердить",
+ "error_title": "Что-то пошло не так",
"error_message": "Не удалось отказаться от участия в Программе вознаграждений. Попробуйте еще раз.",
"processing": "Обработка..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "Отклонить",
@@ -8562,7 +8691,8 @@
"title_claim": "Получить бонус",
"action": "Получить",
"empty-list": "У вас пока нет никаких бонусов.",
- "powered_by": "На основе"
+ "powered_by": "На основе",
+ "available_count": "Доступно: {{count}}"
},
"end_of_season_rewards": {
"confirm_label_default": "Подтвердить",
@@ -8880,9 +9010,11 @@
"musd_claim": "Получить mUSD",
"perps_deposit": "Внести средства",
"perps_withdraw": "Вывод средств",
+ "predict_withdraw": "Вывести {{sourceSymbol}} из {{sourceChain}}",
"predict_deposit": "Внести средства",
"swap": "Обменять токены",
- "swap_approval": "Одобрить токены"
+ "swap_approval": "Одобрить токены",
+ "fiat_purchase": "Купить {{token}} с помощью {{paymentMethod}}"
},
"perps_deposit_solution": "Теперь у вас есть {{fiat}} USDC на Arbitrum. Попробуйте снова внести депозит."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "От высокого к низкому",
"low_to_high": "От низкого к высокому",
"apply": "Применить",
- "search_placeholder": "Поиск токенов, сайтов и URL-адресов",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "Отмена",
"perps": "Перпы",
"rwa_perps_section": "Перпы",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "Перпы",
"predictions": "Прогнозы",
"no_results": "Результаты не найдены",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "Популярные",
"sites": "Сайты",
"popular_sites": "Популярные сайты",
"search_sites": "Поиск по сайтам",
"view_all": "Смотреть все",
+ "view_more": "Показать еще",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "Включить базовый функционал",
"basic_functionality_disabled_title": "Обзор недоступен",
"basic_functionality_disabled_description": "Мы не можем получить необходимые метаданные, когда базовый функционал отключен.",
@@ -8995,6 +9131,14 @@
"crypto": "Крипто",
"sports": "Спорт",
"dapps": "Сайты"
+ },
+ "search_tabs": {
+ "all": "Все",
+ "crypto": "Cryptos",
+ "perps": "Перпы",
+ "stocks": "Акции",
+ "predictions": "Прогнозы",
+ "sites": "Сайты"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "В этой сети нет mUSD. Переключитесь на другую сеть, чтобы увидеть свои mUSD.",
"money_empty_state": {
"get_started": "С чего начать",
+ "earn": "Заработать",
"earn_apy": "Зарабатывайте {{percentage}}% годовых"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "Связанные активы",
"perpetuals": "Бессрочные контракты",
"predictions": "Прогнозы",
- "whats_happening": "Что происходит",
- "whats_happening_ai": "ИИ",
- "whats_happening_impact": {
- "bullish": "Бычий",
- "bearish": "Медвежий",
- "neutral": "Нейтральный"
- },
"top_traders": "Лучшие трейдеры",
- "whats_happening_categories": {
- "geopolitical": "Геополитика",
- "macro": "Макро",
- "regulatory": "Регулирование",
- "technical": "Техника",
- "social": "Социальные вопросы",
- "other": "Другое"
- },
"defi": "DeFi",
"nfts": "NFT",
"trending_tokens": "Популярное",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "Популярные"
+ },
+ "whats_happening": {
+ "title": "Что происходит",
+ "ai": "ИИ",
+ "impact": {
+ "bullish": "Бычий",
+ "bearish": "Медвежий",
+ "neutral": "Нейтральный"
+ },
+ "categories": {
+ "geopolitical": "Геополитика",
+ "macro": "Макро",
+ "regulatory": "Регулирование",
+ "technical": "Техника",
+ "social": "Социальные вопросы",
+ "other": "Другое"
+ }
}
}
diff --git a/locales/languages/tl.json b/locales/languages/tl.json
index ec7bde47ad91..57d9ce61793f 100644
--- a/locales/languages/tl.json
+++ b/locales/languages/tl.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "Walang available na mga pondo. Gumamit ng ibang account."
},
+ "headless_buy_error": {
+ "title": "Nabigo ang pagbili ng fiat",
+ "message": "Nagkaproblema sa pagbili mo ng fiat. Pakisubukan muli."
+ },
"mmpay_hardware_account": {
"title": "Hindi sinusuportahan ang wallet",
"message": "Hindi pa sinusuportahan ang mga wallet na hardware.\nMagpalit ng wallet para magpatuloy."
@@ -133,8 +137,8 @@
"message": "Maaaring hindi sinusuportahan ng address ng tatanggap ang direktang mga paglilipat ng token, na maaaring magresulta sa pagkalugi ng pondo. Magpatuloy lamang kung tiyak ka na matatanggap ng kontratang ito ang paglilipat mo."
},
"gas_sponsorship_reserve_balance": {
- "message": "Hindi available ang pag-iisponsor ng gas para sa transaksyong ito. Kakailanganin mo ng hindi bababa sa %{minBalance} %{nativeTokenSymbol} sa account mo.",
- "title": "Hindi available ang pag-iisponsor ng gas"
+ "message": "Kailangan ng partikular na network na ito na magpanatili ng reserba ng %{minBalance} %{nativeTokenSymbol} sa iyong account.",
+ "title": "Kinakailangan ng reserbang balanse"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "Nagpapadala ka ng mga token sa address ng kontrata ng token. Maaari itong magresulta sa pagkawala ng mga token na ito.",
"smart_contract_address": "Address ng smart na kontrata",
"smart_contract_address_warning": "Maaaring hindi sinusuportahan ng address ng tatanggap ang direktang mga paglilipat ng token, na maaaring magresulta sa pagkalugi ng pondo. Magpatuloy lamang kung tiyak ka na matatanggap ng kontratang ito ang paglilipat mo.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "I-update",
"i_understand": "Nauunawaan ko",
"cancel": "Kanselahin",
"new_address_title": "Bagong address",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "Value",
"pnl_percent": "P&L %",
- "recent": "Recent"
+ "recent": "Kamakailan"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "Mag-trade ng {{symbol}} perp",
"subtitle": "I-multiply ang iyong P&L na hanggang {{leverage}}"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "Kontakin ang suporta"
+ },
"today": "Ngayong araw",
"yesterday": "Kahapon",
"unrealized_pnl": "Unrealized P&L",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "Nailipat ang {{amount}} USDC sa iyong wallet",
"toast_completed_any_token_subtitle": "Nailipat ang {{amount}}{{token}} sa iyong wallet",
"toast_error_title": "Mayroong nang mali",
- "toast_error_description": "Nabigong magpatuloy sa pag-withdraw"
+ "toast_error_description": "Nabigong magpatuloy sa pag-withdraw",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Subukang muli"
},
"quote": {
"network_fee": "Bayad sa network",
@@ -2229,33 +2243,33 @@
"world_cup": {
"title": "World Cup",
"banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
+ "all": "Lahat",
"props": "Props",
"live": "Live"
},
"stages": {
"group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
+ "round_of_32": "Round na 32",
+ "round_of_16": "Round na 16",
"quarterfinals": "Quarterfinals",
"semifinals": "Semifinals",
- "third_place": "Third Place",
+ "third_place": "Ikatlong Pwesto",
"final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_a": "Grupo A",
+ "group_b": "Grupo B",
+ "group_c": "Grupo C",
+ "group_d": "Grupo D",
+ "group_e": "Grupo E",
+ "group_f": "Grupo F",
+ "group_g": "Grupo G",
+ "group_h": "Grupo H",
+ "group_i": "Grupo I",
+ "group_j": "Grupo J",
+ "group_k": "Grupo K",
+ "group_l": "Grupo L",
+ "third_place_match": "Laban sa Ikatlong Pwesto"
}
},
"prediction_markets": "Mga market ng prediksyon",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "Tapos ng mag-withdraw",
"withdraw_completed_subtitle": "Nailipat ang {{amount}} USDC sa iyong wallet",
"withdraw_any_token_completed_subtitle": "Nailipat ang {{amount}}{{token}} sa iyong wallet",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "Pansamantalang hindi available ang mga withdrawal",
+ "unavailable_description": "Para sa agarang tulong, kontakina ng Customer Service.",
"unavailable_got_it": "Nakuha ko",
"error_title": "Mayroong nang mali",
"error_description": "Nabigong magpatuloy sa pag-withdraw",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "Tanggapin at isara",
"pna25_open_settings_button": "Buksan ang Mga Setting"
},
+ "onboarding_interest_questionnaire": {
+ "title": "Ano ang gusto mong gawin sa MetaMask?",
+ "description": "Piliin ang lahat ng naaangkop.",
+ "option_buy_and_sell_crypto": "Bumili at magbenta ng crypto",
+ "option_consolidate_wallets": "Pagsama-samahin ang iyong mga wallet",
+ "option_advanced_trades": "Gumawa ng mga advanced na trade",
+ "option_predict_sports_events": "Hulaan ang mga sport at event",
+ "option_crypto_as_money": "Gamitin ang crypto bilang pera",
+ "option_connect_apps_sites": "Kumonekta sa mga app o site",
+ "continue": "Magpatuloy"
+ },
"template_confirmation": {
"ok": "OK",
"cancel": "Kanselahin"
@@ -3261,8 +3286,27 @@
"notifications_desc": "Pamahalaan ang mga notipikasyon mo",
"allow_notifications": "Payagan ang mga notipikasyon",
"enable_push_notifications": "Paganahin ang mga push na notipikasyon",
- "allow_notifications_desc": "Manatiling may-alam sa nangyayari sa iyong wallet gamit ang mga notification. Para gamitin ang mga notification, gumagamit kami ng profile para i-sync ang ilang mga setting sa lahat ng iyong mga device.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "I-off",
+ "select_all": "Piliin lahat",
+ "deselect_all": "Alisin ang pagkakapili sa lahat",
+ "select_accounts_title": "Mga Account",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "I-customize ang mga notipikasyon mo",
"customize_session_desc": "I-on ang mga uri ng notipikasyon na nais mong matanggap:",
"account_session_title": "Aktibidad ng account",
@@ -3276,8 +3320,7 @@
"snaps_title": "Mga Snap",
"snaps_desc": "Mga bagong tampok at update",
"products_announcements_title": "Mga anunsiyo ng produkto",
- "products_announcements_desc": "Mga bagong produkto at tampok",
- "perps_title": "Perps trading"
+ "products_announcements_desc": "Mga bagong produkto at tampok"
},
"contacts_title": "Mga Kontak",
"contacts_desc": "Magdagdag, mag-edit, mag-alis, at mamahala ng iyong mga account.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "Card",
"reset_onboarding_description": "I-reset ang estado ng Card onboarding para simulan ang daloy ng onboarding mula sa simula.",
- "reset_onboarding_button": "I-reset ang Estado ng Onboarding"
+ "reset_onboarding_button": "I-reset ang Estado ng Onboarding",
+ "unlink_money_account_description": "Bawiin ang allowance ng limitasyon sa paggastos ng USDC na nagpapahintulot sa Card na gumastos mula sa Money Account mo. Isinusumite nito ang aprubadong(0) transaksyon at minamarkahan ang Money Account bilang hindi na-delegate sa backend ng Card.",
+ "unlink_money_account_button": "I-unlink ang Money Account mula sa Card",
+ "unlink_money_account_disabled_hint": "Walang aalisin na aktibong pagka-link."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "Haptics",
@@ -3842,8 +3893,8 @@
"predict_button": "Mga hula",
"add_collectible_button": "Idagdag",
"info": "Impormasyon",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "Magbenta nang Naka-batch",
+ "batch_sell_new_label": "Bago",
"swap": "Mag-swap",
"convert": "I-convert",
"bridge": "Mag-bridge",
@@ -3883,7 +3934,7 @@
"troubleshoot": "I-troubleshoot",
"deposit_description": "Mababang bayad para sa bank o card transfer",
"buy_description": "Mainam para sa pagbili ng partikular na token",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "Magbenta ng hanggang 5 token para sa stablecoin",
"sell_description": "Magbenta ng crypto para sa pera",
"swap_description": "Palitan sa pagitan ng mga token",
"bridge_description": "Maglipat ng mga token sa pagitan ng mga network",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "Mga Setting > Mga notipikasyon.",
"cancel": "Kanselahin",
"cta": "I-on"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "Walang dapat palampasin",
+ "body": "Kunin ang mga real-time na alerto kapag umabot ang mga presyo sa iyong mga target, mga trade na kinumpirma mo, at ang mga galaw ng iyong portfolio. Idagdag pa ang mga update sa produkto at mga reward na madadaanan. Ibabahagi namin ang mga update batay sa iyong mga interaksyon sa MetaMask.",
+ "button_yes": "Oo",
+ "button_not_now": "Hindi sa ngayon",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "ngayon",
+ "title": "Hanggang 4.2% ang ETH ngayon",
+ "message": "Ngayon ay nasa $2,668.51 — mas mataas sa iyong alerto sa presyo"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "1 oras ang nakalipas",
+ "title": "Natanggap ang 0.25 ETH",
+ "message": "Mula sa 0x9a21…4f8c · $640.29"
+ }
+ },
+ "existing_user": {
+ "title": "Ipinapakilala ang mga isinapersonal na alerto",
+ "body": "Makakuha ng mga notipikasyon na tumutugma sa kung paano ka nagti-trade. I-update anumang oras.",
+ "card_title": "Ano ang makukuha mo",
+ "card_description": "Mga isinapersonal na alerto at mga update na sinadya para sa iyong aktibidad sa pagti-trade.",
+ "button_confirm": "Kumpirmahin",
+ "button_not_now": "Hindi sa ngayon"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "Magbayad gamit ang",
"buying_via": "Bumibili sa pamamagitan ng {{providerName}}.",
"change_provider": "Magpalit ng provider.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "Nagkaproblema. Subukang muli.",
"no_payment_methods_available": "Walang available na mga paraan ng pagbabayad.",
"error_fetching_quotes": "Nagkaproblema. Subukang muli.",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "I-convert sa mUSD",
"get_a_percentage_musd_bonus": "Makakuha ng {{percentage}}% mUSD bonus",
"convert": "I-convert",
+ "confirm": "Kumpirmahin",
+ "convert_tooltip_description": "I-convert ang mga stablecoin mo sa mUSD at kumita ng hanggang {{percentage}}% taunang bonus na puwede mong ma-claim araw-araw. Pinapagana ng Relay.",
"fetching_quote": "Kumukuha ng quote...",
"you_convert": "Nagko-convert ka ng",
"network_fee": "Bayad sa network",
@@ -6597,7 +6679,7 @@
"step_progress": "Hakbang {{current}} ng {{total}}",
"title": "Magdagdag ng pera",
"description": "Pondohan ang account mo at simulang kumita ng APY.",
- "add": "Idagdag",
+ "add": "Magdagdag ng pondo",
"step2_title": "Kunin ang MetaMask Card mo",
"step2_description": "Gastusin ang balanse mo sa Money habang kumikita ito, saanman tinatanggap ang Mastercard.",
"step2_cta": "Kunin ang card",
@@ -6605,6 +6687,19 @@
"link_card_description": "Gastusin ang balanse mo habang kumikita ito, saanman tinatanggap ang Mastercard.",
"link_card_cta": "I-link ang card"
},
+ "rive_onboarding": {
+ "step1_title": "Narito ang mga money account",
+ "step1_body": "Kumita ng hanggang {{percentage}}% APY sa iyong balanse, available sa iyong buong wallet.",
+ "step1_footer_text": "Nag-iiba-iba ang APY at maaari itong magbago anumang oras.",
+ "step2_title": "Kumita nang awtomatiko",
+ "step2_body": "Ilipat ang mga stablecoin nang walang mga babayaran sa palitan. Nagsisimulang kumita agad ang mga pondo.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "Gastusin kahit saan",
+ "step3_body": "Makakuha ng hanggang {{percentage}}% pabalik sa mga pagbili sa pamamagitan ng pag-link sa Money account mo papunta sa MetaMask card.",
+ "step4_title": "Mag-trade at kumita sa iisang lugar",
+ "step4_body": "Gamitin ang balanse ng iyong Pera para mag-trade sa buong MetaMask habang kumikita pa rin ng yield.",
+ "continue": "Magpatuloy"
+ },
"action": {
"add": "Idagdag",
"transfer": "Maglipat",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "Paano ito gumagana",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "Magdeposito ng mUSD sa Money account mo at kumita ng hanggang",
+ "description_suffix": ". Ang iyong balanse ay sinusuportahan ng dolyar at handa na para gastusin, i-trade, o ipadala anumang oras."
},
"musd_row": {
"add": "Idagdag"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Balanse sa Money",
"add": "Idagdag",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Balanse sa Money",
+ "info_sheet_body": "Ang balanse ng mUSD mo na sinusuportahan ng dolyar na laging available para gastusin, ipadala, o i-trade anumang oras. Hindi namin ito kinakalkula sa buong balanse ng account mo."
},
"potential_earnings": {
"title": "Kumita sa crypto mo",
"description": "Tingnan kung paano maaaring lumago ang pera mo sa paglipas ng panahon sa pamamagitan ng pag-convert ng iyong crypto sa mUSD.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "I-convert ang iyong {{total}} sa mga asset at maaari kang kumita ng hanggang",
+ "description_with_amounts_suffix": "sa isang taon.",
"convert": "I-convert",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "I-convert ang iyong crypto",
"no_fee": "Walang bayad sa MetaMask",
"view_all": "Tingnan lahat",
"view_potential_earnings": "Tingnan ang potensyal na kikitain"
@@ -6649,41 +6744,44 @@
"cashback": "Ibabalik na {{percentage}}% mUSD",
"get_now": "Kunin ngayon",
"link_title": "I-link ang MetaMask Card",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "Gastusin ang balanse ng Pera mo at kumita sa mga pagbili. At karagdagan, na hanggang {{apy}}% APY sa iyong balanse.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "Makakuha ng {{percentage}}% mUSD pabalik",
+ "link_bullet_apy": "Kumita ng hanggang {{apy}}% APY",
"link_card": "I-link ang card",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "Gumastos at kumita",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "I-link ang card",
+ "manage_card": "Pamahalaan",
+ "avail_balance": "Avail. na balanse"
},
"what_you_get": {
"title": "Ano ang makukuha mo",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "Awtomatikong kumita ng hanggang",
"benefit_dollar_backed": "Itabi nang ligtas ang pera mo sa mUSD, isang stablecoin na sinusuportahan ng dolyar nang 1:1",
"benefit_liquidity": "Makakuha ng ganap na liquidity nang walang lockup, kaya maaari kang mag-trade o mag-withdraw anumang oras",
"benefit_spend_prefix": "Gastusin sa 150M+ merchant gamit ang MetaMask Card at kumita ",
"benefit_spend_cashback": "Ibabalik na 1-3% mUSD",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "Maglipat sa alinman sa iyong mga wallet sa buong MetaMask",
+ "benefit_global": "Magpadala at makatanggap ng mga pondo saanman sa mundo",
"learn_more": "Matuto pa"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "Magdagdag ng pondo"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "Magdagdag ng pondo",
"convert_crypto": "Mag-convert ng crypto",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "Mula sa anumang account",
"deposit_funds": "Magdeposito ng mga pondo",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "Mula sa debit card o bangko",
+ "move_musd": "Ilipat ang iyong {{amount}} mUSD",
+ "move_musd_no_amount": "Ilipat ang iyong mUSD",
+ "move_musd_description": "Mula sa iyong balanse",
"receive_external": "Tumanggap mula sa panlabas na wallet",
"coming_soon": "Paparating na"
},
@@ -6694,7 +6792,7 @@
"contact_support": "Kontakin ang suporta"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "Ilipat ang mga pondo",
"between_accounts": "Sa pagitan ng mga account",
"perps_account": "Perps account",
"predictions_account": "Account sa Predictions",
@@ -6712,8 +6810,8 @@
"body": "Pagtataya ng puwede mong kitain sa loob ng isang yugto ng panahon batay sa kasalukuyan mong balanse at APY ngayong araw. Hindi garantisado ang mga return at posible pa ring magbago ang mga ito."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "Kumita sa crypto mo",
+ "body": "Ipinagpapalagay sa ilustrasyon na ang {{percentage}}% Annual Percentage Yield (APY) ay nananatiling hindi nagbabago sa loob ng isang taon. Nag-iiba-iba ang APY at posible itong magbago dahil sa iba't ibang salik. Hindi iginagarantiya ang return."
},
"activity": {
"title": "Aktibidad",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "Paano ito gumagana",
- "how_it_works_subtitle": "Alamin kung paano mo mapapakinabangan ang pera mo",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "Matuto pa tungkol sa mUSD",
"what_you_get_title": "Ano ang makukuha mo",
@@ -6738,7 +6836,8 @@
"sent": "Nagpadala",
"transferred": "Nailipat",
"card_transaction": "Transaksyon sa card",
- "converted": "Na-convert"
+ "converted": "Na-convert",
+ "failed": "Nabigo"
},
"convert_stablecoins": {
"title": "I-convert ang iyong mga stablecoin",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "Pera",
"section_title": "Paano ito gumagana",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "Magdeposito ng mUSD sa Money account mo at kumita ng hanggang {{percentage}}% APY (variable) nang awtomatiko. Napupunta ang mga pondo sa DeFi vault na kumikita ng mga return sa lahat ng na-audit na lending market—nang walang pag-stake, pag-claim, mga lock-up.",
"description_2": "Ang balanse mo sa Money ang balanseng puwede mong gastusin. I-link ang MetaMask Card mo para magamit ito sa 150M+ merchant sa buong mundo. Patuloy na kikita ang pera mo hanggang sa gastusin mo ito.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "Mga madalas itanong",
"faq_placeholder_answer": "Paparating na.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "Paano gumagana ang {{percentage}}% APY?",
"faq_q2": "Ano ang mUSD?",
"faq_q3": "Saan nanggagaling ang yield?",
"faq_q4": "Naka-lock ba ang pera ko? Puwede ba akong mag-withdraw anumang oras?",
@@ -7030,11 +7129,12 @@
"confirm": "Kumpirmahin",
"pay_with_bottom_sheet": {
"title": "Magbayad gamit ang",
- "last_used": "Last used",
+ "last_used": "Huling ginamit",
+ "bank_and_card": "Bangko at card",
"crypto": "Crypto",
"available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "other_assets": "Iba ang mga asset",
+ "other_assets_description": "Pumili mula sa mga token mo"
},
"staking_footer": {
"part1": "Sa pagpapatuloy, sumasang-ayon ka sa aming ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "Kasama sa mga bayarin sa pag-convert ng mUSD ang mga bayad sa network at posible ring kasama ang mga bayarin sa provider. Walang hihinging bayad sa MetaMask kapag nag-convert ka sa mUSD."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "Isu-swap ng MetaMask ang iyong mUSD sa token na gusto mo. Maaaring maningil ang mga provider sa pag-swap, ngunit hindi ang MetaMask."
},
"title": {
"transaction_fee": "Mga Bayarin"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "Magdagdag ng pondo",
"deposit_edit_amount_predict_withdraw": "Mag-withdraw",
"deposit_edit_amount_musd_conversion": "I-convert sa mUSD",
- "preparing_order": "Preparing order"
+ "preparing_order": "Inihahanda ang order"
},
"change_in_simulation_modal": {
"title": "Nagbago ang mga resulta",
@@ -7250,20 +7350,33 @@
"confirm_swap": "Mag-swap",
"terms_and_conditions": "Mga Tuntunin at Kundisyon",
"select_token": "Pumili ng token",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "Pumili ng hanggang 5 token",
+ "batch_sell_select_subtitle": "Ang lahat ng token ay kailangang nasa parehong network.",
+ "batch_sell_empty_state_title": "Walang mga token. Walang problema.",
+ "batch_sell_empty_state_description": "Walang mga token. Walang problema. Tuklasin at bumili ng mga token para magbenta nang naka-batch.",
+ "batch_sell_continue_with_one_token": "Magpatuloy gamit ang (1) token",
+ "batch_sell_continue_with_tokens": "Magpatuloy gamit ang ({{tokenCount}}) (na) token",
+ "batch_sell_max_tokens_allowed": "Max na 5 token ang pinapayagan",
+ "batch_sell_single_token_dialog_title": "Alerto sa mataas na rate",
+ "batch_sell_single_token_dialog_description": "Ang pagbebenta ng isang token nang naka-batch may maaaring humantong sa mas mataas na rate. Gusto mo bang mag-swap na lang?",
+ "batch_sell_swap_instead": "Oo, i-swap",
+ "batch_sell_review_title": "Suriin ang Pagbebenta nang Naka-batch",
+ "batch_sell_select_stablecoin": "Pumili ng stablecoin",
+ "batch_sell_total_received": "Kabuuang natanggap",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "Suriin",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "I-customize {{tokenSymbol}}",
+ "batch_sell_remove_token": "Alisin ang {{tokenSymbol}}",
+ "batch_sell_checkbox_label": "Ibenta nang Naka-batch ang token",
+ "sort_balance": "Balanse",
+ "next": "Susunod",
+ "explore_tokens": "Tuklasin ang mga token",
"no_tokens_found": "Walang nakitang token",
"no_tokens_found_description": "Wala kaming nakitang anumang token na may ganitong pangalan. Subukang maghanap ng iba.",
"select_network": "Pumili ng network",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "Mawawalan ka ng tinatayang {{priceImpact}} ng halaga ng token mo sa swap na ito. Subukang ibaba ang halaga o pumili ng mas liquid na ruta.",
"proceed": "Magpatuloy",
"cancel": "Kanselahin",
+ "close": "Isara",
"slippage_info_title": "Slippage",
"slippage_info_description": "Ang % ng pagbabago sa presyo na handa mong payagan bago makansela ang iyong transaksyon.",
"blockaid_error_title": "Babawiin ang transaksyong ito",
@@ -8107,7 +8221,14 @@
"retry": "Subukang muli",
"on_linea": "sa Linea",
"account_label": "Account",
- "token_label": "Token"
+ "token_label": "Token",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "cashback na mUSD",
@@ -8336,7 +8457,7 @@
"main_title": "Mga Reward",
"vip": {
"bps_unit": "bps",
- "swaps_label": "Swaps",
+ "swaps_label": "Mga Swap",
"perps_label": "Perps",
"error_title": "We couldn’t load your VIP dashboard",
"error_description": "Check your connection and try again.",
@@ -8445,6 +8566,7 @@
"show_less": "Magpakita ng mas kaunti",
"linking_progress": "Idinadagdag ang mga account... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} ang na-enroll",
+ "accounts_added": "Accounts added",
"add_all_accounts": "Idagdag ang lahat ng account",
"environment_selector": "Environment",
"environment_cancel": "Kanselahin",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "Umalis sa Rewards program",
- "description": "Dahil dito, maaalis ang iyong mga account mula sa programa ng Mga Reward at mawawala ang mga nagawa mo na. Hindi mo na ito mababawi.",
- "confirm": "Permanenteng Tanggalin",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "Sigurado ka ba?",
- "confirmation_description": "Aalisin nito ang lahat ng iyong progreso at hindi na ito maibabalik. Kung muli kang sasali sa programa ng mga Reward sa hinaharap, magsisimula ka muli sa 0.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "Kanselahin",
"confirm": "Kumpirmahin",
+ "error_title": "Mayroong nang mali",
"error_message": "Nabigong mag-opt-out sa mga Reward. Pakisubukang muli.",
"processing": "Pinoproseso..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "I-dismiss",
@@ -8562,7 +8691,8 @@
"title_claim": "Mag-claim ng Benepisyo",
"action": "I-claim",
"empty-list": "Waala kang anumang benepisyo sa ngayon.",
- "powered_by": "Pinapagana ng"
+ "powered_by": "Pinapagana ng",
+ "available_count": "{{count}} available"
},
"end_of_season_rewards": {
"confirm_label_default": "Kumpirmahin",
@@ -8695,7 +8825,7 @@
"label_your_rank": "Rank mo",
"label_portfolio": "Portfolio",
"label_net_inflow": "Net inflow",
- "label_total_inflow": "Total inflow",
+ "label_total_inflow": "Kabuuang inflow",
"label_outflow": "Outflow",
"label_days_held": "Mga araw na na-hold",
"qualified_title": "Kwalipikado ka",
@@ -8880,9 +9010,11 @@
"musd_claim": "I-claim ang mUSD",
"perps_deposit": "Magdagdag ng pondo",
"perps_withdraw": "Pag-withdraw",
+ "predict_withdraw": "Mag-withdraw ng {{sourceSymbol}} mula sa {{sourceChain}}",
"predict_deposit": "Magdagdag ng pondo",
"swap": "Ipagpalit ang mga token",
- "swap_approval": "Aprubahan ang mga token"
+ "swap_approval": "Aprubahan ang mga token",
+ "fiat_purchase": "Bumili ng {{token}} gamit ang {{paymentMethod}}"
},
"perps_deposit_solution": "Mayroon ka na ngayong {{fiat}} ng USDC sa Arbitrum. Subukang magdeposito ulit."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "Mataas Pababa",
"low_to_high": "Mababa Pataas",
"apply": "Ilapat",
- "search_placeholder": "Maghanap ng mga token, site, URL",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "Kanselahin",
"perps": "Perps",
"rwa_perps_section": "Perps",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "Perps",
"predictions": "Mga hula",
"no_results": "Walang nahanap na mga resulta",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "Sikat",
"sites": "Mga Site",
"popular_sites": "Mga sikat na site",
"search_sites": "Maghanap ng mga site",
"view_all": "Tingnan lahat",
+ "view_more": "Tumingin ng higit pa",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "Paganahin ang basic functionality",
"basic_functionality_disabled_title": "Hindi available ang pagtuklas",
"basic_functionality_disabled_description": "Hindi namin makukuha ang kinakailangang metadata kapag hindi pinapagana ang basic functionality.",
@@ -8995,6 +9131,14 @@
"crypto": "Crypto",
"sports": "Palakasan",
"dapps": "Mga Site"
+ },
+ "search_tabs": {
+ "all": "Lahat",
+ "crypto": "Cryptos",
+ "perps": "Perps",
+ "stocks": "Stocks",
+ "predictions": "Mga hula",
+ "sites": "Mga Site"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "Walang mUSD sa network na ito. Lumipat ng network para makita ang mUSD mo.",
"money_empty_state": {
"get_started": "Magsimula",
+ "earn": "Kumita",
"earn_apy": "Kumita ng {{percentage}}% APY"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "Mga Nauugnay na Asset",
"perpetuals": "Perpetuals",
"predictions": "Mga hula",
- "whats_happening": "Ano ang nangyayari",
- "whats_happening_ai": "AI",
- "whats_happening_impact": {
- "bullish": "Bullish",
- "bearish": "Bearish",
- "neutral": "Neutral"
- },
"top_traders": "Mga Nangungunang Trader",
- "whats_happening_categories": {
- "geopolitical": "Heopolitikal",
- "macro": "Makro",
- "regulatory": "Regulatoryo",
- "technical": "Teknikal",
- "social": "Sosyal",
- "other": "Iba pa"
- },
"defi": "DeFi",
"nfts": "Mga NFT",
"trending_tokens": "Nagte-trend",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "Sikat"
+ },
+ "whats_happening": {
+ "title": "Ano ang nangyayari",
+ "ai": "AI",
+ "impact": {
+ "bullish": "Bullish",
+ "bearish": "Bearish",
+ "neutral": "Neutral"
+ },
+ "categories": {
+ "geopolitical": "Heopolitikal",
+ "macro": "Makro",
+ "regulatory": "Regulatoryo",
+ "technical": "Teknikal",
+ "social": "Sosyal",
+ "other": "Iba pa"
+ }
}
}
diff --git a/locales/languages/tr.json b/locales/languages/tr.json
index a5b68282a0ee..1a344c7a31f1 100644
--- a/locales/languages/tr.json
+++ b/locales/languages/tr.json
@@ -69,7 +69,7 @@
"insufficient_balance": {
"title": "Para yetersiz",
"message": "Hesabınızda ağ ücretlerini ödemek için yeterli %{nativeCurrency} yok.",
- "buy_action": "%{nativeCurrency} satın al"
+ "buy_action": "{nativeCurrency}% satın al"
},
"signed_or_submitted": {
"title": "İşlem devam ediyor",
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "Para mevcut değil. Farklı bir hesap kullanın."
},
+ "headless_buy_error": {
+ "title": "Fiat para satın alma işlemi başarısız oldu",
+ "message": "Fiat para satın alma işleminizle ilgili bir şeyler ters gitti. Lütfen tekrar deneyin."
+ },
"mmpay_hardware_account": {
"title": "Cüzdan desteklenmiyor",
"message": "Donanım cüzdanları desteklenmiyor.\nDevam etmek için cüzdan değiştirin."
@@ -133,8 +137,8 @@
"message": "Alıcı adresi doğrudan token transferlerini desteklemiyor olabilir ve bu durum para kaybına neden olabilir. Yalnızca transferinizin bu sözleşmeye ulaşabileceğinden eminseniz devam edin."
},
"gas_sponsorship_reserve_balance": {
- "message": "Gaz sponsorluğu bu işlem için kullanılamıyor. Hesabınızda en az %{minBalance} %{nativeTokenSymbol} tutmanız gerekecek.",
- "title": "Gaz sponsorluğu kullanılamıyor"
+ "message": "Bu ağ için hesabınızda {minBalance}% {nativeTokenSymbol}% rezerv bulunmalıdır.",
+ "title": "Rezerv bakiyesi gereklidir"
},
"token_trust_signal": {
"malicious": {
@@ -431,7 +435,7 @@
"forgot_password": "Şifreyi mi unuttunuz?",
"cant_proceed": "‘Sil’ sözcüğünü yazana kadar ilerleyemezsiniz. Bu eylemle geçerli cüzdanınızı silmeyi seçiyorsunuz.",
"invalid_password": "Şifre yanlış, tekrar deneyin.",
- "type_delete": "Onaylamak için ‘%{erase}’ yazın",
+ "type_delete": "Onaylamak için ‘{erase}%’ yazın",
"erase_my": "Evet, cüzdanı sıfırla",
"cancel": "İptal et",
"are_you_sure": "Emin misiniz?",
@@ -708,6 +712,9 @@
"contractAddressError": "Token'in sözleşme adresine token gönderiyorsunuz. Bu durum, bu token'lerin kaybedilmesine neden olabilir.",
"smart_contract_address": "Akıllı sözleşme adresi",
"smart_contract_address_warning": "Alıcı adresi doğrudan token transferlerini desteklemiyor olabilir ve bu durum para kaybına neden olabilir. Yalnızca transferinizin bu sözleşmeye ulaşabileceğinden eminseniz devam edin.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "Güncelle",
"i_understand": "Anlıyorum",
"cancel": "İptal",
"new_address_title": "Yeni adres",
@@ -991,7 +998,7 @@
"cancel_order_error": "Emrinizi iptal edemedik. Lütfen tekrar deneyin."
},
"webview_modal": {
- "error": "Webview hata aldı: %{code}"
+ "error": "Webview hata aldı: {code}%"
},
"notifications": {
"deposit_failed_title": "{{currency}} para yatırma işlemi başarısız oldu! Lütfen tekrar deneyin, verdiğimiz rahatsızlıktan dolayı özür dileriz!",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "Değer",
"pnl_percent": "K&Z %",
- "recent": "Recent"
+ "recent": "Son Kullanılanlar"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "{{symbol}} sürekli vadeli işlemi yap",
"subtitle": "K&Z'ni {{leverage}} kadar katla"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "Destek ile iletişime geç"
+ },
"today": "Bugün",
"yesterday": "Dün",
"unrealized_pnl": "Gerçekleşmemiş K&Z",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC cüzdanınıza aktarıldı",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} cüzdanınıza taşındı",
"toast_error_title": "Bir şeyler ters gitti",
- "toast_error_description": "Para çekme işlemine ilerlenemedi"
+ "toast_error_description": "Para çekme işlemine ilerlenemedi",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Tekrar dene"
},
"quote": {
"network_fee": "Ağ ücreti",
@@ -2205,7 +2219,7 @@
"got_it": "Anladım"
},
"swap_button": "Takas",
- "buy_button": "Al",
+ "buy_button": "Satın Al",
"sources_count": "+{{count}} kaynak",
"sources_title": "Haber kaynakları",
"feedback_submitted": "Geri bildirim gönderildi",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "MetaMask Tahminler",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "Dünya Kupası",
+ "banner_title": "Dünya Kupası 2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
- "props": "Props",
- "live": "Live"
+ "all": "Tümü",
+ "props": "Özel Bahisler",
+ "live": "Canlı"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
+ "group_stage": "Grup Aşaması",
+ "round_of_32": "Son 32 Tur",
+ "round_of_16": "Son 16 Tur",
+ "quarterfinals": "Çeyrek Finaller",
+ "semifinals": "Yarı Finaller",
+ "third_place": "Üçüncülük",
"final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_a": "Grup A",
+ "group_b": "Grup B",
+ "group_c": "Grup C",
+ "group_d": "Grup D",
+ "group_e": "Grup E",
+ "group_f": "Grup F",
+ "group_g": "Grup G",
+ "group_h": "Grup H",
+ "group_i": "Grup I",
+ "group_j": "Grup J",
+ "group_k": "Grup K",
+ "group_l": "Grup L",
+ "third_place_match": "Üçüncülük Maçı"
}
},
"prediction_markets": "Tahmin piyasaları",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "Çekme işlemi tamamlandı",
"withdraw_completed_subtitle": "{{amount}} USDC cüzdanınıza aktarıldı",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}} cüzdanınıza taşındı",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "Para çekme işlemleri geçici olarak kullanılamıyor",
+ "unavailable_description": "Acil yardım için lütfen Müşteri Hizmetleri ile iletişime geçin.",
"unavailable_got_it": "Anladım",
"error_title": "Bir şeyler ters gitti",
"error_description": "Çekme işlemine ilerlenemedi",
@@ -2559,7 +2573,7 @@
"slippage": "Kayma",
"price_details": "Fiyat bilgileri",
"prediction_order": "Tahmin emri",
- "prediction_order_description": "Her biri {{price}} fiyatta ~{{count}} sözleşme. Son tutar, emir defteri kullanılabilirliğe göre değişiklik gösterebilir ({{slippage}}% orana kadar).",
+ "prediction_order_description": "Her biri {{price}} fiyatta ~{{count}} sözleşme. Son tutar, emir defteri kullanılabilirliğe göre değişiklik gösterebilir ({{slippage}%} orana kadar).",
"metamask_fee_description": "Bu tahmin işlemine yönelik hizmet ücreti",
"exchange_fee": "Borsa ücreti",
"exchange_fee_description": "Borsaya veya piyasaya ödenen ücret",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "Kabul et ve kapat",
"pna25_open_settings_button": "Ayarları Aç"
},
+ "onboarding_interest_questionnaire": {
+ "title": "MetaMask ile ne yapmak istersiniz?",
+ "description": "Geçerli olanların tümünü seçin.",
+ "option_buy_and_sell_crypto": "Kripto alın ve satın",
+ "option_consolidate_wallets": "Cüzdanlarınızı birleştirin",
+ "option_advanced_trades": "Gelişmiş işlemler gerçekleştirin",
+ "option_predict_sports_events": "Spor ve etkinlik sonuçlarını tahmin edin",
+ "option_crypto_as_money": "Kriptoyu para olarak kullanın",
+ "option_connect_apps_sites": "Uygulamalara veya sitelere bağlanın",
+ "continue": "Devam et"
+ },
"template_confirmation": {
"ok": "Tamam",
"cancel": "İptal"
@@ -3167,7 +3192,7 @@
"networks": "Ağlar",
"log_out": "Oturumu kapat",
"notifications": "Bildirimler",
- "buy": "Al",
+ "buy": "Satın Al",
"scan": "Tara"
},
"app_settings": {
@@ -3261,8 +3286,27 @@
"notifications_desc": "Bildirimlerinizi yönetin",
"allow_notifications": "Bildirimlere izin ver",
"enable_push_notifications": "Anlık bildirimleri etkinleştir",
- "allow_notifications_desc": "Bildirimler ile cüzdanınızda neler olduğundan haberdar olun. Bildirimleri kullanmak için cihazlarınızdaki bazı ayarları senkronize etmek amacıyla bir profil kullanıyoruz.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "Kapalı",
+ "select_all": "Tümünü seç",
+ "deselect_all": "Tümünün seçimini kaldır",
+ "select_accounts_title": "Hesaplar",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "Bildirimlerinizi özelleştirin",
"customize_session_desc": "Almak istediğiniz bildirim türlerini açın:",
"account_session_title": "Hesap aktivitesi",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snap'ler",
"snaps_desc": "Yeni özellikler ve güncellemeler",
"products_announcements_title": "Ürün duyuruları",
- "products_announcements_desc": "Yeni ürünler ve özellikler",
- "perps_title": "Sürekli vadeli işlem sözleşmeleri ile işlem"
+ "products_announcements_desc": "Yeni ürünler ve özellikler"
},
"contacts_title": "Kişiler",
"contacts_desc": "Hesap ekle, düzenle, kaldır ve hesaplarını yönet.",
@@ -3372,13 +3415,13 @@
"general_heading": "Genel",
"privacy_heading": "Gizlilik",
"failed_to_fetch_chain_id": "Zincir kimliği alınamadı. RPC URL adresiniz doğru mu?",
- "endpoint_returned_different_chain_id": "Uç nokta farklı bir zincir kimliği getirdi: %{chainIdReturned}",
+ "endpoint_returned_different_chain_id": "Uç nokta farklı bir zincir kimliği getirdi: {chainIdReturned}%",
"chain_id_required": "Zincir kimliği gereklidir. Ağ tarafından getirilen zincir kimliği ile uyumlu olmalıdır. Bir ondalık sayı veya '0x' ön ekli bir on altılı sayı girebilirsiniz.",
"invalid_hex_number": "Geçersiz on altılı sayı.",
"invalid_hex_number_leading_zeros": "Geçersiz on altılı sayı. Lütfen baştaki tüm sıfırları silin.",
"invalid_number": "Geçersiz sayı. Ondalık sayısı veya '0x' ön ekli on altılı bir sayı girin.",
"invalid_number_leading_zeros": "Geçersiz sayı. Baştaki tüm sıfırları silin.",
- "invalid_number_range": "Geçersiz sayı. 1 ile %{maxSafeChainId} arasında bir sayı girin.",
+ "invalid_number_range": "Geçersiz sayı. 1 ile {maxSafeChainId}% arasında bir sayı girin.",
"hide_zero_balance_tokens_title": "Bakiyesi olmayan tokenları gizle",
"hide_zero_balance_tokens_desc": "Bakiyesi olmayan tokenlerin token listenizde gösterilmesini önler.",
"haptic_feedback_title": "Dokunsal geri bildirim",
@@ -3640,7 +3683,15 @@
"card": {
"title": "Kart",
"reset_onboarding_description": "Kayıt akışını baştan başlatmak için Kart kayıt durumunu sıfırlayın.",
- "reset_onboarding_button": "Kayıt Durumunu Sıfırla"
+ "reset_onboarding_button": "Kayıt Durumunu Sıfırla",
+ "unlink_money_account_description": "Kartın, Para Hesabınızdan harcama yapmasına yetki veren USDC harcama limiti iznini iptal edin. Bu işlem bir approve(0) transferi gönderir ve Para Hesabını, Kart altyapısında yetkilendirilmemiş olarak işaretler.",
+ "unlink_money_account_button": "Para Hesabının Kart ile bağlantısını kesin",
+ "unlink_money_account_disabled_hint": "Kaldırılacak aktif bağlantı yok."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "Dokunsal bildirimler",
@@ -3817,9 +3868,9 @@
"fund_actionmenu": {
"deposit": "Para Yatır",
"deposit_description": "Düşük ücretli banka veya kart transferi",
- "buy": "Al",
+ "buy": "Satın al",
"buy_description": "Belirli bir token'ı almak için iyi",
- "buy_unified": "Al",
+ "buy_unified": "Satın al",
"buy_unified_description": "Nakit ile kripto satın alın",
"sell": "Sat",
"sell_description": "Nakit karşılığı kripto sat"
@@ -3842,8 +3893,8 @@
"predict_button": "Tahminler",
"add_collectible_button": "Ekle",
"info": "Bilgi",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "Toplu Satış",
+ "batch_sell_new_label": "Yeni",
"swap": "Takas Et",
"convert": "Dönüştür",
"bridge": "Köprü",
@@ -3883,7 +3934,7 @@
"troubleshoot": "Sorun Giderme",
"deposit_description": "Düşük ücretli banka veya kart transferi",
"buy_description": "Belirli bir token'ı almak için iyi",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "Bir stabil kripto para karşılığında en fazla 5 token satışı yapın",
"sell_description": "Nakit karşılığında kripto sat",
"swap_description": "Tokenler arasında swap gerçekleştirin",
"bridge_description": "Farklı ağlar arasında token transfer et",
@@ -4234,8 +4285,8 @@
"confusable_msg": "ENS adında karıştırılabilir bir karakter algıladık. Olası bir dolandırıcılığı önlemek için ENS adını kontrol edin.",
"similar_to": "şuna benzer:",
"contains_zero_width": "sıfır genişlikli karakter içeriyor",
- "dapp_suggested_gas": "Bu gaz ücreti %{origin} tarafından önerildi. Eski gaz tahminini kullanıyor ve bu yanlış olabilir. Ancak, bu gaz ücretinin düzenlenmesi işleminizde bir soruna neden olabilir. Sorularınız olursa lütfen %{origin} adresine ulaşın.",
- "dapp_suggested_eip1559_gas": "Bu gaz ücreti %{origin} tarafından önerildi. Bunu geçersiz kılmak işleminizde bir soruna neden olabilir. Sorularınız olursa lütfen %{origin} adresine ulaşın.",
+ "dapp_suggested_gas": "Bu gaz ücreti {origin}% tarafından önerildi. Eski gaz tahminini kullanıyor ve bu yanlış olabilir. Ancak, bu gaz ücretinin düzenlenmesi işleminizde bir soruna neden olabilir. Sorularınız olursa lütfen {origin}% adresine ulaşın.",
+ "dapp_suggested_eip1559_gas": "Bu gaz ücreti {origin}% tarafından önerildi. Bunu geçersiz kılmak işleminizde bir soruna neden olabilir. Sorularınız olursa lütfen {origin}% adresine ulaşın.",
"address_invalid": "Alıcı adresi geçersiz.",
"ens_not_found": "Bu isim için hiçbir adres belirlenmemiş.",
"unknown_qr_code": "Geçersiz OR kodu. Lütfen tekrar deneyin.",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "Ayarlar > Bildirimler.",
"cancel": "İptal",
"cta": "Aç"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "Hiçbir hamleyi kaçırmayın",
+ "body": "Fiyatlar hedeflerinize ulaştığında, işlemleriniz onaylandığında ve portföyünüz hareket ettiğinde gerçek zamanlı uyarılar alın. Ayrıca bu süreçte ürün güncellemeleri ve ödüller sunulur. MetaMask ile etkileşimlerinize göre güncellemeler paylaşacağız.",
+ "button_yes": "Evet",
+ "button_not_now": "Şimdi değil",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "şimdi",
+ "title": "ETH bugün %4,2 yükseldi",
+ "message": "Şu an $2.668,51 seviyesinde — fiyat uyarınızın üzerinde"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "1sa önce",
+ "title": "0.25 ETH alındı",
+ "message": "Gönderen 0x9a21…4f8c · $640,29"
+ }
+ },
+ "existing_user": {
+ "title": "Kişiselleştirilmiş uyarılar tanıtımı",
+ "body": "İşlem yapma tarzınıza uygun bildirimler alın. Dilediğiniz zaman güncelleyin.",
+ "card_title": "Alacağınız",
+ "card_description": "İşlem hareketlerinize özel olarak uyarlanmış kişiselleştirilmiş uyarılar ve güncellemeler.",
+ "button_confirm": "Onayla",
+ "button_not_now": "Şimdi değil"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "Ödeme aracı",
"buying_via": "{{providerName}} aracılığıyla satın alınıyor.",
"change_provider": "Sağlayıcı değiştirin.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "Bir şeyler ters gitti. Lütfen tekrar deneyin.",
"no_payment_methods_available": "Kullanılabilir ödeme yöntemi yok.",
"error_fetching_quotes": "Bir şeyler ters gitti. Lütfen tekrar deneyin.",
@@ -5442,7 +5522,7 @@
"change_provider_button": "Sağlayıcı değiştirin"
},
"fiat_on_ramp_aggregator": {
- "buy": "al",
+ "buy": "satın al",
"sell": "sat",
"orders": "Transferler",
"All": "Tümü",
@@ -5515,7 +5595,7 @@
"quotes_timeout": "Teklifler için zaman aşımı",
"request_new_quotes": "En yeni en iyi oranı almak için lütfen yeni teklif talep edin.",
"terms_of_service": "Hizmet Şartları",
- "amount_to_buy": "Al",
+ "amount_to_buy": "Satın Al",
"amount_to_sell": "Sat",
"want_to_buy": "Şunu almak istiyorsun:",
"want_to_sell": "Satmak istediğiniz",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "mUSD'ye dönüştür",
"get_a_percentage_musd_bonus": "{{percentage}}% mUSD bonus al",
"convert": "Dönüştür",
+ "confirm": "Onayla",
+ "convert_tooltip_description": "Stabil kripto paralarınızı mUSD'ye dönüştürün ve günlük olarak alabileceğiniz {{percentage}}% oranına varan yıllıklandırılmış bonus kazanın. Relay tarafından desteklenmektedir.",
"fetching_quote": "Teklif alınıyor...",
"you_convert": "Dönüştürdüğünüz tutar",
"network_fee": "Ağ ücreti",
@@ -6597,7 +6679,7 @@
"step_progress": "Adım {{current}}/{{total}}",
"title": "Para ekle",
"description": "Hesabınıza para yatırın ve APY kazanmaya başlayın.",
- "add": "Ekle",
+ "add": "Bakiye ekleyin",
"step2_title": "MetaMask Card'ınızı alın",
"step2_description": "Para bakiyenizi Mastercard kabul edilen her yerde kazanırken harcamaya devam edin.",
"step2_cta": "Kartı al",
@@ -6605,6 +6687,19 @@
"link_card_description": "Bakiyenizi Mastercard kabul edilen her yerde kazanırken harcamaya devam edin.",
"link_card_cta": "Kartı bağla"
},
+ "rive_onboarding": {
+ "step1_title": "Para hesapları burada",
+ "step1_body": "Tüm cüzdanınızda geçerli olmak üzere, bakiyenizden {{percentage}}% orana varan APY kazanın.",
+ "step1_footer_text": "APY değişkendir ve her zaman değişiklik gösterebilir.",
+ "step2_title": "Otomatik kazanın",
+ "step2_body": "Stabil kripto paraları işlem ücreti ödemeden transfer edin. Fonlarınız anında kazandırmaya başlar.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "Dilediğiniz yerde harcama yapın",
+ "step3_body": "Para hesabınızı bir MetaMask karta bağlayarak satın alma işlemlerinde {{percentage}}% orana varan iade kazanın.",
+ "step4_title": "Tek yerden işlem yapın ve kazanın",
+ "step4_body": "Bir yandan getiri sağlamaya devam ederken diğer yandan Para hesabı bakiyenizi MetaMask genelinde işlem yapmak için kullanın.",
+ "continue": "Devam et"
+ },
"action": {
"add": "Ekle",
"transfer": "Transfer Et",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "Nasıl çalışır?",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "Para hesabınıza mUSD yatırım ve kazanın:",
+ "description_suffix": ". Bakiyeniz dolara endekslidir ve her zaman harcamaya, işlem yapmaya veya gönderime hazırdır."
},
"musd_row": {
"add": "Ekle"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Para bakiyesi",
"add": "Ekle",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Para bakiyesi",
+ "info_sheet_body": "Dilediğiniz zaman harcamaya, gönderime veya işlem yapmaya hazır olan dolar endeksli mUSD bakiyeniz. Bunu toplam hesap bakiyenize dahil etmeyiz. \n\nPara çekme işlemleri, ilgili blokzincirindeki standart ağ onay sürelerine bağlı olarak anında işleme alınır. Likiditenin yetersiz olduğu durumlarda geçici gecikmeler yaşanabilir."
},
"potential_earnings": {
"title": "Kriptonuzdan kazanç sağlayın",
"description": "Kriptonuzu mUSD'ye dönüştürerek paranızın zaman içinde nasıl artabileceğini görün.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "{{total}} varlığınızı dönüştürün ve bir yılda",
+ "description_with_amounts_suffix": "tutara kadar kazanın.",
"convert": "Dönüştür",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "Kriptonuzu dönüştürün",
"no_fee": "MetaMask ücreti yok",
"view_all": "Tümünü görüntüle",
"view_potential_earnings": "Potansiyel kazançları görüntüle"
@@ -6649,41 +6744,44 @@
"cashback": "{{percentage}}% mUSD iadesi",
"get_now": "Hemen alın",
"link_title": "MetaMask Card'ı bağla",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "Para bakiyenizi harcayın ve satın alma işlemlerinizde kazanç sağlayın. Üstelik bakiyenizde {{apy}}% orana varan APY.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "{{percentage}}% mUSD iadesi alın",
+ "link_bullet_apy": "{{apy}}% orana varan APY kazanın",
"link_card": "Kartı bağla",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "Harcayın ve kazanın",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "Kartı bağla",
+ "manage_card": "Yönet",
+ "avail_balance": "Kull. bakiye"
},
"what_you_get": {
"title": "Alacağınız tutar",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "Otomatik kazanın:",
"benefit_dollar_backed": "Paranız 1:1 dolar destekli bir stabil kripto para olan mUSD'de güvende tutun",
"benefit_liquidity": "Kilitleme olmadan tam likidite alarak dilediğiniz zaman işlem yapabilir veya para çekebilirsiniz",
"benefit_spend_prefix": "MetaMask Card ile 150 milyondana fazla satıcıda harcama yapın ve şunu kazanın ",
"benefit_spend_cashback": "%1-3 mUSD iadesi",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "MetaMask genelindeki tüm cüzdanlarınıza transfer gerçekleştirin",
+ "benefit_global": "Dünya çapında para gönderin ve alın",
"learn_more": "Daha fazla bilgi edin"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "Bakiye ekleyin"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "Bakiye ekleyin",
"convert_crypto": "Kripto dönüştür",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "Herhangi bir hesaptan",
"deposit_funds": "Fon yatır",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "Banka kartından veya bankadan",
+ "move_musd": "{{amount}} mUSD'nizi transfer edin",
+ "move_musd_no_amount": "mUSD'nizi transfer edin",
+ "move_musd_description": "Bakiyenizden",
"receive_external": "Harici cüzdandan al",
"coming_soon": "Çok yakında"
},
@@ -6694,7 +6792,7 @@
"contact_support": "Destek ile iletişime geç"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "Fon transfer edin",
"between_accounts": "Hesaplar arasında",
"perps_account": "Perps hesabı",
"predictions_account": "Tahminler hesabı",
@@ -6712,8 +6810,8 @@
"body": "Güncel bakiyenize ve bugünkü APY'ye göre bir dönem boyunca ne kadar kazanabileceğinize dair bir tahmin. Tahminler garantili getiriler olmayıp değişikliğe tabidir."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "Kriptonuzdan kazanç sağlayın",
+ "body": "Gösterim, {{percentage}}% Yıllık Yüzde Getirinin (APY) bir yıl boyunca değişmeden kaldığı varsayımına dayanmaktadır. APY değişkendir ve çeşitli faktörlere bağlı olarak değişiklik gösterebilir. Getiri garantisi yoktur."
},
"activity": {
"title": "Aktivite",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "Nasıl çalışır?",
- "how_it_works_subtitle": "Paranızın sizin için nasıl çalıştığını görün",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "mUSD hakkında daha fazla bilgi alın",
"what_you_get_title": "Alacağınız tutar",
@@ -6738,7 +6836,8 @@
"sent": "Gönderildi",
"transferred": "Transfer Edildi",
"card_transaction": "Kart işlemi",
- "converted": "Dönüştürüldü"
+ "converted": "Dönüştürüldü",
+ "failed": "Başarısız oldu"
},
"convert_stablecoins": {
"title": "Stabil kripto paralarınızı dönüştürün",
@@ -6752,16 +6851,16 @@
"convert_cta": "mUSD'ye dönüştür",
"learn_more": "Daha fazla bilgi edin",
"swap": "Takas",
- "buy": "Al"
+ "buy": "Satın al"
},
"how_it_works_page": {
"header_title": "Para",
"section_title": "Nasıl çalışır?",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "Para hesabınıza mUSD yatırın ve otomatik olarak {{percentage}}% orana varan APY (değişken) kazanın. Fonlar, denetlenmiş borç verme piyasalarında getiri sağlayan bir DeFi kasasına aktarılır; stake etme, talep etme veya kilitleme yoktur.",
"description_2": "Para bakiyeniz, bakiyenizi harcıyor. MetaMask Card'ınızı bağlayarak dünya çapında 150 milyondan fazla satıcıda harcama yapabilirsiniz. Kullandığınız ana kadar para kazanmaya devam eder.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "Sıkça sorulan sorular",
"faq_placeholder_answer": "Çok yakında.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "{{percentage}}% APY nasıl çalışır?",
"faq_q2": "mUSD nedir?",
"faq_q3": "Getiri nereden gelir?",
"faq_q4": "Param kilitli mi? Dilediğim zaman çekebilir miyim?",
@@ -7030,11 +7129,12 @@
"confirm": "Onayla",
"pay_with_bottom_sheet": {
"title": "Ödeme aracı",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "Son kullanılma",
+ "bank_and_card": "Banka ve kart",
+ "crypto": "Kripto",
+ "available_balance": "{{balance}} kullanılabilir",
+ "other_assets": "Diğer varlıklar",
+ "other_assets_description": "Tokenlarınızın arasından seçiminizi yapın"
},
"staking_footer": {
"part1": "Devam ederek şu bölümlerimizi kabul edersiniz: ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "mUSD dönüştürme işlemine ağ maliyetleri dahildir ve sağlayıcı ücretleri dahil olabilir. mUSD dönüştürürken MetaMask ücreti uygulanmaz."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMask, istediğiniz token ile mUSD takası gerçekleştirecek. Takas sağlayıcıları ücret alabilir ancak MetaMask almaz."
},
"title": {
"transaction_fee": "Ücretler"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "Fon ekle",
"deposit_edit_amount_predict_withdraw": "Çek",
"deposit_edit_amount_musd_conversion": "mUSD'ye dönüştür",
- "preparing_order": "Preparing order"
+ "preparing_order": "Emir hazırlanıyor"
},
"change_in_simulation_modal": {
"title": "Sonuçlar değişti",
@@ -7250,20 +7350,33 @@
"confirm_swap": "Swap Yap",
"terms_and_conditions": "Şart ve Koşullar",
"select_token": "Token seç",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "En fazla 5 token seçin",
+ "batch_sell_select_subtitle": "Tüm tokenların aynı ağda olması gerekir.",
+ "batch_sell_empty_state_title": "Token yok. Sorun değil.",
+ "batch_sell_empty_state_description": "Token yok. Sorun değil. Toplu satış için tokenları keşfedin ve satın alın.",
+ "batch_sell_continue_with_one_token": "(1) token ile devam et",
+ "batch_sell_continue_with_tokens": "({{tokenCount}}) token ile devam et",
+ "batch_sell_max_tokens_allowed": "En fazla 5 tokena izin verilir",
+ "batch_sell_single_token_dialog_title": "Yüksek oran uyarısı",
+ "batch_sell_single_token_dialog_description": "Tek bir tokenın toplu satışı daha yüksek bir orana neden olabilir. Onun yerine takas işlemi gerçekleştirmek ister misiniz?",
+ "batch_sell_swap_instead": "Evet, takas et",
+ "batch_sell_review_title": "Toplu Satışı İncele",
+ "batch_sell_select_stablecoin": "Bir stabil kripto para seçin",
+ "batch_sell_total_received": "Toplam alınan",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "İncele",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "Özelleştir: {{tokenSymbol}}",
+ "batch_sell_remove_token": "{{tokenSymbol}} adlı snap'i kaldır",
+ "batch_sell_checkbox_label": "Toplu Token Satışı",
+ "sort_balance": "Bakiye",
+ "next": "Sonraki",
+ "explore_tokens": "Tokenları keşfedin",
"no_tokens_found": "Token bulunamadı",
"no_tokens_found_description": "Bu isimde token bulamadık. Farklı bir arama yapmayı deneyin.",
"select_network": "Ağ seç",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "Bu takas işleminde tokenınızın değerinden yaklaşık {{priceImpact}} kaybedeceksiniz. Miktarı düşürmeyi veya daha likit bir rota seçmeyi deneyin.",
"proceed": "Devam et",
"cancel": "İptal",
+ "close": "Kapat",
"slippage_info_title": "Kayma",
"slippage_info_description": "İşleminiz iptal edilmeden önce izin vermek istediğiniz fiyat değişim % oranı.",
"blockaid_error_title": "Bu işlem geri alınacak",
@@ -8107,7 +8221,14 @@
"retry": "Tekrar dene",
"on_linea": "Linea üzerinde",
"account_label": "Hesap",
- "token_label": "tokenlar"
+ "token_label": "tokenlar",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "mUSD İadesi",
@@ -8445,6 +8566,7 @@
"show_less": "Daha az göster",
"linking_progress": "Hesaplar ekleniyor... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} kayıtlı",
+ "accounts_added": "Accounts added",
"add_all_accounts": "Tüm hesapları ekle",
"environment_selector": "Ortam",
"environment_cancel": "İptal",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "Ödüller Programını Sil",
- "description": "Bu işlem, hesaplarınızı Ödüller programından kaldırır ve ilerlemenizi siler. Bu işlemi geri alamazsınız.",
- "confirm": "Sil",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "Emin misiniz?",
- "confirmation_description": "Bu işlem tüm ilerlemenizi kaldırır ve geri alınamaz. Ödüller programına daha sonra tekrar katılırsanız 0'dan başlayacaksınız.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "İptal",
"confirm": "Onayla",
+ "error_title": "Bir şeyler ters gitti",
"error_message": "Ödüllerden ayrılma başarısız oldu. Lütfen tekrar deneyin.",
"processing": "Gerçekleştiriliyor..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "Yok say",
@@ -8562,7 +8691,8 @@
"title_claim": "Faydayı Al",
"action": "Al",
"empty-list": "Şu anda faydanız yok.",
- "powered_by": "Destekleyen"
+ "powered_by": "Destekleyen",
+ "available_count": "{{count}} kullanılabilir"
},
"end_of_season_rewards": {
"confirm_label_default": "Onayla",
@@ -8880,9 +9010,11 @@
"musd_claim": "mUSD al",
"perps_deposit": "Fon ekle",
"perps_withdraw": "Çekme işlemi",
+ "predict_withdraw": "{{sourceChain}} üzerinden {{sourceSymbol}} çekin",
"predict_deposit": "Fon ekle",
"swap": "Token swap işlemi yapın",
- "swap_approval": "Token'leri onayla"
+ "swap_approval": "Token'leri onayla",
+ "fiat_purchase": "{{paymentMethod}} ile {{token}} satın alın"
},
"perps_deposit_solution": "Şu anda Arbitrum üzerinde {{fiat}} USDC'niz var. Yatırma işleminizi tekrar deneyin."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "Yüksekten düşüğe",
"low_to_high": "Düşükten yükseğe",
"apply": "Uygula",
- "search_placeholder": "Token, site, URL ara",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "İptal",
"perps": "Sürekli Vadeli",
"rwa_perps_section": "Sürekli Vadeli",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "Sürekli Vadeli",
"predictions": "Tahminler",
"no_results": "Sonuç bulunamadı",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "Popüler",
"sites": "Siteler",
"popular_sites": "Popüler siteler",
"search_sites": "Siteleri ara",
"view_all": "Tümünü görüntüle",
+ "view_more": "Daha fazlasını görüntüle",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "Temel işlevselliği etkinleştir",
"basic_functionality_disabled_title": "Keşfet özelliği kullanılamıyor",
"basic_functionality_disabled_description": "Temel işlevsellik devre dışıyken gerekli meta verileri getiremiyoruz.",
@@ -8995,6 +9131,14 @@
"crypto": "Kripto",
"sports": "Spor",
"dapps": "Siteler"
+ },
+ "search_tabs": {
+ "all": "Tümü",
+ "crypto": "Cryptos",
+ "perps": "Sürekli Vadeli",
+ "stocks": "Hisse Senetleri",
+ "predictions": "Tahminler",
+ "sites": "Siteler"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "Bu ağda mUSD yok. mUSD'nizi görmek için ağ değiştirin.",
"money_empty_state": {
"get_started": "Başlarken",
+ "earn": "Kazan",
"earn_apy": "{{percentage}}% APY kazan"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "İlgili Varlıklar",
"perpetuals": "Sürekli Vadeli İşlemler",
"predictions": "Tahminler",
- "whats_happening": "Neler oluyor",
- "whats_happening_ai": "Yapay Zeka",
- "whats_happening_impact": {
- "bullish": "Boğa",
- "bearish": "Ayı",
- "neutral": "Nötr"
- },
"top_traders": "En İyi Yatırımcılar",
- "whats_happening_categories": {
- "geopolitical": "Jeopolitik",
- "macro": "Makro",
- "regulatory": "Mevzuat",
- "technical": "Teknik",
- "social": "Sosyal",
- "other": "Diğer"
- },
"defi": "DeFi",
"nfts": "NFT'ler",
"trending_tokens": "Trend",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "Popüler"
+ },
+ "whats_happening": {
+ "title": "Neler oluyor",
+ "ai": "Yapay Zeka",
+ "impact": {
+ "bullish": "Boğa",
+ "bearish": "Ayı",
+ "neutral": "Nötr"
+ },
+ "categories": {
+ "geopolitical": "Jeopolitik",
+ "macro": "Makro",
+ "regulatory": "Mevzuat",
+ "technical": "Teknik",
+ "social": "Sosyal",
+ "other": "Diğer"
+ }
}
}
diff --git a/locales/languages/vi.json b/locales/languages/vi.json
index 06116eac4254..17ab0afe71ae 100644
--- a/locales/languages/vi.json
+++ b/locales/languages/vi.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "Không có tiền khả dụng. Hãy sử dụng tài khoản khác."
},
+ "headless_buy_error": {
+ "title": "Mua bằng tiền pháp định không thành công",
+ "message": "Đã xảy ra lỗi với giao dịch mua bằng tiền pháp định của bạn. Vui lòng thử lại."
+ },
"mmpay_hardware_account": {
"title": "Ví không được hỗ trợ",
"message": "Ví cứng hiện chưa được hỗ trợ.\nĐổi ví để tiếp tục."
@@ -133,8 +137,8 @@
"message": "Địa chỉ người nhận có thể không hỗ trợ chuyển khoản token trực tiếp, điều này có thể dẫn đến mất tiền. Chỉ tiếp tục nếu bạn chắc chắn hợp đồng này có thể nhận khoản chuyển của bạn."
},
"gas_sponsorship_reserve_balance": {
- "message": "Tài trợ phí gas không khả dụng cho giao dịch này. Bạn cần giữ ít nhất %{minBalance} %{nativeTokenSymbol} trong tài khoản của mình.",
- "title": "Tài trợ phí gas không khả dụng"
+ "message": "Mạng cụ thể này yêu cầu duy trì số dư dự trữ tối thiểu là %{minBalance} %{nativeTokenSymbol} trong tài khoản của bạn.",
+ "title": "Yêu cầu số dư dự trữ"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "Bạn đang gửi token đến địa chỉ hợp đồng của token. Điều này có thể dẫn đến việc mất token đó.",
"smart_contract_address": "Địa chỉ hợp đồng thông minh",
"smart_contract_address_warning": "Địa chỉ người nhận có thể không hỗ trợ chuyển khoản token trực tiếp, điều này có thể dẫn đến mất tiền. Chỉ tiếp tục nếu bạn chắc chắn hợp đồng này có thể nhận khoản chuyển của bạn.",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "Cập nhật",
"i_understand": "Tôi hiểu",
"cancel": "Hủy",
"new_address_title": "Địa chỉ mới",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "Giá trị",
"pnl_percent": "Lãi/Lỗ %",
- "recent": "Recent"
+ "recent": "Gần đây"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "Giao dịch hợp đồng vĩnh cửu {{symbol}}",
"subtitle": "Nhân Lãi/Lỗ của bạn lên đến {{leverage}} lần"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "Liên hệ bộ phận hỗ trợ"
+ },
"today": "Hôm nay",
"yesterday": "Hôm qua",
"unrealized_pnl": "Lãi/Lỗ chưa thực hiện",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC đã được chuyển vào ví của bạn",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} đã được chuyển vào ví của bạn",
"toast_error_title": "Đã xảy ra sự cố",
- "toast_error_description": "Không thể tiến hành rút tiền"
+ "toast_error_description": "Không thể tiến hành rút tiền",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "Thử lại"
},
"quote": {
"network_fee": "Phí mạng",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "MetaMask Dự đoán",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "Giải vô địch bóng đá thế giới",
+ "banner_title": "Giải vô địch bóng đá thế giới 2026",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
- "props": "Props",
- "live": "Live"
+ "all": "Tất cả",
+ "props": "Kèo phụ",
+ "live": "Đang diễn ra"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
- "final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_stage": "Vòng bảng",
+ "round_of_32": "Vòng 32 đội",
+ "round_of_16": "Vòng 16 đội",
+ "quarterfinals": "Tứ kết",
+ "semifinals": "Bán kết",
+ "third_place": "Tranh hạng ba",
+ "final": "Chung cuộc",
+ "group_a": "Bảng A",
+ "group_b": "Bảng B",
+ "group_c": "Bảng C",
+ "group_d": "Bảng D",
+ "group_e": "Bảng E",
+ "group_f": "Bảng F",
+ "group_g": "Bảng G",
+ "group_h": "Bảng H",
+ "group_i": "Bảng I",
+ "group_j": "Bảng J",
+ "group_k": "Bảng K",
+ "group_l": "Bảng L",
+ "third_place_match": "Trận tranh hạng ba"
}
},
"prediction_markets": "Thị trường dự đoán",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "Rút tiền hoàn tất",
"withdraw_completed_subtitle": "{{amount}} USDC đã được chuyển vào ví của bạn",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}} đã được chuyển vào ví của bạn",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "Tính năng rút tiền tạm thời không khả dụng",
+ "unavailable_description": "Để được hỗ trợ khẩn cấp, vui lòng liên hệ Bộ phận Chăm sóc Khách hàng.",
"unavailable_got_it": "Tôi đã hiểu",
"error_title": "Đã xảy ra sự cố",
"error_description": "Không thể thực hiện yêu cầu rút tiền",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "Chấp nhận và đóng",
"pna25_open_settings_button": "Mở Cài đặt"
},
+ "onboarding_interest_questionnaire": {
+ "title": "Bạn muốn làm gì với MetaMask?",
+ "description": "Chọn tất cả các mục phù hợp.",
+ "option_buy_and_sell_crypto": "Mua và bán tiền mã hóa",
+ "option_consolidate_wallets": "Hợp nhất ví của bạn",
+ "option_advanced_trades": "Thực hiện giao dịch nâng cao",
+ "option_predict_sports_events": "Dự đoán thể thao và sự kiện",
+ "option_crypto_as_money": "Sử dụng tiền mã hóa như tiền tệ",
+ "option_connect_apps_sites": "Kết nối với ứng dụng hoặc trang web",
+ "continue": "Tiếp tục"
+ },
"template_confirmation": {
"ok": "OK",
"cancel": "Hủy"
@@ -3261,8 +3286,27 @@
"notifications_desc": "Quản lý thông báo",
"allow_notifications": "Cho phép thông báo",
"enable_push_notifications": "Bật thông báo đẩy",
- "allow_notifications_desc": "Nhận thông báo để cập nhật tình hình trong ví của bạn. Để sử dụng tính năng thông báo, chúng tôi sử dụng hồ sơ để đồng bộ một số chế độ cài đặt trên các thiết bị của bạn.",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "Tắt",
+ "select_all": "Chọn tất cả",
+ "deselect_all": "Bỏ chọn tất cả",
+ "select_accounts_title": "Tài khoản",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "Tùy chỉnh thông báo",
"customize_session_desc": "Bật các loại thông báo mà bạn muốn nhận:",
"account_session_title": "Hoạt động của tài khoản",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snap",
"snaps_desc": "Tính năng và cập nhật mới",
"products_announcements_title": "Thông báo sản phẩm",
- "products_announcements_desc": "Sản phẩm và tính năng mới",
- "perps_title": "Giao dịch hợp đồng vĩnh cửu"
+ "products_announcements_desc": "Sản phẩm và tính năng mới"
},
"contacts_title": "Danh bạ",
"contacts_desc": "Thêm, sửa, xóa và quản lý các tài khoản của bạn.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "Thẻ",
"reset_onboarding_description": "Đặt lại trạng thái thiết lập Thẻ để bắt đầu lại quy trình thiết lập từ đầu.",
- "reset_onboarding_button": "Đặt lại trạng thái thiết lập"
+ "reset_onboarding_button": "Đặt lại trạng thái thiết lập",
+ "unlink_money_account_description": "Thu hồi quyền cấp hạn mức chi tiêu USDC cho phép Thẻ sử dụng tiền từ Tài khoản Tài chính của bạn. Thao tác này sẽ gửi một giao dịch phê duyệt (0) và đánh dấu Tài khoản Tài chính là chưa được ủy quyền trên hệ thống phụ trợ của Thẻ.",
+ "unlink_money_account_button": "Hủy liên kết Tài khoản Tài chính khỏi Thẻ",
+ "unlink_money_account_disabled_hint": "Không có liên kết đang hoạt động để xóa."
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "Phản hồi rung",
@@ -3842,8 +3893,8 @@
"predict_button": "Dự đoán",
"add_collectible_button": "Thêm",
"info": "Thông tin",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "Bán hàng loạt",
+ "batch_sell_new_label": "Mới",
"swap": "Hoán đổi",
"convert": "Chuyển đổi",
"bridge": "Cầu nối",
@@ -3883,7 +3934,7 @@
"troubleshoot": "Khắc phục sự cố",
"deposit_description": "Chuyển khoản ngân hàng hoặc thẻ với phí thấp",
"buy_description": "Phù hợp để mua một token cụ thể",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "Bán tối đa 5 token để đổi lấy một đồng ổn định",
"sell_description": "Bán tiền mã hóa lấy tiền mặt",
"swap_description": "Chuyển đổi giữa các token",
"bridge_description": "Chuyển token giữa các mạng",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "Cài đặt > Thông báo.",
"cancel": "Hủy",
"cta": "Bật"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "Đừng bỏ lỡ bất kỳ biến động nào",
+ "body": "Nhận cảnh báo theo thời gian thực khi giá đạt mục tiêu, giao dịch được xác nhận và danh mục đầu tư của bạn có biến động. Ngoài ra còn có cập nhật sản phẩm và phần thưởng trong suốt quá trình sử dụng. Chúng tôi sẽ chia sẻ các cập nhật dựa trên tương tác của bạn với MetaMask.",
+ "button_yes": "Có",
+ "button_not_now": "Không phải bây giờ",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "ngay bây giờ",
+ "title": "Hôm nay ETH tăng 4,2%",
+ "message": "Hiện ở mức $2.668,51 — cao hơn mức cảnh báo giá của bạn"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "1 giờ trước",
+ "title": "Đã nhận 0,25 ETH",
+ "message": "Từ 0x9a21…4f8c · $640,29"
+ }
+ },
+ "existing_user": {
+ "title": "Giới thiệu về cảnh báo được cá nhân hóa",
+ "body": "Nhận thông báo phù hợp với cách bạn giao dịch. Có thể cập nhật bất cứ lúc nào.",
+ "card_title": "Bạn sẽ nhận được gì",
+ "card_description": "Cảnh báo và cập nhật được cá nhân hóa dựa trên hoạt động giao dịch của bạn.",
+ "button_confirm": "Xác nhận",
+ "button_not_now": "Không phải bây giờ"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "Thanh toán bằng",
"buying_via": "Mua qua {{providerName}}.",
"change_provider": "Thay đổi nhà cung cấp.",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "Đã xảy ra lỗi. Vui lòng thử lại.",
"no_payment_methods_available": "Không có phương thức thanh toán khả dụng.",
"error_fetching_quotes": "Đã xảy ra lỗi. Vui lòng thử lại.",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "Chuyển đổi sang mUSD",
"get_a_percentage_musd_bonus": "Nhận thưởng {{percentage}}% mUSD",
"convert": "Chuyển đổi",
+ "confirm": "Xác nhận",
+ "convert_tooltip_description": "Chuyển đổi đồng ổn định của bạn sang mUSD và nhận thưởng quy đổi theo năm lên đến {{percentage}}% mà bạn có thể nhận hằng ngày. Được hỗ trợ bởi Relay.",
"fetching_quote": "Tìm nạp báo giá...",
"you_convert": "Bạn chuyển đổi",
"network_fee": "Phí mạng",
@@ -6597,7 +6679,7 @@
"step_progress": "Bước {{current}}/{{total}}",
"title": "Nạp tiền",
"description": "Nạp tiền vào tài khoản của bạn và bắt đầu nhận APY.",
- "add": "Thêm",
+ "add": "Nạp tiền",
"step2_title": "Nhận Thẻ MetaMask của bạn",
"step2_description": "Chi tiêu số dư Tài chính của bạn trong khi vẫn sinh lời, ở bất kỳ đâu chấp nhận Mastercard.",
"step2_cta": "Nhận thẻ",
@@ -6605,6 +6687,19 @@
"link_card_description": "Chi tiêu số dư của bạn trong khi vẫn sinh lời, ở bất kỳ đâu chấp nhận Mastercard.",
"link_card_cta": "Liên kết thẻ"
},
+ "rive_onboarding": {
+ "step1_title": "Tài khoản Tài chính đã ra mắt",
+ "step1_body": "Sinh lời lên đến {{percentage}}% APY trên số dư của bạn, áp dụng cho toàn bộ ví của bạn.",
+ "step1_footer_text": "APY sẽ biến động và có thể thay đổi bất cứ lúc nào.",
+ "step2_title": "Tự động sinh lời",
+ "step2_body": "Chuyển đồng ổn định mà không mất phí chuyển đổi. Tài sản bắt đầu sinh lời ngay lập tức.",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "Chi tiêu ở bất kỳ đâu",
+ "step3_body": "Nhận hoàn tiền lên đến {{percentage}}% cho các giao dịch mua sắm khi liên kết Tài khoản Tài chính với thẻ MetaMask của bạn.",
+ "step4_title": "Giao dịch và sinh lời trong cùng một nơi",
+ "step4_body": "Sử dụng số dư tài khoản Tài chính của bạn để giao dịch trên MetaMask trong khi vẫn tiếp tục nhận lợi nhuận.",
+ "continue": "Tiếp tục"
+ },
"action": {
"add": "Thêm",
"transfer": "Chuyển",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "Cách hoạt động",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "Gửi mUSD vào tài khoản Tài chính của bạn và nhận lên đến",
+ "description_suffix": ". Số dư của bạn được bảo chứng theo USD, sẵn sàng để chi tiêu, giao dịch hoặc gửi bất cứ lúc nào."
},
"musd_row": {
"add": "Thêm"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Số dư Tài chính",
"add": "Thêm",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Số dư Tài chính",
+ "info_sheet_body": "Số dư mUSD của bạn được bảo chứng theo USD, luôn sẵn sàng để chi tiêu, gửi hoặc giao dịch bất kỳ lúc nào. Chúng tôi không tính khoản này vào tổng số dư tài khoản của bạn.\n\nQuá trình rút tiền được xử lý ngay lập tức, tùy thuộc vào thời gian xác nhận mạng tiêu chuẩn trên chuỗi khối liên quan. Nếu thanh khoản bị hạn chế, có thể xảy ra chậm trễ tạm thời."
},
"potential_earnings": {
"title": "Kiếm lợi nhuận từ tiền mã hóa của bạn",
"description": "Xem tiền của bạn có thể tăng trưởng theo thời gian như thế nào bằng cách chuyển đổi tiền mã hóa sang mUSD.",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "Chuyển đổi {{total}} tài sản của bạn và bạn có thể nhận được lên đến",
+ "description_with_amounts_suffix": "trong một năm.",
"convert": "Chuyển đổi",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "Chuyển đổi tiền mã hóa của bạn",
"no_fee": "Không có phí MetaMask",
"view_all": "Xem tất cả",
"view_potential_earnings": "Xem thu nhập tiềm năng"
@@ -6649,41 +6744,44 @@
"cashback": "Hoàn tiền {{percentage}}% bằng mUSD",
"get_now": "Nhận ngay",
"link_title": "Liên kết Thẻ MetaMask",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "Chi tiêu số dư tài khoản Tài chính của bạn và nhận thưởng khi mua sắm. Ngoài ra, còn có thể sinh lời lên đến {{apy}}% APY trên số dư của bạn.",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "Nhận hoàn tiền {{percentage}}% bằng mUSD",
+ "link_bullet_apy": "Sinh lời lên đến {{apy}}% APY",
"link_card": "Liên kết thẻ",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "Chi tiêu và sinh lời",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "Liên kết thẻ",
+ "manage_card": "Quản lý",
+ "avail_balance": "Số dư khả dụng"
},
"what_you_get": {
"title": "Những gì bạn nhận được",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "Tự động sinh lời lên đến",
"benefit_dollar_backed": "Giữ tiền của bạn an toàn dưới dạng mUSD, một đồng ổn định được bảo chứng 1:1 theo USD",
"benefit_liquidity": "Có khả năng thanh khoản cao, không bị khóa vốn, cho phép giao dịch hoặc rút tiền bất cứ lúc nào",
"benefit_spend_prefix": "Chi tiêu tại hơn 150 triệu điểm chấp nhận với Thẻ MetaMask và được ",
"benefit_spend_cashback": "Hoàn tiền 1-3% bằng mUSD",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "Chuyển khoản đến bất kỳ ví nào của bạn trên MetaMask",
+ "benefit_global": "Gửi và nhận tiền trên toàn cầu",
"learn_more": "Tìm hiểu thêm"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "Nạp tiền"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "Nạp tiền",
"convert_crypto": "Chuyển đổi tiền mã hóa",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "Từ bất kỳ tài khoản nào",
"deposit_funds": "Nạp tiền",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "Từ thẻ ghi nợ hoặc tài khoản ngân hàng",
+ "move_musd": "Chuyển khoản {{amount}} mUSD của bạn",
+ "move_musd_no_amount": "Chuyển khoản mUSD của bạn",
+ "move_musd_description": "Từ số dư của bạn",
"receive_external": "Nhận từ ví bên ngoài",
"coming_soon": "Sắp ra mắt"
},
@@ -6694,7 +6792,7 @@
"contact_support": "Liên hệ bộ phận hỗ trợ"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "Chuyển tiền",
"between_accounts": "Giữa các tài khoản",
"perps_account": "Tài khoản hợp đồng vĩnh cửu",
"predictions_account": "Tài khoản dự đoán",
@@ -6712,8 +6810,8 @@
"body": "Ước tính số tiền bạn có thể kiếm được trong một khoảng thời gian dựa trên số dư hiện tại và APY hôm nay của bạn. Các khoản ước tính không phải là lợi nhuận được đảm bảo và vẫn có thể thay đổi."
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "Kiếm lợi nhuận từ tiền mã hóa của bạn",
+ "body": "Hình minh họa giả định rằng Lợi suất phần trăm hằng năm (APY) {{percentage}}% không thay đổi trong vòng một năm. APY sẽ biến động và có thể thay đổi do nhiều yếu tố khác nhau. Không đảm bảo lợi nhuận."
},
"activity": {
"title": "Hoạt động",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "Cách hoạt động",
- "how_it_works_subtitle": "Xem cách tiền của bạn sinh lời",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "Tìm hiểu thêm về mUSD",
"what_you_get_title": "Những gì bạn nhận được",
@@ -6738,7 +6836,8 @@
"sent": "Đã gửi",
"transferred": "Đã chuyển",
"card_transaction": "Giao dịch thẻ",
- "converted": "Đã chuyển đổi"
+ "converted": "Đã chuyển đổi",
+ "failed": "Không thành công"
},
"convert_stablecoins": {
"title": "Chuyển đổi đồng ổn định của bạn",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "Tài chính",
"section_title": "Cách hoạt động",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "Gửi mUSD vào tài khoản Tài chính của bạn và tự động sinh lời lên đến {{percentage}}% APY (biến động). Tài sản sẽ được đưa vào một kho DeFi tạo lợi nhuận trên các thị trường cho vay đã được kiểm toán — không cần ký gửi, không cần nhận, không bị khóa tài sản.",
"description_2": "Số dư Tài chính của bạn là số dư để chi tiêu. Liên kết Thẻ MetaMask để chi tiêu tại hơn 150 triệu cửa hàng trên toàn cầu. Tiền của bạn vẫn sinh lời cho đến thời điểm bạn sử dụng.",
- "faq_title": "Frequently asked questions",
+ "faq_title": "Câu hỏi thường gặp",
"faq_placeholder_answer": "Sắp ra mắt.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "APY {{percentage}}% hoạt động như thế nào?",
"faq_q2": "mUSD là gì?",
"faq_q3": "Lợi suất đến từ đâu?",
"faq_q4": "Tiền của tôi có bị khóa không? Tôi có thể rút tiền bất cứ lúc nào không?",
@@ -7030,11 +7129,12 @@
"confirm": "Xác nhận",
"pay_with_bottom_sheet": {
"title": "Thanh toán bằng",
- "last_used": "Last used",
+ "last_used": "Sử dụng lần cuối",
+ "bank_and_card": "Ngân hàng và thẻ",
"crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "available_balance": "{{balance}} khả dụng",
+ "other_assets": "Tài sản khác",
+ "other_assets_description": "Chọn từ các token của bạn"
},
"staking_footer": {
"part1": "Bằng cách tiếp tục, bạn đồng ý với ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "Phí chuyển đổi mUSD bao gồm phí mạng và có thể bao gồm phí từ nhà cung cấp. Không áp dụng phí MetaMask khi bạn chuyển đổi sang mUSD."
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMask sẽ hoán đổi mUSD của bạn sang token mong muốn. Nhà cung cấp dịch vụ hoán đổi có thể tính phí, nhưng MetaMask sẽ không tính phí."
},
"title": {
"transaction_fee": "Phí"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "Nạp tiền",
"deposit_edit_amount_predict_withdraw": "Rút tiền",
"deposit_edit_amount_musd_conversion": "Chuyển đổi sang mUSD",
- "preparing_order": "Preparing order"
+ "preparing_order": "Đang chuẩn bị lệnh"
},
"change_in_simulation_modal": {
"title": "Kết quả đã thay đổi",
@@ -7250,20 +7350,33 @@
"confirm_swap": "Hoán đổi",
"terms_and_conditions": "Điều khoản và điều kiện",
"select_token": "Chọn token",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "Chọn tối đa 5 token",
+ "batch_sell_select_subtitle": "Tất cả token phải nằm trên cùng một mạng.",
+ "batch_sell_empty_state_title": "Không có token? Không sao cả.",
+ "batch_sell_empty_state_description": "Không có token? Không sao cả. Khám phá và mua token để bán hàng loạt.",
+ "batch_sell_continue_with_one_token": "Tiếp tục với (1) token",
+ "batch_sell_continue_with_tokens": "Tiếp tục với ({{tokenCount}}) token",
+ "batch_sell_max_tokens_allowed": "Cho phép tối đa 5 token",
+ "batch_sell_single_token_dialog_title": "Cảnh báo tỷ giá cao",
+ "batch_sell_single_token_dialog_description": "Việc bán hàng loạt chỉ một token có thể dẫn đến tỷ giá cao hơn. Bạn có muốn thay bằng một giao dịch hoán đổi không?",
+ "batch_sell_swap_instead": "Có, hoán đổi",
+ "batch_sell_review_title": "Xem lại giao dịch bán hàng loạt",
+ "batch_sell_select_stablecoin": "Chọn một đồng ổn định",
+ "batch_sell_total_received": "Tổng số nhận được",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "Xem lại",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "Tùy chỉnh {{tokenSymbol}}",
+ "batch_sell_remove_token": "Xóa {{tokenSymbol}}",
+ "batch_sell_checkbox_label": "Bán hàng loạt token",
+ "sort_balance": "Số dư",
+ "next": "Tiếp theo",
+ "explore_tokens": "Khám phá token",
"no_tokens_found": "Không tìm thấy token",
"no_tokens_found_description": "Chúng tôi không tìm thấy token nào với tên này. Hãy thử từ khóa tìm kiếm khác.",
"select_network": "Chọn mạng",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "Bạn sẽ mất khoảng {{priceImpact}} giá trị token trong lần hoán đổi này. Hãy thử giảm số lượng hoặc chọn tuyến thanh khoản tốt hơn.",
"proceed": "Tiếp tục",
"cancel": "Hủy",
+ "close": "Đóng",
"slippage_info_title": "Trượt giá",
"slippage_info_description": "Phần trăm thay đổi giá mà bạn chấp nhận trước khi giao dịch bị hủy.",
"blockaid_error_title": "Giao dịch này sẽ được hoàn lại",
@@ -8107,7 +8221,14 @@
"retry": "Thử lại",
"on_linea": "trên Linea",
"account_label": "Tài khoản",
- "token_label": "Token"
+ "token_label": "Token",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "Hoàn tiền bằng mUSD",
@@ -8445,6 +8566,7 @@
"show_less": "Thu gọn",
"linking_progress": "Đang thêm tài khoản... ({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}/{{total}} đã đăng ký tham gia",
+ "accounts_added": "Accounts added",
"add_all_accounts": "Thêm tất cả tài khoản",
"environment_selector": "Môi trường",
"environment_cancel": "Hủy",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "Hủy tham gia chương trình Phần thưởng",
- "description": "Thao tác này sẽ xóa các tài khoản của bạn khỏi chương trình Phần thưởng và xóa toàn bộ tiến trình của bạn. Bạn sẽ không thể hoàn tác.",
- "confirm": "Xóa vĩnh viễn",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "Bạn có chắc chắn không?",
- "confirmation_description": "Hành động này sẽ xóa toàn bộ tiến trình của bạn và không thể đảo ngược. Nếu bạn tham gia lại chương trình Phần thưởng sau này, bạn sẽ bắt đầu lại từ 0.",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "Hủy",
"confirm": "Xác nhận",
+ "error_title": "Đã xảy ra sự cố",
"error_message": "Không thể rút khỏi chương trình Phần thưởng. Vui lòng thử lại.",
"processing": "Đang xử lý..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "Đóng",
@@ -8562,7 +8691,8 @@
"title_claim": "Nhận quyền lợi",
"action": "Nhận",
"empty-list": "Bạn hiện không có quyền lợi nào.",
- "powered_by": "Được cung cấp bởi"
+ "powered_by": "Được cung cấp bởi",
+ "available_count": "Có sẵn {{count}}"
},
"end_of_season_rewards": {
"confirm_label_default": "Xác nhận",
@@ -8880,9 +9010,11 @@
"musd_claim": "Nhận mUSD",
"perps_deposit": "Nạp tiền",
"perps_withdraw": "Rút tiền",
+ "predict_withdraw": "Rút {{sourceSymbol}} từ {{sourceChain}}",
"predict_deposit": "Nạp tiền",
"swap": "Hoán đổi token",
- "swap_approval": "Phê duyệt token"
+ "swap_approval": "Phê duyệt token",
+ "fiat_purchase": "Mua {{token}} bằng {{paymentMethod}}"
},
"perps_deposit_solution": "Bạn hiện có {{fiat}} USDC trên Arbitrum. Hãy thử nạp tiền lại."
},
@@ -8958,7 +9090,7 @@
"high_to_low": "Cao đến thấp",
"low_to_high": "Thấp đến cao",
"apply": "Áp dụng",
- "search_placeholder": "Tìm kiếm token, trang web, URL",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "Hủy",
"perps": "Vĩnh cửu",
"rwa_perps_section": "Vĩnh cửu",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "Vĩnh cửu",
"predictions": "Dự đoán",
"no_results": "Không tìm thấy kết quả",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "Phổ biến",
"sites": "Trang web",
"popular_sites": "Trang web phổ biến",
"search_sites": "Tìm kiếm trang web",
"view_all": "Xem tất cả",
+ "view_more": "Xem thêm",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "Bật chức năng cơ bản",
"basic_functionality_disabled_title": "Khám phá không khả dụng",
"basic_functionality_disabled_description": "Chúng tôi không thể tìm nạp siêu dữ liệu cần thiết khi chức năng cơ bản bị tắt.",
@@ -8995,6 +9131,14 @@
"crypto": "Crypto",
"sports": "Thể thao",
"dapps": "Trang web"
+ },
+ "search_tabs": {
+ "all": "Tất cả",
+ "crypto": "Cryptos",
+ "perps": "Vĩnh cửu",
+ "stocks": "Cổ phiếu",
+ "predictions": "Dự đoán",
+ "sites": "Trang web"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "Không có mUSD trên mạng này. Hãy chuyển mạng để xem mUSD của bạn.",
"money_empty_state": {
"get_started": "Bắt đầu",
+ "earn": "Kiếm lợi nhuận",
"earn_apy": "Nhận {{percentage}}% APY"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "Tài sản liên quan",
"perpetuals": "Hợp đồng vĩnh cửu",
"predictions": "Dự đoán",
- "whats_happening": "Tình hình hiện nay",
- "whats_happening_ai": "AI",
- "whats_happening_impact": {
- "bullish": "Xu hướng tăng",
- "bearish": "Xu hướng giảm",
- "neutral": "Trung lập"
- },
"top_traders": "Nhà giao dịch hàng đầu",
- "whats_happening_categories": {
- "geopolitical": "Địa chính trị",
- "macro": "Vĩ mô",
- "regulatory": "Quy định",
- "technical": "Kỹ thuật",
- "social": "Xã hội",
- "other": "Khác"
- },
"defi": "DeFi",
"nfts": "NFT",
"trending_tokens": "Xu hướng",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "Phổ biến"
+ },
+ "whats_happening": {
+ "title": "Tình hình hiện nay",
+ "ai": "AI",
+ "impact": {
+ "bullish": "Xu hướng tăng",
+ "bearish": "Xu hướng giảm",
+ "neutral": "Trung lập"
+ },
+ "categories": {
+ "geopolitical": "Địa chính trị",
+ "macro": "Vĩ mô",
+ "regulatory": "Quy định",
+ "technical": "Kỹ thuật",
+ "social": "Xã hội",
+ "other": "Khác"
+ }
}
}
diff --git a/locales/languages/zh.json b/locales/languages/zh.json
index 766fca8ff8c7..7d2174b5d2a0 100644
--- a/locales/languages/zh.json
+++ b/locales/languages/zh.json
@@ -120,6 +120,10 @@
"account_no_funds": {
"message": "无可用资金。请使用其他账户。"
},
+ "headless_buy_error": {
+ "title": "法币购买失败",
+ "message": "您的法币购买出错了。请重试。"
+ },
"mmpay_hardware_account": {
"title": "钱包不获支持",
"message": "暂不支持硬件钱包。请切换钱包继续操作。"
@@ -133,8 +137,8 @@
"message": "该代币接收地址可能不支持直接转账,这可能导致资金损失。建议仅在确认该合约能够接收转账的情况下继续操作。"
},
"gas_sponsorship_reserve_balance": {
- "message": "本次交易无法使用燃料赞助。您的账户中需要至少保留 %{minBalance} %{nativeTokenSymbol}。",
- "title": "燃料赞助不可用"
+ "message": "此特定网络要求在您的账户中保留 %{minBalance} %{nativeTokenSymbol} 的储备金。",
+ "title": "需要维持储备金余额"
},
"token_trust_signal": {
"malicious": {
@@ -708,6 +712,9 @@
"contractAddressError": "您正在向代币的合约地址发送代币。这可能导致这些代币丢失。",
"smart_contract_address": "智能合约地址",
"smart_contract_address_warning": "该代币接收地址可能不支持直接转账,这可能导致资金损失。建议仅在确认该合约能够接收转账的情况下继续操作。",
+ "unavailable_network_connection": "Unavailable network connection",
+ "unavailable_network_connection_description": "The connection with {{network}} is unreliable. Update network connectivity details before continuing or try again later.",
+ "update": "更新",
"i_understand": "我理解",
"cancel": "取消",
"new_address_title": "新地址",
@@ -1068,7 +1075,7 @@
"sort": {
"value": "价值",
"pnl_percent": "损益 %",
- "recent": "Recent"
+ "recent": "最近"
}
},
"trader_position": {
@@ -1120,6 +1127,11 @@
"title": "交易 {{symbol}} 永续合约",
"subtitle": "将您的盈亏放大至 {{leverage}} 倍"
},
+ "service_interruption": {
+ "title": "We're experiencing an outage",
+ "description": "Some services may be unavailable while the team works on a fix.",
+ "contact_support": "联系支持团队"
+ },
"today": "今天",
"yesterday": "昨天",
"unrealized_pnl": "未实现盈亏",
@@ -1284,7 +1296,9 @@
"toast_completed_subtitle": "{{amount}} USDC 已转移到您的钱包",
"toast_completed_any_token_subtitle": "{{amount}} {{token}} 已转入您的钱包",
"toast_error_title": "出错了......",
- "toast_error_description": "提款处理失败"
+ "toast_error_description": "提款处理失败",
+ "toast_start_error_description": "Your withdrawal wasn’t started.",
+ "try_again": "请重试"
},
"quote": {
"network_fee": "网络费",
@@ -2227,35 +2241,35 @@
"predict": {
"title": "MetaMask 预测",
"world_cup": {
- "title": "World Cup",
- "banner_title": "World Cup 2026",
- "banner_description": "Trade on World Cup markets",
+ "title": "世界杯",
+ "banner_title": "2026 年世界杯",
+ "banner_description": "Trade every match, every moment.",
"tabs": {
- "all": "All",
- "props": "Props",
- "live": "Live"
+ "all": "所有",
+ "props": "命题投注",
+ "live": "进行中"
},
"stages": {
- "group_stage": "Group Stage",
- "round_of_32": "Round of 32",
- "round_of_16": "Round of 16",
- "quarterfinals": "Quarterfinals",
- "semifinals": "Semifinals",
- "third_place": "Third Place",
- "final": "Final",
- "group_a": "Group A",
- "group_b": "Group B",
- "group_c": "Group C",
- "group_d": "Group D",
- "group_e": "Group E",
- "group_f": "Group F",
- "group_g": "Group G",
- "group_h": "Group H",
- "group_i": "Group I",
- "group_j": "Group J",
- "group_k": "Group K",
- "group_l": "Group L",
- "third_place_match": "Third Place Match"
+ "group_stage": "小组赛",
+ "round_of_32": "32 强赛",
+ "round_of_16": "16 强赛",
+ "quarterfinals": "四分之一决赛",
+ "semifinals": "半决赛",
+ "third_place": "季军赛",
+ "final": "终场",
+ "group_a": "A 组",
+ "group_b": "B 组",
+ "group_c": "C 组",
+ "group_d": "D 组",
+ "group_e": "E 组",
+ "group_f": "F 组",
+ "group_g": "G 组",
+ "group_h": "H 组",
+ "group_i": "I 组",
+ "group_j": "J 组",
+ "group_k": "K 组",
+ "group_l": "L 组",
+ "third_place_match": "季军赛"
}
},
"prediction_markets": "预测市场",
@@ -2537,8 +2551,8 @@
"withdraw_completed": "提取完成",
"withdraw_completed_subtitle": "{{amount}} USDC 已转移到您的钱包",
"withdraw_any_token_completed_subtitle": "{{amount}} {{token}} 已转入您的钱包",
- "unavailable_title": "Withdrawals temporarily unavailable",
- "unavailable_description": "For urgent assistance, please contact Customer Service.",
+ "unavailable_title": "提现服务暂时不可用",
+ "unavailable_description": "如需紧急协助,请联系客服。",
"unavailable_got_it": "知道了",
"error_title": "出错了......",
"error_description": "提款处理失败",
@@ -2941,6 +2955,17 @@
"pna25_confirm_button": "接受并关闭",
"pna25_open_settings_button": "打开设置"
},
+ "onboarding_interest_questionnaire": {
+ "title": "您想使用 MetaMask 做什么?",
+ "description": "请选择所有适用项。",
+ "option_buy_and_sell_crypto": "买卖加密货币",
+ "option_consolidate_wallets": "合并您的钱包",
+ "option_advanced_trades": "进行高级交易",
+ "option_predict_sports_events": "预测体育赛事和事件",
+ "option_crypto_as_money": "将加密货币用作货币",
+ "option_connect_apps_sites": "连接到应用或网站",
+ "continue": "继续"
+ },
"template_confirmation": {
"ok": "OK",
"cancel": "取消"
@@ -3261,8 +3286,27 @@
"notifications_desc": "管理您的通知",
"allow_notifications": "允许通知",
"enable_push_notifications": "启用推送通知",
- "allow_notifications_desc": "通过通知随时了解您的钱包动态。为了使用通知,我们使用配置文件在您的不同设备上同步某些设置。",
+ "allow_notifications_desc": "Choose what you're notified about and how.",
"notifications_opts": {
+ "preferences_title": "Preferences",
+ "push_recommended": "Push",
+ "in_app": "In-app",
+ "status_push": "Push",
+ "status_in_app": "In app",
+ "status_off": "关闭",
+ "select_all": "全部选择",
+ "deselect_all": "取消全部选择",
+ "select_accounts_title": "账户",
+ "select_accounts_desc": "Choose which accounts you'd like to get notifications for.",
+ "wallet_activity_title": "Wallet Activity",
+ "wallet_activity_desc": "Buy, sells, transfers, swaps and rewards",
+ "perps_title": "Trading Activity",
+ "perps_desc": "Perps position changes, liquidations, funding rates, and margin updates",
+ "social_ai_title": "Trading Signals",
+ "social_ai_desc": "Updates from traders and assets you follow, plus currated market news",
+ "marketing_title": "Updates and Rewards",
+ "marketing_desc": "Product updates, feature announcements, and new releases",
+ "marketing_disclaimer": "By turning this on, you agree to receive product news and marketing updates from MetaMask.",
"customize_session_title": "自定义您的通知",
"customize_session_desc": "开启您想要接收的通知类型:",
"account_session_title": "账户活动",
@@ -3276,8 +3320,7 @@
"snaps_title": "Snap",
"snaps_desc": "新功能和更新",
"products_announcements_title": "产品公告",
- "products_announcements_desc": "新产品和功能",
- "perps_title": "永续合约交易"
+ "products_announcements_desc": "新产品和功能"
},
"contacts_title": "联系方式",
"contacts_desc": "添加、编辑、删除和管理您的账户.",
@@ -3640,7 +3683,15 @@
"card": {
"title": "卡",
"reset_onboarding_description": "重设卡绑定状态,使绑定流程从头开始。",
- "reset_onboarding_button": "重设绑定状态"
+ "reset_onboarding_button": "重设绑定状态",
+ "unlink_money_account_description": "撤销授权卡从您的 Money 账户中支出 USDC 的限额。此操作将提交一笔批准 (0) 交易,并在卡后端将 Money 账户标记为未委托。",
+ "unlink_money_account_button": "解除 Money 账户与卡的关联",
+ "unlink_money_account_disabled_hint": "没有可移除的活跃关联。"
+ },
+ "identity": {
+ "title": "Identity",
+ "description": "Clears the persisted authentication session. Use this after toggling MM_DEV_API_ENV — otherwise a prod-minted JWT keeps being handed to dev-API consumers until it expires. Current MM_DEV_API_ENV: {{env}}.",
+ "clear_auth_session_button": "Clear persisted auth session"
},
"haptics": {
"title": "振动",
@@ -3842,8 +3893,8 @@
"predict_button": "预测",
"add_collectible_button": "添加",
"info": "信息",
- "batch_sell": "Batch Sell",
- "batch_sell_new_label": "New",
+ "batch_sell": "批量卖出",
+ "batch_sell_new_label": "新增",
"swap": "兑换",
"convert": "兑换",
"bridge": "桥接",
@@ -3883,7 +3934,7 @@
"troubleshoot": "故障排除",
"deposit_description": "低手续费的银行或卡转账",
"buy_description": "适合购买特定代币",
- "batch_sell_description": "Sell up to 5 tokens for a stablecoin",
+ "batch_sell_description": "卖出最多 5 个代币以换取稳定币",
"sell_description": "卖出加密货币换取现金",
"swap_description": "代币之间的兑换",
"bridge_description": "在不同网络间传送代币",
@@ -5205,6 +5256,34 @@
"manage_preferences_2": "设置 > 通知。",
"cancel": "取消",
"cta": "开启"
+ },
+ "push_onboarding": {
+ "new_user": {
+ "title": "绝不错过任何市场行情",
+ "body": "当价格达到您的目标价位、交易确认以及投资组合发生变动时,您会收到实时提醒。此外,您还会在过程中获得产品更新和奖励。我们会根据您与 MetaMask 的互动情况推送相关更新。",
+ "button_yes": "是",
+ "button_not_now": "暂时不",
+ "preview_card_1": {
+ "eyebrow": "METAMASK",
+ "time": "立即",
+ "title": "ETH 今日上涨 4.2%",
+ "message": "现价 $2668.51——已超出您设置的价格提醒"
+ },
+ "preview_card_2": {
+ "eyebrow": "METAMASK",
+ "time": "1 小时前",
+ "title": "已收到 0.25 ETH",
+ "message": "来自 0x9a21…4f8c · $640.29"
+ }
+ },
+ "existing_user": {
+ "title": "推出个性化提醒",
+ "body": "获取与您的交易方式相匹配的通知。可随时更新。",
+ "card_title": "您将获得",
+ "card_description": "根据您的交易活动量身定制的个性化提醒和更新。",
+ "button_confirm": "确认",
+ "button_not_now": "暂时不"
+ }
}
},
"protect_your_wallet_modal": {
@@ -5315,6 +5394,7 @@
"pay_with": "支付方式:",
"buying_via": "正在通过 {{providerName}} 购买。",
"change_provider": "更换提供商。",
+ "circuit_breaker_open": "This service is temporarily unavailable. Please try again in about 30 minutes.",
"payment_error": "出错了。请重试。",
"no_payment_methods_available": "无可用支付方式。",
"error_fetching_quotes": "出错了。请重试。",
@@ -6520,6 +6600,8 @@
"convert_to_musd": "兑换为 mUSD",
"get_a_percentage_musd_bonus": "获取 {{percentage}}% mUSD 奖励",
"convert": "兑换",
+ "confirm": "确认",
+ "convert_tooltip_description": "将您的稳定币兑换为 mUSD,即可赚取高达 {{percentage}}% 的年化奖励,该奖励每日可领取。由 Relay 提供支持。",
"fetching_quote": "正在获取报价...",
"you_convert": "您兑换",
"network_fee": "网络费",
@@ -6597,7 +6679,7 @@
"step_progress": "第 {{current}} 步(共 {{total}} 步)",
"title": "存入资金",
"description": "为您的账户充值并开始赚取收益(APY)。",
- "add": "添加",
+ "add": "充值",
"step2_title": "获取您的 MetaMask 卡",
"step2_description": "在任何受理万事达卡的地方,使用您的 Money 账户余额消费,同时赚取收益。",
"step2_cta": "获取卡",
@@ -6605,6 +6687,19 @@
"link_card_description": "在任何受理万事达卡的地方,使用您的余额消费,同时赚取收益。",
"link_card_cta": "关联卡"
},
+ "rive_onboarding": {
+ "step1_title": "Money 账户已上线",
+ "step1_body": "您的全部钱包余额均可享高达 {{percentage}}% 的年化收益率。",
+ "step1_footer_text": "年化收益率会变动,且可能随时更改。",
+ "step2_title": "自动赚取收益",
+ "step2_body": "转移稳定币无需兑换手续费。资金即刻开始赚取收益。",
+ "step2_footer_text": "Provided by Veda and Steakhouse Financial.",
+ "step3_title": "随处消费",
+ "step3_body": "将您的 Money 账户与 MetaMask 卡关联,消费可享高达 {{percentage}}% 的返现。",
+ "step4_title": "交易与赚取收益,一站完成",
+ "step4_body": "使用您的 Money 余额在 MetaMask 内进行交易,同时持续赚取收益。",
+ "continue": "继续"
+ },
"action": {
"add": "添加",
"transfer": "转账",
@@ -6618,8 +6713,8 @@
},
"how_it_works": {
"title": "如何运行",
- "description_prefix": "Deposit mUSD into your Money account and earn up to",
- "description_suffix": ". Your balance is dollar-backed and ready to spend, trade, or send anytime."
+ "description_prefix": "将 mUSD 存入您的 Money 账户,即可赚取高达",
+ "description_suffix": "。您的余额由美元支持,可随时用于消费、交易或转账。"
},
"musd_row": {
"add": "添加"
@@ -6627,16 +6722,16 @@
"balance_card": {
"label": "Money 余额",
"add": "添加",
- "info_sheet_title": "Money balance",
- "info_sheet_body": "Your dollar-backed mUSD balance that's always available to spend, send, or trade anytime. We don't calculate this into your total account balance.\n\nWithdrawals process immediately, subject to standard network confirmation times on the relevant blockchain. If liquidity is tight, there may be temporary delays."
+ "info_sheet_title": "Money 余额",
+ "info_sheet_body": "您的美元支持 mUSD 余额,始终可随时用于消费、转账或交易。我们不将其计入您的总账户余额。\n\n提现即时处理,但需遵循相关区块链的标准网络确认时间。若流动性紧张,可能会出现暂时延迟。"
},
"potential_earnings": {
"title": "用您的加密货币赚取收益",
"description": "了解将您的加密货币转换为 mUSD 后,资金如何随时间增值。",
- "description_with_amounts_prefix": "Convert your {{total}} in assets and you could earn up to",
- "description_with_amounts_suffix": "in one year.",
+ "description_with_amounts_prefix": "将您的 {{total}} 资产进行转换,即可赚取高达",
+ "description_with_amounts_suffix": "(每年)。",
"convert": "兑换",
- "convert_cta": "Convert your crypto",
+ "convert_cta": "兑换加密货币",
"no_fee": "无 MetaMask 手续费",
"view_all": "查看全部",
"view_potential_earnings": "查看潜在收益"
@@ -6649,41 +6744,44 @@
"cashback": "{{percentage}}% mUSD 返现",
"get_now": "立即获取",
"link_title": "关联 MetaMask 卡",
- "link_subtitle": "Spend your Money balance and earn on purchases. Plus, up to {{apy}}% APY on your balance.",
- "link_bullet_cashback": "Get {{percentage}}% mUSD back",
- "link_bullet_apy": "Earn up to {{apy}}% APY",
+ "link_subtitle": "消费您的 Money 余额,消费即享收益。此外,余额还可享高达 {{apy}}% 的年化收益率。",
+ "link_subtitle_no_apy": "Spend your Money balance and earn on purchases.",
+ "link_bullet_cashback": "获得 {{percentage}}% mUSD 返现",
+ "link_bullet_apy": "享高达 {{apy}}% 年化收益率",
"link_card": "关联卡",
- "link_pending_title": "Linking card",
- "link_pending_description": "Approving spending limit…",
- "link_success_title": "Card linked successfully",
- "link_success_description": "You can now spend while you earn",
- "link_error": "Couldn't link card",
- "manage_card": "Manage",
- "avail_balance": "Avail. balance"
+ "link_pending_title": "Linking your card",
+ "link_success_title": "Your card is ready to use",
+ "link_error": "Something went wrong linking your card",
+ "link_card_sheet_title": "消费并赚取收益",
+ "link_card_sheet_description": "Link your card so you can spend your Money balance and earn mUSD back on purchases—all while earning up to {{apy}}% APY.",
+ "link_card_sheet_description_no_apy": "Link your card so you can spend your Money balance and earn mUSD back on purchases.",
+ "link_card_sheet_cta": "关联卡",
+ "manage_card": "管理",
+ "avail_balance": "可用余额"
},
"what_you_get": {
"title": "您将获得",
- "benefit_auto_earn": "Auto-earn up to",
+ "benefit_auto_earn": "自动赚取高达",
"benefit_dollar_backed": "将您的资金安全存放于 mUSD(一种 1:1 锚定美元的稳定币)",
"benefit_liquidity": "享受完全流动性,无锁仓期限制,可随时交易或提现",
"benefit_spend_prefix": "通过 MetaMask 卡可在超过 1.5 亿家商户消费,并赚取 ",
"benefit_spend_cashback": "1-3% mUSD 返现",
- "benefit_transfer": "Transfer to any of your wallets across MetaMask",
- "benefit_global": "Send and receive funds globally",
+ "benefit_transfer": "转入您 MetaMask 中的任意钱包",
+ "benefit_global": "全球收发资金",
"learn_more": "了解详情"
},
"footer": {
- "add_money": "Add funds"
+ "add_money": "充值"
},
"add_money_sheet": {
- "title": "Add funds",
+ "title": "充值",
"convert_crypto": "兑换加密货币",
- "convert_crypto_description": "From any account",
+ "convert_crypto_description": "从任何账户",
"deposit_funds": "存入资金",
- "deposit_funds_description": "From debit card or bank",
- "move_musd": "Transfer your {{amount}} mUSD",
- "move_musd_no_amount": "Transfer your mUSD",
- "move_musd_description": "From your balance",
+ "deposit_funds_description": "从借记卡或银行",
+ "move_musd": "转账您的 {{amount}} mUSD",
+ "move_musd_no_amount": "转账您的 mUSD",
+ "move_musd_description": "从您的余额",
"receive_external": "从外部钱包收款",
"coming_soon": "即将推出"
},
@@ -6694,7 +6792,7 @@
"contact_support": "联系支持团队"
},
"transfer_sheet": {
- "title": "Transfer funds",
+ "title": "转账",
"between_accounts": "账户间转账",
"perps_account": "永续合约(Perps)账户",
"predictions_account": "预测(Predictions)账户",
@@ -6712,8 +6810,8 @@
"body": "根据您当前的余额和今天的 APY,估算您在一段时间内可能赚取的收益。预估收益仅供参考,不代表实际到账收益,且可能随市场波动。"
},
"earn_crypto_info_sheet": {
- "title": "Earn on your crypto",
- "body": "Illustration assumes {{percentage}}% Annual Percentage Yield (APY) remains unchanged for one year. APY is variable and may change due to various factors. No guarantee of return."
+ "title": "用您的加密货币赚取收益",
+ "body": "示例假设 {{percentage}}% 的年化收益率在一年内保持不变。年化收益率会浮动,并可能因多种因素而变动。收益不做保证。"
},
"activity": {
"title": "活动",
@@ -6725,7 +6823,7 @@
},
"condensed_cards": {
"how_it_works_title": "如何运行",
- "how_it_works_subtitle": "了解您的资金如何为您创造收益",
+ "how_it_works_subtitle": "See how your money can grow",
"musd_title": "MetaMask USD",
"musd_subtitle": "了解 mUSD 详情",
"what_you_get_title": "您将获得",
@@ -6738,7 +6836,8 @@
"sent": "已发送",
"transferred": "已转账",
"card_transaction": "卡交易",
- "converted": "已兑换"
+ "converted": "已兑换",
+ "failed": "失败"
},
"convert_stablecoins": {
"title": "兑换您的稳定币",
@@ -6757,11 +6856,11 @@
"how_it_works_page": {
"header_title": "Money",
"section_title": "如何运行",
- "description_1": "Deposit mUSD into your Money account and earn up to {{percentage}}% APY (variable) automatically. Funds go into a DeFi vault that generates returns across audited lending markets—no staking, no claiming, no lock-ups.",
+ "description_1": "将 mUSD 存入您的 Money 账户,即可自动赚取高达 {{percentage}}% 的年化收益率(可变动)。资金进入去中心化金融金库,通过经过审计的借贷市场产生收益——无需质押、无需领取、无锁定期。",
"description_2": "您的 Money 账户余额即您的消费余额。关联您的 MetaMask 卡,即可在全球超过 1.5 亿家商户消费。您的资金在您使用之前会持续赚取收益。",
- "faq_title": "Frequently asked questions",
+ "faq_title": "常见问题解答",
"faq_placeholder_answer": "即将推出.",
- "faq_q1": "How does the {{percentage}}% APY work?",
+ "faq_q1": "{{percentage}}% 年化收益率是如何计算的?",
"faq_q2": "什么是 mUSD?",
"faq_q3": "收益从何而来?",
"faq_q4": "我的资金会被锁定吗?可以随时提现吗?",
@@ -7030,11 +7129,12 @@
"confirm": "确认",
"pay_with_bottom_sheet": {
"title": "支付方式:",
- "last_used": "Last used",
- "crypto": "Crypto",
- "available_balance": "{{balance}} available",
- "other_assets": "Other assets",
- "other_assets_description": "Select from your tokens"
+ "last_used": "上次使用",
+ "bank_and_card": "银行与卡",
+ "crypto": "加密货币",
+ "available_balance": "{{balance}} 可用",
+ "other_assets": "其他资产",
+ "other_assets_description": "从您的代币中选择"
},
"staking_footer": {
"part1": "继续即表示您同意我们的 ",
@@ -7120,7 +7220,7 @@
"transaction_fee": "mUSD 兑换费用包含网络费用,并可能包含提供商费用。兑换为 mUSD 时,MetaMask 不收取任何费用。"
},
"money_account_withdraw": {
- "transaction_fee": "MetaMask will swap your mUSD for your desired token. Swap providers may charge a fee, but MetaMask won't."
+ "transaction_fee": "MetaMask 会将您的 mUSD 兑换为您想要的代币。兑换提供商可能会收取费用,但 MetaMask 不会。"
},
"title": {
"transaction_fee": "费用"
@@ -7225,7 +7325,7 @@
"deposit_edit_amount_done": "充值",
"deposit_edit_amount_predict_withdraw": "提取",
"deposit_edit_amount_musd_conversion": "兑换为 mUSD",
- "preparing_order": "Preparing order"
+ "preparing_order": "正在准备订单"
},
"change_in_simulation_modal": {
"title": "结果已发生变化",
@@ -7250,20 +7350,33 @@
"confirm_swap": "交换",
"terms_and_conditions": "条款和条件",
"select_token": "选择代币",
- "batch_sell_select_title": "Select up to 5 tokens",
- "batch_sell_select_subtitle": "All tokens need to be on the same network.",
- "batch_sell_empty_state_title": "No tokens. No problem.",
- "batch_sell_empty_state_description": "No tokens. No problem. Explore and buy tokens to batch sell.",
- "batch_sell_continue_with_one_token": "Continue with (1) token",
- "batch_sell_continue_with_tokens": "Continue with ({{tokenCount}}) tokens",
- "batch_sell_max_tokens_allowed": "Max 5 tokens allowed",
- "batch_sell_single_token_dialog_title": "High rate alert",
- "batch_sell_single_token_dialog_description": "Batch selling one token could lead to a higher rate. Want to do a swap instead?",
- "batch_sell_swap_instead": "Yes, swap",
- "batch_sell_checkbox_label": "Batch Sell token",
- "sort_balance": "Balance",
- "next": "Next",
- "explore_tokens": "Explore tokens",
+ "batch_sell_select_title": "选择最多 5 个代币",
+ "batch_sell_select_subtitle": "所有代币需位于同一网络。",
+ "batch_sell_empty_state_title": "没有代币。没问题。",
+ "batch_sell_empty_state_description": "没有代币。没问题。浏览并购买代币,即可进行批量卖出。",
+ "batch_sell_continue_with_one_token": "继续使用 (1) 个代币",
+ "batch_sell_continue_with_tokens": "继续使用({{tokenCount}})个代币",
+ "batch_sell_max_tokens_allowed": "最多允许 5 个代币",
+ "batch_sell_single_token_dialog_title": "高费率提醒",
+ "batch_sell_single_token_dialog_description": "批量卖出一种代币可能导致更高费率。要改用兑换吗?",
+ "batch_sell_swap_instead": "是的,兑换",
+ "batch_sell_review_title": "批量卖出审核",
+ "batch_sell_select_stablecoin": "选择一种稳定币",
+ "batch_sell_total_received": "总计收到",
+ "batch_sell_minimum_received": "Minimum received",
+ "batch_sell_quote_details_row": "{{tokenSymbol}} • {{slippage}} slippage",
+ "batch_sell_review": "查看",
+ "batch_sell_you_sell": "You sell",
+ "batch_sell_token_count": "{{tokenCount}} tokens",
+ "batch_sell_toggle_you_sell": "Toggle token details",
+ "batch_sell_sell_all": "Sell all",
+ "batch_sell_includes_metamask_fee": "Includes {{fee}}% MetaMask fee",
+ "batch_sell_customize_token": "自定义 {{tokenSymbol}}",
+ "batch_sell_remove_token": "去除 {{tokenSymbol}}",
+ "batch_sell_checkbox_label": "批量卖出代币",
+ "sort_balance": "余额",
+ "next": "下一步",
+ "explore_tokens": "探索代币",
"no_tokens_found": "找不到代币",
"no_tokens_found_description": "我们未找到与此名称匹配的代币。请尝试其他搜索方式。",
"select_network": "选择网络",
@@ -7332,6 +7445,7 @@
"price_impact_execution_description": "在此兑换中,您将损失约 {{priceImpact}} 的代币价值。请尝试降低金额或选择流动性更高的路径。",
"proceed": "继续",
"cancel": "取消",
+ "close": "关闭",
"slippage_info_title": "滑点",
"slippage_info_description": "在交易取消前,您愿意接受的价格波动百分比。",
"blockaid_error_title": "此交易将被撤销",
@@ -8107,7 +8221,14 @@
"retry": "请重试",
"on_linea": "在 Linea 上",
"account_label": "账户",
- "token_label": "代币"
+ "token_label": "代币",
+ "money_account_label": "Money account",
+ "money_account_token_symbol": "mUSD",
+ "use_money_account_cta": "Use Money account",
+ "spend_and_earn_title": "Spend while you earn",
+ "spend_and_earn_description": "Spend with your Money account and earn up to {{apy}}% APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_description_no_apy": "Spend with your Money account and earn APY on your balance. Also get {{cashback}}% mUSD back.",
+ "spend_and_earn_cta": "Link to Money account"
},
"cashback_screen": {
"title": "mUSD 返现",
@@ -8445,6 +8566,7 @@
"show_less": "收起",
"linking_progress": "正在添加账户……({{current}}/{{total}})",
"accounts_linked_count": "{{linked}}{{total}} 已加入",
+ "accounts_added": "Accounts added",
"add_all_accounts": "添加全部账户",
"environment_selector": "环境",
"environment_cancel": "取消",
@@ -8469,15 +8591,22 @@
},
"optout": {
"title": "注销奖励计划账户",
- "description": "此操作会将您的账户移出奖励计划并清除所有进度。此操作无法撤销。",
- "confirm": "注销",
+ "description": "This will remove your accounts from the Rewards program and your progress in any campaigns you've joined. Only do this if you are absolutely sure you want to erase your progress.",
+ "erase_button": "Erase progress",
"modal": {
"confirmation_title": "您确定吗?",
- "confirmation_description": "此操作将清除您所有的进度,且无法撤销。若您日后重新加入奖励计划,将从 0 开始累积。",
+ "confirmation_description": "This will erase all your progress, and can't be reversed. If you rejoin the Rewards program later, you'll start back at 0.",
+ "type_to_confirm": "Type 'erase progress' to continue",
+ "confirm_phrase": "erase progress",
"cancel": "取消",
"confirm": "确认",
+ "error_title": "出错了......",
"error_message": "注销奖励计划账户失败。请重试。",
"processing": "处理中..."
+ },
+ "request_received": {
+ "title": "Request received",
+ "description": "In about 7 days, your progress will be fully erased. If you made this request by mistake, please contact support through the app."
}
},
"toast_dismiss": "忽略",
@@ -8562,7 +8691,8 @@
"title_claim": "领取福利",
"action": "领取",
"empty-list": "您当前暂无任何福利。",
- "powered_by": "技术支持:"
+ "powered_by": "技术支持:",
+ "available_count": "{{count}} 可用"
},
"end_of_season_rewards": {
"confirm_label_default": "确认",
@@ -8880,9 +9010,11 @@
"musd_claim": "领取 mUSD",
"perps_deposit": "充值",
"perps_withdraw": "提取",
+ "predict_withdraw": "从 {{sourceChain}} 提取 {{sourceSymbol}}",
"predict_deposit": "充值",
"swap": "兑换代币",
- "swap_approval": "批准代币"
+ "swap_approval": "批准代币",
+ "fiat_purchase": "使用 {{paymentMethod}} 购买 {{token}}"
},
"perps_deposit_solution": "您目前在 Arbitrum 网络上持有价值 {{fiat}} 的 USDC。请重新尝试存款。"
},
@@ -8958,7 +9090,7 @@
"high_to_low": "从高到低",
"low_to_high": "从低到高",
"apply": "应用",
- "search_placeholder": "搜索代币、网站、URL",
+ "search_placeholder": "Search tokens, markets and URLs",
"cancel": "取消",
"perps": "永续合约",
"rwa_perps_section": "永续合约",
@@ -8971,11 +9103,15 @@
"crypto_perps_section": "永续合约",
"predictions": "预测",
"no_results": "未找到结果",
+ "no_results_for_feed": "No {{feedName}} results for \"{{query}}\"",
+ "showing_all_results_for": "We found these results for \"{{query}}\"",
"popular": "热门",
"sites": "网站",
"popular_sites": "热门网站",
"search_sites": "搜索网站",
"view_all": "查看全部",
+ "view_more": "查看更多",
+ "view_x_more": "View {{count}} more",
"enable_basic_functionality": "启用基本功能",
"basic_functionality_disabled_title": "探索功能不可用",
"basic_functionality_disabled_description": "当基础功能被禁用时,我们无法获取所需的元数据。",
@@ -8995,6 +9131,14 @@
"crypto": "加密货币",
"sports": "体育",
"dapps": "网站"
+ },
+ "search_tabs": {
+ "all": "所有",
+ "crypto": "Cryptos",
+ "perps": "永续合约",
+ "stocks": "股票",
+ "predictions": "预测",
+ "sites": "网站"
}
},
"ota_update_modal": {
@@ -9132,6 +9276,7 @@
"money_empty_description_network_filter": "此网络中没有 mUSD。请切换网络以查看您的 mUSD。",
"money_empty_state": {
"get_started": "开始",
+ "earn": "赚取",
"earn_apy": "赚取{{percentage}}% APY"
},
"money_filled_state": {
@@ -9143,22 +9288,7 @@
"related_assets": "相关资产",
"perpetuals": "永续合约",
"predictions": "预测",
- "whats_happening": "发生了什么",
- "whats_happening_ai": "人工智能(AI)",
- "whats_happening_impact": {
- "bullish": "看涨",
- "bearish": "看跌",
- "neutral": "看平"
- },
"top_traders": "顶尖交易者",
- "whats_happening_categories": {
- "geopolitical": "地缘政治",
- "macro": "宏观",
- "regulatory": "监管",
- "technical": "技术",
- "social": "社会",
- "other": "其他"
- },
"defi": "DeFi",
"nfts": "NFT",
"trending_tokens": "趋势",
@@ -9178,5 +9308,22 @@
},
"sites": {
"popular": "热门"
+ },
+ "whats_happening": {
+ "title": "发生了什么",
+ "ai": "人工智能(AI)",
+ "impact": {
+ "bullish": "看涨",
+ "bearish": "看跌",
+ "neutral": "看平"
+ },
+ "categories": {
+ "geopolitical": "地缘政治",
+ "macro": "宏观",
+ "regulatory": "监管",
+ "technical": "技术",
+ "social": "社会",
+ "other": "其他"
+ }
}
}
From 57b7bd6076280b12ca06a978d16eff24c6e7cb19 Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Thu, 21 May 2026 20:48:18 +0000
Subject: [PATCH 60/66] [skip ci] Bump version number to 5142
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 52a65577da32..060e01e64f2e 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5138
+ versionCode 5142
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index 5ac73292c1e9..a2f5008d0675 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5138
+ VERSION_NUMBER: 5142
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5138
+ FLASK_VERSION_NUMBER: 5142
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 534c1cbb67e3..d4697636c4e2 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5138;
+ CURRENT_PROJECT_VERSION = 5142;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5138;
+ CURRENT_PROJECT_VERSION = 5142;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5138;
+ CURRENT_PROJECT_VERSION = 5142;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5138;
+ CURRENT_PROJECT_VERSION = 5142;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From e74790ede914b2cf4cb516088b33a8642d44d08e Mon Sep 17 00:00:00 2001
From: metamaskbot
Date: Thu, 21 May 2026 20:54:58 +0000
Subject: [PATCH 61/66] [skip ci] Bump version number to 5143
---
android/app/build.gradle | 2 +-
bitrise.yml | 4 ++--
ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 060e01e64f2e..c35d2ace7f0a 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -188,7 +188,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.78.0"
- versionCode 5142
+ versionCode 5143
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
missingDimensionStrategy 'detox', 'full'
diff --git a/bitrise.yml b/bitrise.yml
index a2f5008d0675..a1d5694d1325 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -3558,13 +3558,13 @@ app:
VERSION_NAME: 7.78.0
- opts:
is_expand: false
- VERSION_NUMBER: 5142
+ VERSION_NUMBER: 5143
- opts:
is_expand: false
FLASK_VERSION_NAME: 7.78.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 5142
+ FLASK_VERSION_NUMBER: 5143
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index d4697636c4e2..175274ca6c67 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -990,7 +990,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5142;
+ CURRENT_PROJECT_VERSION = 5143;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1059,7 +1059,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5142;
+ CURRENT_PROJECT_VERSION = 5143;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1125,7 +1125,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5142;
+ CURRENT_PROJECT_VERSION = 5143;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1192,7 +1192,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 5142;
+ CURRENT_PROJECT_VERSION = 5143;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
From 33f6c5809bb392553a0a062cb55c325541b2b609 Mon Sep 17 00:00:00 2001
From: chloeYue
Date: Thu, 21 May 2026 23:38:17 +0200
Subject: [PATCH 62/66] ci: re-trigger CI on release/7.78.0 (previous HEAD had
[skip ci])
Co-authored-by: Cursor
From 23dfdbda43a5eeb08054727d31aaad86ba029eb2 Mon Sep 17 00:00:00 2001
From: chloeYue
Date: Thu, 21 May 2026 23:43:04 +0200
Subject: [PATCH 63/66] ci: trigger pull_request workflows after PR unlock
Co-authored-by: Cursor
From 1afb72784d86f6edebbaf278699fa21ec5007274 Mon Sep 17 00:00:00 2001
From: "runway-github[bot]"
<73448015+runway-github[bot]@users.noreply.github.com>
Date: Fri, 22 May 2026 09:18:46 +0200
Subject: [PATCH 64/66] chore(runway): cherry-pick chore: update uuid to
v14.0.0 (#30558)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- chore: update uuid to v14.0.0 (#29224)
## **Description**
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/MCWP-557
## **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**
- [ ] 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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Medium Risk**
> Major-version bump of `uuid` can change module format/typing and
affect any runtime UUID generation paths, especially in React
Native/Jest transforms. The code changes are small but dependency
behavior changes could surface at build/test time or in places relying
on `uuid` options.
>
> **Overview**
> **Upgrades `uuid` from `^8.3.2` to `^14.0.0`** (and updates
`yarn.lock` accordingly), removing the now-unneeded npm audit ignore
entry for the prior `uuid` advisory.
>
> Adjusts test infrastructure for the new `uuid` package shape: adds
`uuid` to Jest’s `transformIgnorePatterns` allowlist, updates
`analyticsId.test.ts`’s `v4` mock typing, and tweaks
`accountsControllerTestUtils.ts` to cast the `v4({ random })` input to
`Uint8Array` for the updated type expectations.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
608d200ac56b2e37a400552bcca97b61f319fc47. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---------
Co-authored-by: tommasini <46944231+tommasini@users.noreply.github.com>
Co-authored-by: tommasini
[8f0d058](https://github.com/MetaMask/metamask-mobile/commit/8f0d058a9128d57634927f7578bb95e9eaf2fb64)
Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com>
Co-authored-by: tommasini <46944231+tommasini@users.noreply.github.com>
Co-authored-by: tommasini
---
.yarnrc.yml | 1 -
app/util/analytics/analyticsId.test.ts | 2 +-
app/util/test/accountsControllerTestUtils.ts | 2 +-
jest.config.js | 2 +-
package.json | 2 +-
yarn.lock | 11 ++++++++++-
6 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/.yarnrc.yml b/.yarnrc.yml
index dba849354f62..c42f38825869 100644
--- a/.yarnrc.yml
+++ b/.yarnrc.yml
@@ -18,7 +18,6 @@ npmAuditIgnoreAdvisories:
- 1113441 # bn.js affected by an infinite loop. No fix available yet (latest is 5.2.1, affected <=5.2.3). Suppressing for now to unblock CI. https://github.com/advisories/GHSA-378v-28hj-76wf
- 1113442 # bn.js affected by an infinite loop. No fix available yet (latest is 5.2.1, affected <=5.2.3). Suppressing for now to unblock CI. https://github.com/advisories/GHSA-378v-28hj-76wf
- 1115765 # XML injection via unsafe CDATA serialization allows attacker-controlled markup insertion https://github.com/advisories/GHSA-wh4c-j3r5-mjhp
- - 1116970 # uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided. We're using v4 and v1 which are not affected. Ignored while we work through the breaking changes between fixed and used versions. Track progress: https://consensyssoftware.atlassian.net/browse/MCWP-557
yarnPath: .yarn/releases/yarn-4.14.1.cjs
diff --git a/app/util/analytics/analyticsId.test.ts b/app/util/analytics/analyticsId.test.ts
index 7acc9e90f8f5..7fb057df21f8 100644
--- a/app/util/analytics/analyticsId.test.ts
+++ b/app/util/analytics/analyticsId.test.ts
@@ -21,7 +21,7 @@ jest.mock('uuid', () => {
const mockedStorageWrapper = storageWrapper as jest.Mocked<
typeof storageWrapper
>;
-const mockedV4 = v4 as jest.MockedFunction;
+const mockedV4 = v4 as jest.Mock;
describe('getAnalyticsId', () => {
beforeEach(() => {
diff --git a/app/util/test/accountsControllerTestUtils.ts b/app/util/test/accountsControllerTestUtils.ts
index 20b684a9eeb4..8b5c9e8f90c6 100644
--- a/app/util/test/accountsControllerTestUtils.ts
+++ b/app/util/test/accountsControllerTestUtils.ts
@@ -43,7 +43,7 @@ export function createMockUuidFromAddress(address: string): AccountId {
(_, i) => address.charCodeAt(i) || 0,
);
return uuidV4({
- random: fakeShaFromAddress,
+ random: fakeShaFromAddress as unknown as Uint8Array,
});
}
diff --git a/jest.config.js b/jest.config.js
index 2ad9efe35c43..6ae224271faf 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -29,7 +29,7 @@ const config = {
setupFilesAfterEnv: ['/app/util/test/testSetup.js'],
testEnvironment: 'jest-environment-node',
transformIgnorePatterns: [
- 'node_modules/(?!((@metamask/)?(@react-native|react-native|redux-persist-filesystem|@react-navigation|@react-native-community|@react-native-masked-view|react-navigation|react-navigation-redux-helpers|@sentry|d3-color|d3-shape|d3-path|d3-scale|d3-array|d3-time|d3-format|d3-interpolate|d3-selection|d3-axis|d3-transition|internmap|react-native-wagmi-charts|react-native-nitro-modules|@notifee|expo-file-system|expo-modules-core|expo(nent)?|@expo(nent)?/.*)|@noble/.*|@nktkas/hyperliquid|@metamask/design-system-twrnc-preset|@metamask/design-system-react-native|@metamask/native-utils|@metamask/smart-transactions-controller|@tommasini/react-native-scrollable-tab-view|@veriff/react-native-sdk|@braze/react-native-sdk))',
+ 'node_modules/(?!((@metamask/)?(@react-native|react-native|redux-persist-filesystem|@react-navigation|@react-native-community|@react-native-masked-view|react-navigation|react-navigation-redux-helpers|@sentry|d3-color|d3-shape|d3-path|d3-scale|d3-array|d3-time|d3-format|d3-interpolate|d3-selection|d3-axis|d3-transition|internmap|react-native-wagmi-charts|react-native-nitro-modules|@notifee|expo-file-system|expo-modules-core|expo(nent)?|@expo(nent)?/.*)|@noble/.*|@nktkas/hyperliquid|@metamask/design-system-twrnc-preset|@metamask/design-system-react-native|@metamask/native-utils|@metamask/smart-transactions-controller|@tommasini/react-native-scrollable-tab-view|@veriff/react-native-sdk|@braze/react-native-sdk|uuid))',
],
transform: {
'^.+\\.[jt]sx?$': ['babel-jest', { configFile: './babel.config.tests.js' }],
diff --git a/package.json b/package.json
index 47e823c77f46..0135164fc2a8 100644
--- a/package.json
+++ b/package.json
@@ -547,7 +547,7 @@
"uri-js": "^4.4.1",
"url": "0.11.0",
"url-parse": "1.5.9",
- "uuid": "^8.3.2",
+ "uuid": "^14.0.0",
"valid-url": "1.0.9",
"viem": "^2.28.0",
"vm-browserify": "1.1.2"
diff --git a/yarn.lock b/yarn.lock
index 9dc6718399ad..0157dd588561 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -35845,7 +35845,7 @@ __metadata:
uri-js: "npm:^4.4.1"
url: "npm:0.11.0"
url-parse: "npm:1.5.9"
- uuid: "npm:^8.3.2"
+ uuid: "npm:^14.0.0"
valid-url: "npm:1.0.9"
viem: "npm:^2.28.0"
vm-browserify: "npm:1.1.2"
@@ -46271,6 +46271,15 @@ __metadata:
languageName: node
linkType: hard
+"uuid@npm:^14.0.0":
+ version: 14.0.0
+ resolution: "uuid@npm:14.0.0"
+ bin:
+ uuid: dist-node/bin/uuid
+ checksum: 10/8ee9b98f9650e25555515f7a28d3c3ae9364e72f7bb19b9e08b681bc135338beba5509b2830f6ae1cfaba4d45401da0d16d4d109b977097bc3d6ba0c5583341b
+ languageName: node
+ linkType: hard
+
"uuid@npm:^7.0.3":
version: 7.0.3
resolution: "uuid@npm:7.0.3"
From dcae24579ab26375d6a66bbc35289570efbce7ef Mon Sep 17 00:00:00 2001
From: Jyoti Puri
Date: Fri, 22 May 2026 19:06:48 +0530
Subject: [PATCH 65/66] feat: adding money account methods to get transaction
data array (#30532)
## **Description**
Adding methods to get money account transaction data array to be added
to perps / predict transactions.
## **Changelog**
CHANGELOG entry:
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/CONF-1428
## **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**
NA
## **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.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [ ] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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]
> **Medium Risk**
> Adds new exported helpers that compute and encode on-chain calldata
for Money Account deposits/withdrawals, including RPC reads for
quotes/rates; incorrect encoding or dependency on store/provider
availability could impact transaction construction.
>
> **Overview**
> Adds two new public utilities,
`getMoneyAccountDepositTransactionsData` and
`getMoneyAccountWithdrawTransactionsData`, that return raw calldata
arrays for Money Account **deposit** (approve+deposit) and **withdraw**
(withdraw+ERC-20 transfer) flows, sourcing vault config/money account
from Redux and short-circuiting to `[]` when required state/provider is
unavailable.
>
> Extends `moneyAccountTransactions.test.ts` with coverage for both
helpers, including expected hex output shape, correct recipient encoding
for withdrawals, and error propagation/early-return behavior when
prerequisites are missing.
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
4ae58fea9081616aa413723da078132dc81829de. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---
.../utils/moneyAccountTransactions.test.ts | 163 ++++++++++++++++++
.../Money/utils/moneyAccountTransactions.ts | 94 ++++++++++
2 files changed, 257 insertions(+)
diff --git a/app/components/UI/Money/utils/moneyAccountTransactions.test.ts b/app/components/UI/Money/utils/moneyAccountTransactions.test.ts
index 235f7edc01ba..721d243646f5 100644
--- a/app/components/UI/Money/utils/moneyAccountTransactions.test.ts
+++ b/app/components/UI/Money/utils/moneyAccountTransactions.test.ts
@@ -12,6 +12,8 @@ import {
buildMoneyAccountWithdrawBatch,
updateMoneyAccountDepositTokenAmount,
updateMoneyAccountWithdrawTokenAmount,
+ getMoneyAccountDepositTransactionsData,
+ getMoneyAccountWithdrawTransactionsData,
} from './moneyAccountTransactions';
import ReduxService from '../../../../core/redux/ReduxService';
import { selectPrimaryMoneyAccount } from '../../../../selectors/moneyAccountController';
@@ -528,4 +530,165 @@ describe('moneyAccountTransactions', () => {
);
});
});
+
+ describe('getMoneyAccountDepositTransactionsData', () => {
+ beforeEach(() => {
+ mockGetProviderByChainId.mockReturnValue(MOCK_PROVIDER as never);
+ mockSelectMoneyAccountVaultConfig.mockReturnValue(MOCK_VAULT_CONFIG);
+ (
+ jest.mocked(ReduxService) as unknown as {
+ store: { getState: jest.Mock };
+ }
+ ).store = { getState: jest.fn().mockReturnValue({}) };
+ });
+
+ it('returns two hex calldata strings for a valid amount', async () => {
+ mockPreviewDeposit.mockResolvedValue(ethers.BigNumber.from('1000000'));
+
+ const result = await getMoneyAccountDepositTransactionsData(
+ MOCK_CHAIN_ID,
+ '1.0',
+ );
+
+ expect(result).toHaveLength(2);
+ expect(result[0]).toMatch(/^0x/);
+ expect(result[1]).toMatch(/^0x/);
+ });
+
+ it('returns [] when vault config is missing', async () => {
+ mockSelectMoneyAccountVaultConfig.mockReturnValue(undefined);
+
+ const result = await getMoneyAccountDepositTransactionsData(
+ MOCK_CHAIN_ID,
+ '1.0',
+ );
+
+ expect(result).toEqual([]);
+ expect(mockPreviewDeposit).not.toHaveBeenCalled();
+ });
+
+ it('returns [] when provider is missing', async () => {
+ mockGetProviderByChainId.mockReturnValue(undefined as never);
+
+ const result = await getMoneyAccountDepositTransactionsData(
+ MOCK_CHAIN_ID,
+ '1.0',
+ );
+
+ expect(result).toEqual([]);
+ expect(mockPreviewDeposit).not.toHaveBeenCalled();
+ });
+
+ it('calls previewDeposit with the converted token amount', async () => {
+ mockPreviewDeposit.mockResolvedValue(ethers.BigNumber.from('1000000'));
+
+ await getMoneyAccountDepositTransactionsData(MOCK_CHAIN_ID, '1.0');
+
+ // 1.0 with 6 decimals = 1_000_000
+ expect(mockPreviewDeposit).toHaveBeenCalledWith(
+ expect.any(String),
+ '1000000',
+ MOCK_VAULT_CONFIG.boringVault,
+ MOCK_VAULT_CONFIG.accountantAddress,
+ );
+ });
+
+ it('propagates RPC errors', async () => {
+ mockPreviewDeposit.mockRejectedValue(new Error('RPC timeout'));
+
+ await expect(
+ getMoneyAccountDepositTransactionsData(MOCK_CHAIN_ID, '1.0'),
+ ).rejects.toThrow('RPC timeout');
+ });
+ });
+
+ describe('getMoneyAccountWithdrawTransactionsData', () => {
+ const mockGetState = jest.mocked(ReduxService).store.getState as jest.Mock;
+ const mockSelectVaultConfig = jest.mocked(selectMoneyAccountVaultConfig);
+ const mockSelectPrimaryMoneyAccount = jest.mocked(
+ selectPrimaryMoneyAccount,
+ );
+ const mockGetProvider = jest.mocked(getProviderByChainId);
+
+ const MOCK_MONEY_ACCOUNT_ADDRESS =
+ '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' as Hex;
+ const MOCK_RECIPIENT = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as Hex;
+
+ beforeEach(() => {
+ mockGetState.mockReturnValue({});
+ mockSelectVaultConfig.mockReturnValue(MOCK_VAULT_CONFIG);
+ mockSelectPrimaryMoneyAccount.mockReturnValue({
+ address: MOCK_MONEY_ACCOUNT_ADDRESS,
+ } as ReturnType);
+ mockGetProvider.mockReturnValue(
+ MOCK_PROVIDER as ReturnType,
+ );
+ mockGetRate.mockResolvedValue(ethers.BigNumber.from('1000000'));
+ });
+
+ it('returns two hex calldata strings for a valid amount', async () => {
+ const result = await getMoneyAccountWithdrawTransactionsData(
+ MOCK_CHAIN_ID,
+ '1.0',
+ MOCK_RECIPIENT,
+ );
+
+ expect(result).toHaveLength(2);
+ expect(result[0]).toMatch(/^0x/);
+ expect(result[1]).toMatch(/^0x/);
+ });
+
+ it('encodes the recipient address in the transfer calldata', async () => {
+ const result = await getMoneyAccountWithdrawTransactionsData(
+ MOCK_CHAIN_ID,
+ '1.0',
+ MOCK_RECIPIENT,
+ );
+
+ expect(result[1].toLowerCase()).toContain(
+ MOCK_RECIPIENT.toLowerCase().slice(2),
+ );
+ });
+
+ it('returns [] when vault config is missing', async () => {
+ mockSelectVaultConfig.mockReturnValue(undefined);
+
+ const result = await getMoneyAccountWithdrawTransactionsData(
+ MOCK_CHAIN_ID,
+ '1.0',
+ MOCK_RECIPIENT,
+ );
+
+ expect(result).toEqual([]);
+ expect(mockGetRate).not.toHaveBeenCalled();
+ });
+
+ it('returns [] when primary money account is missing', async () => {
+ mockSelectPrimaryMoneyAccount.mockReturnValue(undefined);
+
+ const result = await getMoneyAccountWithdrawTransactionsData(
+ MOCK_CHAIN_ID,
+ '1.0',
+ MOCK_RECIPIENT,
+ );
+
+ expect(result).toEqual([]);
+ expect(mockGetRate).not.toHaveBeenCalled();
+ });
+
+ it('returns [] when provider is missing', async () => {
+ mockGetProvider.mockReturnValue(
+ undefined as unknown as ReturnType,
+ );
+
+ const result = await getMoneyAccountWithdrawTransactionsData(
+ MOCK_CHAIN_ID,
+ '1.0',
+ MOCK_RECIPIENT,
+ );
+
+ expect(result).toEqual([]);
+ expect(mockGetRate).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/app/components/UI/Money/utils/moneyAccountTransactions.ts b/app/components/UI/Money/utils/moneyAccountTransactions.ts
index 506cce3069f7..0e2c55490953 100644
--- a/app/components/UI/Money/utils/moneyAccountTransactions.ts
+++ b/app/components/UI/Money/utils/moneyAccountTransactions.ts
@@ -296,6 +296,100 @@ export async function updateMoneyAccountWithdrawTokenAmount(
];
}
+/**
+ * Returns encoded calldata for the approve + deposit batch of a Money Account deposit.
+ *
+ * @param chainId - Chain ID in hex
+ * @param amountHuman - Human-readable deposit amount (e.g. "10.5")
+ * @returns `[approveData, depositData]`, or `[]` if vault config or provider is unavailable
+ */
+export async function getMoneyAccountDepositTransactionsData(
+ chainId: Hex,
+ amountHuman: string,
+): Promise {
+ const vaultConfig = selectMoneyAccountVaultConfig(
+ ReduxService.store.getState() as RootState,
+ );
+ if (!vaultConfig) return [];
+
+ const provider = getProviderByChainId(chainId);
+ if (!provider) return [];
+
+ const musdAddress = getMoneyAccountDepositAssetAddress(chainId);
+ const amount = BigInt(
+ calcTokenValue(amountHuman, MUSD_DECIMALS)
+ .decimalPlaces(0, BigNumber.ROUND_UP)
+ .toFixed(0),
+ );
+ const minimumMint =
+ amount === 0n
+ ? 0n
+ : applySlippage(
+ await getExpectedDepositShares({
+ lensAddress: vaultConfig.lensAddress,
+ boringVault: vaultConfig.boringVault,
+ accountantAddress: vaultConfig.accountantAddress,
+ musdAddress,
+ amount,
+ provider,
+ }),
+ );
+
+ const approveData = buildApproveData(vaultConfig.boringVault, amount);
+ const depositData = buildDepositData(musdAddress, amount, minimumMint);
+
+ return [approveData, depositData];
+}
+
+/**
+ * Returns encoded calldata for the withdraw + transfer batch of a Money Account withdrawal.
+ *
+ * @param chainId - Chain ID in hex
+ * @param amountHuman - Human-readable withdrawal amount (e.g. "10.5")
+ * @param recipient - EVM address to receive the withdrawn USDC
+ * @returns `[withdrawData, transferData]`, or `[]` if vault config or provider is unavailable
+ */
+export async function getMoneyAccountWithdrawTransactionsData(
+ chainId: Hex,
+ amountHuman: string,
+ recipient: Hex,
+): Promise {
+ const state = ReduxService.store.getState() as RootState;
+ const vaultConfig = selectMoneyAccountVaultConfig(state);
+ const primaryMoneyAccount = selectPrimaryMoneyAccount(state);
+ if (!vaultConfig || !primaryMoneyAccount?.address) return [];
+
+ const provider = getProviderByChainId(chainId);
+ if (!provider) return [];
+
+ const musdAddress = getMoneyAccountDepositAssetAddress(chainId);
+ const amount = BigInt(
+ calcTokenValue(amountHuman, MUSD_DECIMALS)
+ .decimalPlaces(0, BigNumber.ROUND_UP)
+ .toFixed(0),
+ );
+ const shareAmount =
+ amount === 0n
+ ? 0n
+ : getSharesForWithdrawal(
+ amount,
+ await getVaultRate({
+ accountantAddress: vaultConfig.accountantAddress,
+ provider,
+ }),
+ );
+
+ const withdrawData = buildWithdrawData(
+ musdAddress,
+ shareAmount,
+ amount,
+ primaryMoneyAccount.address,
+ );
+ const transferData = buildErc20TransferData(recipient, amount);
+
+ return [withdrawData, transferData];
+}
+
// -- Withdrawal helpers ----------------------------------------------------
async function getVaultRate({
From 65cbeec3271e4a4b8cced44ee0f5a10ce405acaf Mon Sep 17 00:00:00 2001
From: abretonc7s <107169956+abretonc7s@users.noreply.github.com>
Date: Fri, 22 May 2026 23:57:47 +0800
Subject: [PATCH 66/66] feat(perps): decouple agentic cache fingerprint from
project EAS fingerprint (#30569)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Adds `scripts/perps/agentic/lib/compute-cache-fp.js`, an agentic-local
native-build fingerprint, and switches the build cache in
`bc_fingerprint` to use it instead of `scripts/generate-fingerprint.js`.
The project-wide fingerprint script and `fingerprint.config.js` are
untouched, so EAS Build, EAS Update, and the OTA fingerprint guard in
`nightly-ota-updates.md` keep their existing semantics.
**Why:** `--mode auto`'s shared cache (#30565) was keyed off the same
fingerprint EAS/OTA depend on, which conservatively hashes build outputs
(`ios/build/`, `.gradle/`, IDE `xcuserdata`, env-populated
`xcconfig`/`google-services.json`). Those paths diverge per worktree, so
two slots on the same commit hashed to different keys and never shared a
cached `.app` — the cross-worktree benefit the cache was designed for
never landed. The new fingerprint uses the same `extraSources` (so
anything that genuinely affects the binary still participates) but
ignores the per-worktree noise paths.
Verified across three worktrees on the same commit (`c06187a24c`, all on
`main`):
```
mm-1: c7fe9e2161109d4ff2e092e068821a2e9984aac5
mm-5: c7fe9e2161109d4ff2e092e068821a2e9984aac5
mm-6: c7fe9e2161109d4ff2e092e068821a2e9984aac5
```
Identical → next `--mode auto` dispatch on any of these slots will
install the cached artifact from whichever slot built first.
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Follows #30565.
## **Manual testing steps**
```gherkin
Feature: Agentic cache fingerprint decoupled from project fingerprint
Scenario: Project fingerprint unchanged
When I run "node scripts/generate-fingerprint.js"
Then the hash is the same as on `main` before this PR
And EAS / OTA tools that depend on it are unaffected
Scenario: Agentic cache fingerprint ignores build artifacts
Given a worktree at any state
When I capture the agentic fingerprint
And I write a poison file under `ios/build/`
And I capture the agentic fingerprint again
Then the two fingerprints are identical
Scenario: Cross-worktree fingerprint match on same commit
Given two worktrees (mm-5, mm-6) at the same git commit
When I compute the agentic fingerprint in each
Then they match
And `--mode auto` on the second slot installs from the cached artifact stored by the first
```
Programmatic check the test suite runs:
```bash
bash scripts/perps/agentic/lib/test-build-cache.sh
```
New section "agentic fp ignores build artifacts" verifies the
ignorePaths actually take effect.
## **Screenshots/Recordings**
N/A — script-only change, no UI surface.
### **Before**
N/A
### **After**
N/A
## **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 (new "agentic fp ignores build
artifacts" assertion in `test-build-cache.sh`)
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable (file-level rationale in `compute-cache-fp.js`)
- [x] I've applied the right labels on the PR
#### Performance checks (if applicable)
- [ ] I've tested on Android — N/A (script-only)
- [ ] I've tested with a power user scenario — N/A
- [ ] I've instrumented key operations with Sentry traces — N/A
(developer tooling)
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR.
- [ ] I confirm that this PR addresses all acceptance criteria.
---
> [!NOTE]
> **Medium Risk**
> Changes the cache key used for agentic preflight native build reuse;
incorrect ignore/hashing boundaries could cause stale or mismatched
cached binaries across worktrees.
>
> **Overview**
> Decouples the agentic shared native build cache fingerprint from the
repo-wide EAS/OTA fingerprint by introducing
`scripts/perps/agentic/lib/compute-cache-fp.js` and switching
`bc_fingerprint` to use it.
>
> The new fingerprint inherits `fingerprint.config.js` `extraSources`
(plus explicitly hashes `app/core/InpageBridgeWeb3.js`) while adding
`ignorePaths` for per-worktree build outputs (e.g., `ios/build`, Xcode
`xcuserdata`, `.gradle`, NDK `.cxx`) so cache artifacts can be shared
across parallel worktrees.
>
> Updates agentic docs to describe the new keying behavior and extends
`test-build-cache.sh` with boundary tests ensuring ignored paths don’t
shift the fingerprint while binary-affecting sources still do (and
adjusts the fast-mode failure test to reference the new script).
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
32aa686a4887b73b948e120fda0d72641e336069. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---
scripts/perps/agentic/README.md | 2 +-
scripts/perps/agentic/lib/build-cache.sh | 5 +-
scripts/perps/agentic/lib/compute-cache-fp.js | 74 +++++++++++++++++++
scripts/perps/agentic/lib/test-build-cache.sh | 70 +++++++++++++++++-
4 files changed, 148 insertions(+), 3 deletions(-)
create mode 100644 scripts/perps/agentic/lib/compute-cache-fp.js
diff --git a/scripts/perps/agentic/README.md b/scripts/perps/agentic/README.md
index 494ecbb47422..4e6193260f6a 100644
--- a/scripts/perps/agentic/README.md
+++ b/scripts/perps/agentic/README.md
@@ -224,7 +224,7 @@ Compound: `{ all: [...] }`, `{ any: [...] }`, `{ none: [...] }`.
| `rebuild-native` | no | yes (no `--repo-update`) | yes | no |
| `clean` (legacy `--clean`) | yes | yes with `--repo-update` | yes | no (writes only) |
-Cache lives in `$MM_BUILD_CACHE_DIR` (default `~/Library/Caches/mm-mobile-builds` on macOS, `~/.cache/mm-mobile-builds` on Linux), keyed by `@expo/fingerprint` hash. Parallel worktrees at the same fingerprint share one artifact through a per-fingerprint mutex: Linux uses `flock(1)` (auto-released by the kernel on process death); macOS, where `flock` is not in base, uses an atomic `mkdir .lock.d` fallback that is released by the script's `EXIT` trap. If a script is killed with `kill -9` between `mkdir` and the trap, the mutex dir can be left behind — delete it manually under `$MM_BUILD_CACHE_DIR//`. Override retention with `BUILD_CACHE_RETAIN=N` (default 5 per platform).
+Cache lives in `$MM_BUILD_CACHE_DIR` (default `~/Library/Caches/mm-mobile-builds` on macOS, `~/.cache/mm-mobile-builds` on Linux), keyed by an agentic `@expo/fingerprint` hash computed by `scripts/perps/agentic/lib/compute-cache-fp.js`. The agentic fingerprint *extends* the project-wide `fingerprint.config.js` (which EAS Build and OTA still consume unchanged) with additional `ignorePaths` for per-worktree build artifacts that don't influence binary semantics (`ios/build/`, `.gradle/`, Xcode `xcuserdata`, NDK `.cxx`, etc.). Binary-affecting inputs — env-populated `xcconfig`, `google-services.json`, and the bundled `InpageBridgeWeb3.js` — stay hashed, so the cache only converges across worktrees when those inputs match. Parallel worktrees at the same fingerprint share one artifact through a per-fingerprint mutex: Linux uses `flock(1)` (auto-released by the kernel on process death); macOS, where `flock` is not in base, uses an atomic `mkdir .lock.d` fallback that is released by the script's `EXIT` trap. If a script is killed with `kill -9` between `mkdir` and the trap, the mutex dir can be left behind — delete it manually under `$MM_BUILD_CACHE_DIR//`. Override retention with `BUILD_CACHE_RETAIN=N` (default 5 per platform).
Invoke directly:
diff --git a/scripts/perps/agentic/lib/build-cache.sh b/scripts/perps/agentic/lib/build-cache.sh
index 839288390b50..397d552ee288 100644
--- a/scripts/perps/agentic/lib/build-cache.sh
+++ b/scripts/perps/agentic/lib/build-cache.sh
@@ -78,7 +78,10 @@ bc_fingerprint() {
fi
fi
local fp
- fp=$(node scripts/generate-fingerprint.js 2>/dev/null || true)
+ # Use the agentic fingerprint. It extends the project's fingerprint.config.js
+ # (so EAS/OTA inputs still participate) with additional ignorePaths for
+ # per-worktree build outputs. See compute-cache-fp.js for the rationale.
+ fp=$(node scripts/perps/agentic/lib/compute-cache-fp.js 2>/dev/null || true)
if [ -z "$fp" ]; then
return 1
fi
diff --git a/scripts/perps/agentic/lib/compute-cache-fp.js b/scripts/perps/agentic/lib/compute-cache-fp.js
new file mode 100644
index 000000000000..6296495021c3
--- /dev/null
+++ b/scripts/perps/agentic/lib/compute-cache-fp.js
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+// compute-cache-fp.js — agentic-local native-build fingerprint for the
+// shared build cache.
+//
+// Relation to the project fingerprint:
+// The repo-wide `scripts/generate-fingerprint.js` is consumed by EAS Build,
+// EAS Update, and the OTA fingerprint guard in
+// `docs/nightly-ota-updates.md`. Its `fingerprint.config.js` deliberately
+// errs on the side of hashing too much — every local build artifact that
+// could conceivably influence the produced binary participates in the key
+// so a hash collision can never reuse a build whose inputs we cannot
+// vouch for.
+//
+// `@expo/fingerprint`'s `createFingerprintAsync(projectRoot, options)`
+// loads `fingerprint.config.js` and applies caller options with these
+// semantics (per `@expo/fingerprint` 0.15.x):
+// - `extraSources`: caller OVERRIDES the config's list when set.
+// - `ignorePaths`: caller is MERGED with the config's list.
+// To stay in sync with future edits to `fingerprint.config.js`, we
+// `require()` it directly and spread its lists into our options. Our
+// added ignorePaths cover per-worktree dev/build artifacts that don't
+// affect binary semantics (compile outputs, IDE state, NDK cache,
+// per-machine `.xcode.env.local`).
+// Binary-affecting inputs — env-populated xcconfig, `google-services.json`,
+// the bridge source — stay hashed. The cache only converges across
+// worktrees when those inputs match, which is the correct behaviour.
+
+const fp = require('@expo/fingerprint');
+// Import the project's config so future additions to its extraSources
+// automatically flow into the agentic fingerprint. Using require here
+// (vs. literally copying the list) means a new entry in
+// `fingerprint.config.js` cannot silently leave the agentic cache
+// behind.
+const projectConfig = require('../../../../fingerprint.config.js');
+
+const options = {
+ // Inherit the project's extraSources and append the runtime JS bridge
+ // source. The bridge is copied into android/ios assets at build time
+ // and embedded in the .jsbundle, so its content affects binary output.
+ extraSources: [
+ ...(projectConfig.extraSources || []),
+ {
+ type: 'file',
+ filePath: 'app/core/InpageBridgeWeb3.js',
+ reasons: ['Bundled into the runtime JS — affects binary behaviour.'],
+ },
+ ],
+ // Per-worktree dev/build state that does not influence the produced
+ // binary. All are gitignored and regenerated locally; ignoring them
+ // lets two slots on the same commit with the same source env share a
+ // cached `.app`/`.apk`.
+ ignorePaths: [
+ 'ios/build/**',
+ 'ios/.xcode.env.local',
+ 'ios/MetaMask.xcworkspace/xcshareddata/swiftpm/**',
+ 'ios/**/xcuserdata/**',
+ 'android/.gradle/**',
+ 'android/app/.cxx/**',
+ 'android/app/build/**',
+ // Mirror of app/core/InpageBridgeWeb3.js — already tracked via the
+ // extraSources entry above; ignore the generated copy so we don't
+ // double-count it on rebuild.
+ 'android/app/src/main/assets/InpageBridgeWeb3.js',
+ ],
+};
+
+fp.createFingerprintAsync(process.cwd(), options)
+ .then(({ hash }) => {
+ process.stdout.write(hash);
+ })
+ .catch((err) => {
+ process.stderr.write(`compute-cache-fp: ${err.message}\n`);
+ process.exit(1);
+ });
diff --git a/scripts/perps/agentic/lib/test-build-cache.sh b/scripts/perps/agentic/lib/test-build-cache.sh
index 338c31f80807..8608f21f0286 100644
--- a/scripts/perps/agentic/lib/test-build-cache.sh
+++ b/scripts/perps/agentic/lib/test-build-cache.sh
@@ -184,6 +184,74 @@ echo "$out" | grep -qE "Mode:.*clean.*yarn setup" && pass "clean mode header ren
out=$(_capture_for 10 bash scripts/perps/agentic/preflight.sh --clean --check-only 2>&1 | head -20 || true)
echo "$out" | grep -qE "Mode:.*clean.*yarn setup" && pass "legacy --clean still maps to clean" || fail "legacy --clean broken"
+# ─── 10b. Agentic fp respects the safe/unsafe ignorePath boundary ──
+# compute-cache-fp.js ignores per-worktree build outputs but MUST keep
+# binary-affecting inputs (xcconfig, google-services.json, the bundled
+# InpageBridgeWeb3 source) hashed. Verify both halves of that contract.
+hdr "agentic fp respects ignorePath boundary"
+
+_capture_fp() {
+ bc_memo_cleanup 2>/dev/null || true
+ bc_memo_init
+ bc_fingerprint 2>/dev/null
+}
+
+FP_BASELINE=$(_capture_fp)
+[ -n "$FP_BASELINE" ] && pass "baseline fp computed: ${FP_BASELINE:0:12}" \
+ || fail "baseline fp empty"
+
+# (a) Poisoning an IGNORED path must NOT change fp.
+mkdir -p ios/build
+POISON_IGNORED="ios/build/__bc_test_poison_$$.bin"
+echo "poison-$RANDOM" > "$POISON_IGNORED"
+FP_IGNORED=$(_capture_fp)
+rm -f "$POISON_IGNORED"
+if [ "$FP_BASELINE" = "$FP_IGNORED" ]; then
+ pass "ignored ios/build/ poison did NOT shift fp"
+else
+ fail "ignored ios/build/ poison SHIFTED fp (drift): $FP_BASELINE -> $FP_IGNORED"
+fi
+
+# Restore-trapped poison helper: backs up the file, layers a temporary
+# EXIT trap that restores the file AND re-invokes the suite-level cleanup,
+# then restores the original suite-level trap before returning. Ensures
+# .agent/build-cache cleanup still runs on early abort inside the helper.
+_poison_must_shift_fp() {
+ local label="$1" path="$2"
+ if [ ! -f "$path" ]; then
+ fail "missing $path — cannot run boundary test"
+ return
+ fi
+ local bak="/tmp/__bc_test_$(basename "$path")_$$.bak"
+ cp "$path" "$bak"
+ # Capture the suite-level EXIT trap so we can re-install it after.
+ local prev_trap
+ prev_trap=$(trap -p EXIT)
+ trap "cp '$bak' '$path' 2>/dev/null; rm -f '$bak' 2>/dev/null; cleanup" EXIT
+ echo "// __bc_test_poison_$$ $RANDOM" >> "$path"
+ local fp_after
+ fp_after=$(_capture_fp)
+ cp "$bak" "$path"
+ rm -f "$bak"
+ # Restore the suite-level cleanup trap.
+ eval "${prev_trap:-trap - EXIT}"
+ if [ "$fp_after" != "$FP_BASELINE" ]; then
+ pass "$label DID shift fp (${fp_after:0:12})"
+ else
+ fail "$label was silently ignored — cache could serve stale binary"
+ fi
+}
+
+# (b) Poisoning a HASHED, binary-affecting path MUST change fp.
+_poison_must_shift_fp "InpageBridgeWeb3.js (bridge source)" "app/core/InpageBridgeWeb3.js"
+
+# (c) Poisoning an inherited project extraSource MUST change fp — proves
+# the script repeats fingerprint.config.js extraSources correctly.
+_poison_must_shift_fp "scripts/setup.mjs (project extraSource)" "scripts/setup.mjs"
+
+# Restore baseline state for the rest of the suite.
+_capture_fp >/dev/null
+
# ─── 11. Memo cleanup refuses inherited / unowned BC_MEMO_DIR ──────
# Across R6/R7/R8/R9 codex flagged five attack shapes against the memo
# directory cleanup. Each scenario sets up a "victim" dir, hands its path
@@ -215,7 +283,7 @@ _memo_attack "R9B: EXIT cleanup on inherited memo" ""
# Codex R2 B3: --mode fast must hard-fail if the fingerprint command can't
# run, instead of silently falling through to the legacy build path.
hdr "preflight --mode fast / fingerprint failure"
-FP_SCRIPT="scripts/generate-fingerprint.js"
+FP_SCRIPT="scripts/perps/agentic/lib/compute-cache-fp.js"
FP_BACKUP="${FP_SCRIPT}.test-bak-$$"
mv "$FP_SCRIPT" "$FP_BACKUP"
restore_fp() { [ -f "$FP_BACKUP" ] && mv "$FP_BACKUP" "$FP_SCRIPT" 2>/dev/null || true; }