From 4799a2f2166fb5eece3ca413078297dbdbca5f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Fri, 20 Feb 2026 00:23:29 +0000 Subject: [PATCH 001/131] fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a47a4adbf4..7f7f648bd64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - - fix: check chainRanking against ALLOWED_BRIDGE_CHAIN_IDS (#25808) +- fix: check chainRanking against ALLOWED_BRIDGE_CHAIN_IDS (#25808) ## [7.64.0] From 5b312f88a9bb590b1a37ad6eb6962fa57d5d45aa Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 01:54:56 +0000 Subject: [PATCH 002/131] Cherry-picking commits from main to release/7.67.0 for PR #26292 (#26316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refactor(analytics): migrate Batch 2-11: metamask-earn (#26292) ## **Description** Phase 2 analytics migration (Batch 2-11): migrate Stake/Earn's `useMetrics` hook and `MetaMetrics.getInstance()` calls from the legacy MetaMetrics system to the new analytics system. **Reason**: Deprecate MetaMetrics in favour of the shared analytics utility and AnalyticsController. **Changes**: Stake components and hooks now use `useAnalytics` from `hooks/useAnalytics/useAnalytics` and import `MetaMetricsEvents` directly from `core/Analytics`; `withMetaMetrics` utility now uses `analytics.trackEvent()` and `AnalyticsEventBuilder` instead of `MetaMetrics.getInstance().trackEvent()` and `MetricsEventBuilder`; test mocks updated accordingly. ### Changes **Source files (6)**: - `LearnMoreModalFooter.tsx`, `StakingButtons.tsx`, `StakingBalance.tsx`, `StakeButton/index.tsx`: replaced `useMetrics` with `useAnalytics`; `MetaMetricsEvents` now imported from `core/Analytics` - `usePoolStakedDeposit/index.ts`: replaced `useMetrics` with `useAnalytics` (also migrated as it's the source for a listed test file) - `withMetaMetrics.ts`: replaced `MetaMetrics.getInstance().trackEvent()` with `analytics.trackEvent()` and `MetricsEventBuilder` with `AnalyticsEventBuilder` **Test files (3)**: - `StakeButton.test.tsx`: replaced `useMetrics` mock with `useAnalytics` mock; added transitive `useMetrics` mock for unmigrated `useStablecoinLendingRedirect` dependency; replaced `MetricsEventBuilder` with `AnalyticsEventBuilder` - `usePoolStakedDeposit.test.tsx`: replaced `useMetrics` mock/import with `useAnalytics`; replaced `MetricsEventBuilder` with `AnalyticsEventBuilder` - `withMetaMetrics.test.ts`: replaced `MetaMetrics.getInstance()` spy with `analytics` module mock; updated `MetaMetricsEvents` import from `core/Analytics` ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MCWP-298 (Batch 2-11) ## **Manual testing steps** ```gherkin Feature: Stake/Earn analytics Scenario: user triggers a stake/earn flow event Given app is open and user is in a stake/earn flow When user performs an action that triggers analytics (e.g. stake button, unstake button, learn more, view staked positions) Then the event is tracked on Mixpanel ``` ## **Screenshots/Recordings** N/A – analytics migration, no UI change. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > This is a broad analytics plumbing migration across Stake/Earn UI and transaction hooks; risk is mainly missed/changed event emission or metadata due to swapped builders and tracking surfaces, not user-facing flow changes. > > **Overview** > Migrates Stake/Earn (Earn CTA, staking balance/buttons, learn-more modal, and pooled-stake deposit hook) from legacy `useMetrics`/`MetaMetrics.getInstance()` tracking to the new analytics stack via `useAnalytics`, `analytics.trackEvent`, and `AnalyticsEventBuilder`. > > Updates related unit tests to mock `useAnalytics`/`analytics` instead of `useMetrics`/MetaMetrics, including a new mock for `useStablecoinLendingRedirect` to avoid transitive legacy metrics behavior and keep navigation assertions stable. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8971698510897209f67205d212ef19ad36c8bab8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [b5b8c71](https://github.com/MetaMask/metamask-mobile/commit/b5b8c710c60822e1f33ec2831a8843ec69d27523) Co-authored-by: Nico MASSART --- .../LearnMoreModal/LearnMoreModalFooter.tsx | 5 ++-- .../StakeButton/StakeButton.test.tsx | 23 ++++++++++++++----- .../UI/Stake/components/StakeButton/index.tsx | 5 ++-- .../StakingBalance/StakingBalance.test.tsx | 17 ++++++++++++++ .../StakingBalance/StakingBalance.tsx | 5 ++-- .../StakingButtons/StakingButtons.test.tsx | 10 ++------ .../StakingButtons/StakingButtons.tsx | 5 ++-- .../Stake/hooks/usePoolStakedDeposit/index.ts | 9 ++++---- .../usePoolStakedDeposit.test.tsx | 14 +++++------ .../utils/metaMetrics/withMetaMetrics.test.ts | 21 +++++++++-------- .../utils/metaMetrics/withMetaMetrics.ts | 14 +++++------ 11 files changed, 78 insertions(+), 50 deletions(-) diff --git a/app/components/UI/Stake/components/LearnMoreModal/LearnMoreModalFooter.tsx b/app/components/UI/Stake/components/LearnMoreModal/LearnMoreModalFooter.tsx index 0a8186e8526..02f6c9542ae 100644 --- a/app/components/UI/Stake/components/LearnMoreModal/LearnMoreModalFooter.tsx +++ b/app/components/UI/Stake/components/LearnMoreModal/LearnMoreModalFooter.tsx @@ -13,7 +13,8 @@ import Text, { TextColor, TextVariant, } from '../../../../../component-library/components/Texts/Text'; -import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../../../core/Analytics'; +import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; import { strings } from '../../../../../../locales/i18n'; import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; @@ -29,7 +30,7 @@ export const LearnMoreModalFooter = ({ style, }: LearnMoreModalFooterProps) => { const { navigate } = useNavigation(); - const { trackEvent, createEventBuilder } = useMetrics(); + const { trackEvent, createEventBuilder } = useAnalytics(); const redirectToLearnMore = () => { navigate('Webview', { diff --git a/app/components/UI/Stake/components/StakeButton/StakeButton.test.tsx b/app/components/UI/Stake/components/StakeButton/StakeButton.test.tsx index 3f5e0bb0b3d..ead5075c2c4 100644 --- a/app/components/UI/Stake/components/StakeButton/StakeButton.test.tsx +++ b/app/components/UI/Stake/components/StakeButton/StakeButton.test.tsx @@ -8,8 +8,8 @@ import { MOCK_ETH_MAINNET_ASSET, MOCK_USDC_MAINNET_ASSET, } from '../../__mocks__/stakeMockData'; -import { useMetrics } from '../../../../hooks/useMetrics'; -import { MetricsEventBuilder } from '../../../../../core/Analytics/MetricsEventBuilder'; +import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; +import { AnalyticsEventBuilder } from '../../../../../util/analytics/AnalyticsEventBuilder'; import { mockNetworkState } from '../../../../../util/test/network'; import useStakingEligibility from '../../hooks/useStakingEligibility'; import { RootState } from '../../../../../reducers'; @@ -35,7 +35,18 @@ jest.mock('@react-navigation/native', () => { }; }); -jest.mock('../../../../hooks/useMetrics'); +jest.mock('../../../../hooks/useAnalytics/useAnalytics'); + +jest.mock('../../../Earn/hooks/useStablecoinLendingRedirect', () => ({ + useStablecoinLendingRedirect: jest.fn(({ asset }: Record) => + jest.fn(() => { + mockNavigate('StakeScreens', { + screen: 'Stake', + params: { token: asset }, + }); + }), + ), +})); jest.mock('../../../../hooks/useBuildPortfolioUrl', () => ({ useBuildPortfolioUrl: jest.fn(() => (baseUrl: string) => { @@ -79,9 +90,9 @@ jest.mock('../../../../../selectors/earnController/earn', () => ({ }, })); -(useMetrics as jest.MockedFn).mockReturnValue({ +(useAnalytics as jest.MockedFn).mockReturnValue({ trackEvent: jest.fn(), - createEventBuilder: MetricsEventBuilder.createEventBuilder, + createEventBuilder: AnalyticsEventBuilder.createEventBuilder, enable: jest.fn(), addTraitsToUser: jest.fn(), createDataDeletionTask: jest.fn(), @@ -90,7 +101,7 @@ jest.mock('../../../../../selectors/earnController/earn', () => ({ getDeleteRegulationId: jest.fn(), isDataRecorded: jest.fn(), isEnabled: jest.fn(), - getMetaMetricsId: jest.fn(), + getAnalyticsId: jest.fn(), }); jest.mock('../../../../../core/Engine', () => ({ diff --git a/app/components/UI/Stake/components/StakeButton/index.tsx b/app/components/UI/Stake/components/StakeButton/index.tsx index 9477de9979c..77c2569a0e9 100644 --- a/app/components/UI/Stake/components/StakeButton/index.tsx +++ b/app/components/UI/Stake/components/StakeButton/index.tsx @@ -17,7 +17,8 @@ import { selectNetworkConfigurationByChainId, } from '../../../../../selectors/networkController'; import { getDecimalChainId } from '../../../../../util/networks'; -import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../../../core/Analytics'; +import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; import { EARN_EXPERIENCES } from '../../../Earn/constants/experiences'; import { selectPooledStakingEnabledFlag, @@ -51,7 +52,7 @@ interface StakeButtonContentProps { const StakeButtonContent = ({ earnToken }: StakeButtonContentProps) => { const { styles } = useStyles(styleSheet, {}); const navigation = useNavigation(); - const { trackEvent, createEventBuilder } = useMetrics(); + const { trackEvent, createEventBuilder } = useAnalytics(); const chainId = useSelector(selectEvmChainId); const { isStakingSupportedChain } = useStakingChain(); diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx index 724070a3ea8..dc3f9e1c01a 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx @@ -24,6 +24,8 @@ import { EARN_EXPERIENCES } from '../../../Earn/constants/experiences'; import { selectPooledStakingEnabledFlag } from '../../../Earn/selectors/featureFlags'; import { TokenI } from '../../../Tokens/types'; import useStakingEligibility from '../../hooks/useStakingEligibility'; +import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; +import { AnalyticsEventBuilder } from '../../../../../util/analytics/AnalyticsEventBuilder'; const mockEarnTokenPair = getMockUseEarnTokens(EARN_EXPERIENCES.POOLED_STAKING); jest.mock('../../../Earn/hooks/useEarnings', () => ({ @@ -88,6 +90,8 @@ const MOCK_APR_VALUES: { [symbol: string]: string } = { DAI: '5.0', }; +jest.mock('../../../../hooks/useAnalytics/useAnalytics'); + jest.mock('../../../../hooks/useIpfsGateway', () => jest.fn()); Image.getSize = jest @@ -191,6 +195,19 @@ afterEach(() => { describe('StakingBalance', () => { beforeEach(() => { jest.resetAllMocks(); + (useAnalytics as jest.MockedFn).mockReturnValue({ + trackEvent: jest.fn(), + createEventBuilder: AnalyticsEventBuilder.createEventBuilder, + enable: jest.fn(), + addTraitsToUser: jest.fn(), + createDataDeletionTask: jest.fn(), + checkDataDeleteStatus: jest.fn(), + getDeleteRegulationCreationDate: jest.fn(), + getDeleteRegulationId: jest.fn(), + isDataRecorded: jest.fn(), + isEnabled: jest.fn(), + getAnalyticsId: jest.fn(), + }); mockUseStakingEligibility.mockReturnValue({ isEligible: true, isLoadingEligibility: false, diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx index 67fce0ecc6e..6eb4fe4a66e 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx @@ -20,7 +20,8 @@ import { RootState } from '../../../../../reducers'; import { selectNetworkConfigurationByChainId } from '../../../../../selectors/networkController'; import { getTimeDifferenceFromNow } from '../../../../../util/date'; import { getDecimalChainId } from '../../../../../util/networks'; -import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../../../core/Analytics'; +import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; import AssetElement from '../../../AssetElement'; import { NetworkBadgeSource } from '../../../AssetOverview/Balance/Balance'; import NetworkAssetLogo from '../../../NetworkAssetLogo'; @@ -74,7 +75,7 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { asset.chainId as Hex, ); - const { trackEvent, createEventBuilder } = useMetrics(); + const { trackEvent, createEventBuilder } = useAnalytics(); const decimalChainId = getDecimalChainId(asset.chainId); const { diff --git a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.test.tsx b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.test.tsx index a59dfdd1349..951753c0cee 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.test.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.test.tsx @@ -27,14 +27,8 @@ import { getMockUseEarnTokens } from '../../../../Earn/__mocks__/earnMockData'; const mockEarnTokenPair = getMockUseEarnTokens(EARN_EXPERIENCES.POOLED_STAKING); -// Prevent `useMetrics` from triggering async Engine readiness polling (`whenEngineReady`) -// which can cause Jest timeouts / "import after environment torn down" errors. -jest.mock('../../../../../hooks/useMetrics', () => ({ - MetaMetricsEvents: { - STAKE_BUTTON_CLICKED: 'STAKE_BUTTON_CLICKED', - STAKE_WITHDRAW_BUTTON_CLICKED: 'STAKE_WITHDRAW_BUTTON_CLICKED', - }, - useMetrics: () => ({ +jest.mock('../../../../../hooks/useAnalytics/useAnalytics', () => ({ + useAnalytics: () => ({ trackEvent: jest.fn(), createEventBuilder: () => ({ addProperties: jest.fn().mockReturnThis(), diff --git a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx index c8121c3d6e7..835fff6f656 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx @@ -12,7 +12,8 @@ import Engine from '../../../../../../core/Engine'; import { RootState } from '../../../../../../reducers'; import { earnSelectors } from '../../../../../../selectors/earnController'; import { selectEvmChainId } from '../../../../../../selectors/networkController'; -import { MetaMetricsEvents, useMetrics } from '../../../../../hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../../../../core/Analytics'; +import { useAnalytics } from '../../../../../hooks/useAnalytics/useAnalytics'; import { selectPooledStakingEnabledFlag } from '../../../../Earn/selectors/featureFlags'; import { TokenI } from '../../../../Tokens/types'; import { EVENT_LOCATIONS } from '../../../constants/events'; @@ -37,7 +38,7 @@ const StakingButtons = ({ const { styles } = useStyles(styleSheet, {}); - const { trackEvent, createEventBuilder } = useMetrics(); + const { trackEvent, createEventBuilder } = useAnalytics(); const { isEligible } = useStakingEligibility(); diff --git a/app/components/UI/Stake/hooks/usePoolStakedDeposit/index.ts b/app/components/UI/Stake/hooks/usePoolStakedDeposit/index.ts index d5b75a72738..c13c42f3538 100644 --- a/app/components/UI/Stake/hooks/usePoolStakedDeposit/index.ts +++ b/app/components/UI/Stake/hooks/usePoolStakedDeposit/index.ts @@ -10,7 +10,8 @@ import { formatEther } from 'ethers/lib/utils'; import { NetworkClientId } from '@metamask/network-controller'; import { addTransaction } from '../../../../../util/transaction-controller'; import trackErrorAsAnalytics from '../../../../../util/metrics/TrackError/trackErrorAsAnalytics'; -import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../../../core/Analytics'; +import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; import { Stake } from '../../sdk/stakeSdkProvider'; import { EVENT_PROVIDERS } from '../../constants/events'; import { useStakeContext } from '../useStakeContext'; @@ -35,8 +36,8 @@ const attemptDepositTransaction = ( pooledStakingContract: PooledStakingContract, networkClientId: NetworkClientId, - trackEvent: ReturnType['trackEvent'], - createEventBuilder: ReturnType['createEventBuilder'], + trackEvent: ReturnType['trackEvent'], + createEventBuilder: ReturnType['createEventBuilder'], ) => async ( depositValueWei: string, @@ -97,7 +98,7 @@ const attemptDepositTransaction = const usePoolStakedDeposit = () => { const { networkClientId, stakingContract } = useStakeContext() as Required; - const { trackEvent, createEventBuilder } = useMetrics(); + const { trackEvent, createEventBuilder } = useAnalytics(); // Linter is complaining that function may use other dependencies // We will simply ignore since we don't want to use inline function diff --git a/app/components/UI/Stake/hooks/usePoolStakedDeposit/usePoolStakedDeposit.test.tsx b/app/components/UI/Stake/hooks/usePoolStakedDeposit/usePoolStakedDeposit.test.tsx index 77487d3994a..72788a78abf 100644 --- a/app/components/UI/Stake/hooks/usePoolStakedDeposit/usePoolStakedDeposit.test.tsx +++ b/app/components/UI/Stake/hooks/usePoolStakedDeposit/usePoolStakedDeposit.test.tsx @@ -1,11 +1,11 @@ import { toHex } from '@metamask/controller-utils'; import { ChainId, PooledStakingContract } from '@metamask/stake-sdk'; import { Contract } from 'ethers'; -import { MetricsEventBuilder } from '../../../../../core/Analytics/MetricsEventBuilder'; +import { AnalyticsEventBuilder } from '../../../../../util/analytics/AnalyticsEventBuilder'; import { createMockAccountsControllerState } from '../../../../../util/test/accountsControllerTestUtils'; import { backgroundState } from '../../../../../util/test/initial-root-state'; import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; -import useMetrics from '../../../../hooks/useMetrics/useMetrics'; +import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; import { EVENT_PROVIDERS } from '../../constants/events'; import { Stake } from '../../sdk/stakeSdkProvider'; import usePoolStakedDeposit from './index'; @@ -96,17 +96,17 @@ jest.mock('../useStakeContext', () => ({ useStakeContext: () => mockSdkContext, })); -jest.mock('../../../../hooks/useMetrics/useMetrics'); +jest.mock('../../../../hooks/useAnalytics/useAnalytics'); describe('usePoolStakedDeposit', () => { const mockTrackEvent = jest.fn(); - const useMetricsMock = jest.mocked(useMetrics); + const useAnalyticsMock = jest.mocked(useAnalytics); beforeEach(() => { - useMetricsMock.mockReturnValue({ + useAnalyticsMock.mockReturnValue({ trackEvent: mockTrackEvent, - createEventBuilder: MetricsEventBuilder.createEventBuilder, - } as unknown as ReturnType); + createEventBuilder: AnalyticsEventBuilder.createEventBuilder, + } as unknown as ReturnType); }); afterEach(() => { diff --git a/app/components/UI/Stake/utils/metaMetrics/withMetaMetrics.test.ts b/app/components/UI/Stake/utils/metaMetrics/withMetaMetrics.test.ts index 3fd30dd5e99..b28110f2770 100644 --- a/app/components/UI/Stake/utils/metaMetrics/withMetaMetrics.test.ts +++ b/app/components/UI/Stake/utils/metaMetrics/withMetaMetrics.test.ts @@ -1,18 +1,21 @@ import { withMetaMetrics } from './withMetaMetrics'; -import { MetaMetrics } from '../../../../../core/Analytics'; -import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../../../core/Analytics'; +import { analytics } from '../../../../../util/analytics/analytics'; import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events'; -describe('withMetaMetrics', () => { - let trackEventSpy: jest.SpyInstance; +jest.mock('../../../../../util/analytics/analytics', () => ({ + analytics: { + trackEvent: jest.fn(), + }, +})); +describe('withMetaMetrics', () => { const MOCK_HANDLER_RESULT = 123; const mockHandler = () => MOCK_HANDLER_RESULT; const mockAsyncHandler = async () => MOCK_HANDLER_RESULT; beforeEach(() => { jest.resetAllMocks(); - trackEventSpy = jest.spyOn(MetaMetrics.getInstance(), 'trackEvent'); }); it('fires single event when wrapping sync function', () => { @@ -26,7 +29,7 @@ describe('withMetaMetrics', () => { const result = mockHandlerWithMetaMetrics(); expect(result).toEqual(MOCK_HANDLER_RESULT); - expect(trackEventSpy).toHaveBeenCalledTimes(1); + expect(analytics.trackEvent).toHaveBeenCalledTimes(1); }); it('fires array of events when wrapping sync function', () => { @@ -53,7 +56,7 @@ describe('withMetaMetrics', () => { const result = mockHandlerWithMetaMetrics(); expect(result).toEqual(MOCK_HANDLER_RESULT); - expect(trackEventSpy).toHaveBeenCalledTimes(2); + expect(analytics.trackEvent).toHaveBeenCalledTimes(2); }); it('fires single event when wrapping async function', async () => { @@ -67,7 +70,7 @@ describe('withMetaMetrics', () => { const result = await mockAsyncHandlerWithMetaMetrics(); expect(result).toEqual(MOCK_HANDLER_RESULT); - expect(trackEventSpy).toHaveBeenCalledTimes(1); + expect(analytics.trackEvent).toHaveBeenCalledTimes(1); }); it('fires all events when wrapping async function', async () => { @@ -95,6 +98,6 @@ describe('withMetaMetrics', () => { const result = await mockAsyncHandlerWithMetaMetrics(); expect(result).toEqual(MOCK_HANDLER_RESULT); - expect(trackEventSpy).toHaveBeenCalledTimes(2); + expect(analytics.trackEvent).toHaveBeenCalledTimes(2); }); }); diff --git a/app/components/UI/Stake/utils/metaMetrics/withMetaMetrics.ts b/app/components/UI/Stake/utils/metaMetrics/withMetaMetrics.ts index 13fe2a7e84a..ba3813cc870 100644 --- a/app/components/UI/Stake/utils/metaMetrics/withMetaMetrics.ts +++ b/app/components/UI/Stake/utils/metaMetrics/withMetaMetrics.ts @@ -1,16 +1,16 @@ -import { +import type { IMetaMetricsEvent, JsonMap, } from '../../../../../core/Analytics/MetaMetrics.types'; -import { MetricsEventBuilder } from '../../../../../core/Analytics/MetricsEventBuilder'; -import { MetaMetrics } from '../../../../../core/Analytics'; +import { AnalyticsEventBuilder } from '../../../../../util/analytics/AnalyticsEventBuilder'; +import { analytics } from '../../../../../util/analytics/analytics'; export interface WithMetaMetricsEvent { event: IMetaMetricsEvent; properties?: JsonMap; } -const createEventBuilder = MetricsEventBuilder.createEventBuilder; +const createEventBuilder = AnalyticsEventBuilder.createEventBuilder; const shouldAddProperties = (properties?: JsonMap): properties is JsonMap => { if (!properties) return false; @@ -43,14 +43,12 @@ export const withMetaMetrics = any>( if (result instanceof Promise) { return result.then((res) => { - builtEvents.forEach((event) => - MetaMetrics.getInstance().trackEvent(event), - ); + builtEvents.forEach((event) => analytics.trackEvent(event)); return res; }) as Promise>; } - builtEvents.forEach((event) => MetaMetrics.getInstance().trackEvent(event)); + builtEvents.forEach((event) => analytics.trackEvent(event)); return result as ReturnType; }; From 9f71945aae98f6729c95c17f112d81cf5736fdf8 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Fri, 20 Feb 2026 01:59:59 +0000 Subject: [PATCH 003/131] [skip ci] Bump version number to 3764 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 76a0e520d55..bbb8e985b1b 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.67.0" - versionCode 3607 + versionCode 3764 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index dbb2aa0b3bc..2b41bd3fb49 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3607 + VERSION_NUMBER: 3764 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3607 + FLASK_VERSION_NUMBER: 3764 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 528c2c4320b..0b16ca98adb 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3764; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3764; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3764; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3764; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3764; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3764; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From a96cf72d3fa5ed3b030f7ae54435ad865016e802 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:44:24 +0000 Subject: [PATCH 004/131] Cherry-picking commits from main to release/7.67.0 for PR #26312 (#26352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refactor(analytics): migrate Batch 3-1: mobile-platform (#26312) ## **Description** Phase 3 analytics migration (Batch 3-1): migrate Authentication core's `Authentication.ts` from `MetaMetrics.getInstance()` to the new analytics system. **Reason**: Deprecate MetaMetrics in favour of the shared analytics utility and AnalyticsController. **Changes**: `Authentication.ts` now uses `analytics.isEnabled()` from `app/util/analytics/analytics` instead of `MetaMetrics.getInstance().isEnabled()`; test mocks updated to mock the analytics utility instead of MetaMetrics. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MCWP-301 (Batch 3-1) ## **Manual testing steps** ```gherkin Feature: Authentication analytics Scenario: user triggers an authentication flow event Given app is open and user is in an authentication flow When user performs an action that triggers analytics (e.g. unlock wallet, login) Then the event is tracked on Mixpanel ``` ## **Screenshots/Recordings** N/A – analytics migration, no UI change. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Small refactor limited to the metrics-consent check after unlocking the wallet; main risk is a regression in opt-in navigation if `analytics.isEnabled()` differs from the old `MetaMetrics` behavior. > > **Overview** > `Authentication.unlockWallet` now uses the shared `analytics.isEnabled()` helper (from `util/analytics/analytics`) instead of `MetaMetrics.getInstance().isEnabled()` when deciding whether to route users to the metrics opt-in screen after login. > > Unit tests were updated to mock and spy on the new `analytics` helper rather than `MetaMetrics`, keeping the same enabled/disabled behavior expectations. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a7de545e365c298b48da0fa3e41a24f9b50a0e82. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [e4effa3](https://github.com/MetaMask/metamask-mobile/commit/e4effa32d77064b541ac979d448d6fb294dcfdcf) Co-authored-by: Nico MASSART --- .../Authentication/Authentication.test.ts | 50 +++++-------------- app/core/Authentication/Authentication.ts | 4 +- 2 files changed, 15 insertions(+), 39 deletions(-) diff --git a/app/core/Authentication/Authentication.test.ts b/app/core/Authentication/Authentication.test.ts index a67c5dabacf..5ea4f211780 100644 --- a/app/core/Authentication/Authentication.test.ts +++ b/app/core/Authentication/Authentication.test.ts @@ -47,7 +47,7 @@ import { SeedlessOnboardingControllerErrorType, } from '../Engine/controllers/seedless-onboarding-controller/error'; import { TraceName, TraceOperation } from '../../util/trace'; -import MetaMetrics from '../Analytics/MetaMetrics'; +import { analytics } from '../../util/analytics/analytics'; import { resetProviderToken as depositResetProviderToken } from '../../components/UI/Ramp/Deposit/utils/ProviderTokenVault'; import { clearAllVaultBackups } from '../BackupVault/backupVault'; import { Engine as EngineClass } from '../Engine/Engine'; @@ -230,15 +230,11 @@ jest.mock('../BackupVault/backupVault', () => ({ clearAllVaultBackups: jest.fn(), })); -jest.mock('../Analytics/MetaMetrics', () => { - const mockInstance = {}; - return { - __esModule: true, - default: { - getInstance: jest.fn(() => mockInstance), - }, - }; -}); +jest.mock('../../util/analytics/analytics', () => ({ + analytics: { + isEnabled: jest.fn().mockReturnValue(true), + }, +})); jest.mock('../../util/analytics/analyticsDataDeletion', () => ({ createDataDeletionTask: jest.fn().mockResolvedValue(undefined), @@ -1309,9 +1305,7 @@ describe('Authentication', () => { }), } as unknown as ReduxStore); - jest - .spyOn(MetaMetrics, 'getInstance') - .mockReturnValue({ isEnabled: () => true } as MetaMetrics); + jest.spyOn(analytics, 'isEnabled').mockReturnValue(true); await Authentication.unlockWallet(); @@ -1344,9 +1338,7 @@ describe('Authentication', () => { }), } as unknown as ReduxStore); - jest - .spyOn(MetaMetrics, 'getInstance') - .mockReturnValue({ isEnabled: () => true } as MetaMetrics); + jest.spyOn(analytics, 'isEnabled').mockReturnValue(true); await Authentication.unlockWallet(); @@ -1695,10 +1687,7 @@ describe('Authentication', () => { }), } as unknown as ReduxStore); - // Mock MetaMetrics.getInstance to return true for isEnabled - jest - .spyOn(MetaMetrics, 'getInstance') - .mockReturnValue({ isEnabled: () => true } as MetaMetrics); + jest.spyOn(analytics, 'isEnabled').mockReturnValue(true); const mockKeyring = { getAccounts: jest.fn().mockResolvedValue(['0x1234567890abcdef']), @@ -2142,10 +2131,7 @@ describe('Authentication', () => { getState: jest.fn(() => mockState), } as unknown as ReduxStore); - // Mock MetaMetrics.getInstance to return true for isEnabled - jest - .spyOn(MetaMetrics, 'getInstance') - .mockReturnValue({ isEnabled: () => true } as MetaMetrics); + jest.spyOn(analytics, 'isEnabled').mockReturnValue(true); Engine.context.SeedlessOnboardingController = { state: { vault: {} }, @@ -2632,10 +2618,7 @@ describe('Authentication', () => { }, } as unknown as KeyringController; - // Mock MetaMetrics.getInstance to return true for isEnabled - jest - .spyOn(MetaMetrics, 'getInstance') - .mockReturnValue({ isEnabled: () => true } as MetaMetrics); + jest.spyOn(analytics, 'isEnabled').mockReturnValue(true); }); it('throw an error if not using seedless onboarding flow', async () => { @@ -4494,10 +4477,7 @@ describe('Authentication', () => { beforeEach(() => { // Mock lockApp. jest.spyOn(Authentication, 'lockApp').mockResolvedValueOnce(undefined); - // Mock MetaMetrics.getInstance to return true for isEnabled. - jest - .spyOn(MetaMetrics, 'getInstance') - .mockReturnValueOnce({ isEnabled: () => true } as MetaMetrics); + jest.spyOn(analytics, 'isEnabled').mockReturnValue(true); const Engine = jest.requireMock('../Engine'); // Restore the KeyringController mock that may have been replaced by other test suites. @@ -4590,11 +4570,7 @@ describe('Authentication', () => { it('navigates to the optin metrics flow when metrics are not enabled and UI has not been seen', async () => { // Mock StorageWrapper.getItem to return null for OPTIN_META_METRICS_UI_SEEN. jest.spyOn(StorageWrapper, 'getItem').mockResolvedValue(null); - // Clear beforeEach mock and set MetaMetrics.getInstance to return false for isEnabled. - jest.spyOn(MetaMetrics, 'getInstance').mockReset(); - jest - .spyOn(MetaMetrics, 'getInstance') - .mockReturnValue({ isEnabled: () => false } as MetaMetrics); + jest.spyOn(analytics, 'isEnabled').mockReturnValue(false); // Call unlockWallet with a password. await Authentication.unlockWallet({ password: passwordToUse }); diff --git a/app/core/Authentication/Authentication.ts b/app/core/Authentication/Authentication.ts index 0a77d66997f..1d0e81adaa2 100644 --- a/app/core/Authentication/Authentication.ts +++ b/app/core/Authentication/Authentication.ts @@ -67,7 +67,7 @@ import { toChecksumHexAddress } from '@metamask/controller-utils'; import AccountTreeInitService from '../../multichain-accounts/AccountTreeInitService'; import { renewSeedlessControllerRefreshTokens } from '../OAuthService/SeedlessControllerHelper'; import { EntropySourceId } from '@metamask/keyring-api'; -import MetaMetrics from '../Analytics/MetaMetrics'; +import { analytics } from '../../util/analytics/analytics'; import { createDataDeletionTask as createDataDeletionTaskUtil } from '../../util/analytics/analyticsDataDeletion'; import { resetProviderToken as depositResetProviderToken } from '../../components/UI/Ramp/Deposit/utils/ProviderTokenVault'; import { setAllowLoginWithRememberMe } from '../../actions/security'; @@ -782,7 +782,7 @@ class AuthenticationService { // TODO: Refactor this orchestration to sagas. // Navigate to optin metrics or home screen based on metrics consent and UI seen. - const isMetricsEnabled = MetaMetrics.getInstance().isEnabled(); + const isMetricsEnabled = analytics.isEnabled(); const isOptinMetaMetricsUISeen = await StorageWrapper.getItem( OPTIN_META_METRICS_UI_SEEN, ); From be6548857f4299027c589e5883d49959358ed0a8 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Mon, 23 Feb 2026 21:46:22 +0000 Subject: [PATCH 005/131] [skip ci] Bump version number to 3779 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index bbb8e985b1b..ed46021c2c0 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.67.0" - versionCode 3764 + versionCode 3779 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 2b41bd3fb49..68b7de4599c 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3764 + VERSION_NUMBER: 3779 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3764 + FLASK_VERSION_NUMBER: 3779 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 0b16ca98adb..ed65c96dbcf 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3764; + CURRENT_PROJECT_VERSION = 3779; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3764; + CURRENT_PROJECT_VERSION = 3779; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3764; + CURRENT_PROJECT_VERSION = 3779; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3764; + CURRENT_PROJECT_VERSION = 3779; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3764; + CURRENT_PROJECT_VERSION = 3779; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3764; + CURRENT_PROJECT_VERSION = 3779; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 433eb106184a8dde76c224bdc2cf5a73fd45ae07 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:47:36 +0000 Subject: [PATCH 006/131] chore(runway): cherry-pick chore: New Crowdin translations by Github Action cp-7.67.0 (#26457) - chore: New Crowdin translations by Github Action cp-7.67.0 (#26152) --- > [!NOTE] > **Low Risk** > Localization-only JSON string changes; low functional risk aside from potential missing/renamed keys affecting UI text rendering. > > **Overview** > Updates German locale strings (`locales/languages/de.json`) with a large batch of Crowdin changes, including **new translation keys** for features like Market Insights, homepage sections, device authentication/biometrics messaging, network management, and MetaMask Card freeze/unfreeze flows. > > Also revises many existing strings (mostly English-to-German fixes and copy updates) across swap/bridge, perps activity, QR scanning, permissions/connect flows, and various error/toast messages, with some removals/renames (e.g., `merkl_rewards`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e3e2bd4e79f313ee97d20962f55e65a7e7703925. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: metamaskbot [9b792be](https://github.com/MetaMask/metamask-mobile/commit/9b792be53ac55c33a81a3726312f6251757321e4) Co-authored-by: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com> Co-authored-by: metamaskbot --- locales/languages/de.json | 494 ++++++++++++++++++++----------------- locales/languages/el.json | 490 +++++++++++++++++++++---------------- locales/languages/es.json | 480 ++++++++++++++++++++---------------- locales/languages/fr.json | 490 +++++++++++++++++++++---------------- locales/languages/hi.json | 498 +++++++++++++++++++++----------------- locales/languages/id.json | 492 +++++++++++++++++++++---------------- locales/languages/ja.json | 490 +++++++++++++++++++++---------------- locales/languages/ko.json | 494 ++++++++++++++++++++----------------- locales/languages/pt.json | 484 ++++++++++++++++++++---------------- locales/languages/ru.json | 492 +++++++++++++++++++++---------------- locales/languages/tl.json | 480 ++++++++++++++++++++---------------- locales/languages/tr.json | 488 +++++++++++++++++++++---------------- locales/languages/vi.json | 490 +++++++++++++++++++++---------------- locales/languages/zh.json | 492 +++++++++++++++++++++---------------- 14 files changed, 3861 insertions(+), 2993 deletions(-) diff --git a/locales/languages/de.json b/locales/languages/de.json index 2c00088dd2d..ec87201225a 100644 --- a/locales/languages/de.json +++ b/locales/languages/de.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "dauerhaft", "reset_wallet_desc_2": "von MetaMask auf diesem Gerät gelöscht. Dies kann nicht rückgängig gemacht werden.", "reset_wallet_desc_login": "Um Ihre Wallet wiederherzustellen, können Sie Ihre geheime Wiederherstellungsphrase oder Ihr Google- oder Apple-Kontopasswort verwenden. MetaMask verfügt nicht über diese Informationen.", - "reset_wallet_desc_srp": "Zur Wiederherstellung Ihrer Wallet ist Ihre geheime Wiederherstellungsphrase erforderlich. MetaMask verfügt nicht über diese Informationen." + "reset_wallet_desc_srp": "Zur Wiederherstellung Ihrer Wallet ist Ihre geheime Wiederherstellungsphrase erforderlich. MetaMask verfügt nicht über diese Informationen.", + "biometric_authentication_cancelled": "Biometrische Authentifizierung abgebrochen", + "biometric_authentication_cancelled_title": "Biometrische Einrichtung fehlgeschlagen", + "biometric_authentication_cancelled_description": "Bitte richten Sie die biometrische Authentifizierung in den Einstellungen erneut ein.", + "biometric_authentication_cancelled_button": "Bestätigen" }, "connect_hardware": { "title_select_hardware": "Verbinden Sie eine Hardware-Wallet", @@ -1040,7 +1044,7 @@ "title": "Einzuzahlender Betrag", "get_usdc_hyperliquid": "USDC erhalten • Hyperliquid", "insufficient_funds": "Unzureichende Gelder", - "no_funds_available": "Keine Gelder verfügbar. Bitte tätigen Sie zuerst eine Einzahlung.", + "no_funds_available": "Nicht genügend Guthaben vorhanden. Zahlen Sie Guthaben ein oder wählen Sie eine andere Zahlungsmethode.", "enter_amount": "Betrag eingeben", "fetching_quote": "Angebot einholen", "submitting": "Übermittlung der Transaktion", @@ -1970,8 +1974,8 @@ "trade_again": "Erneut traden", "activity": { "deposit_title": "Einzahlung", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "{{amount}} {{symbol}} eingezahlt", + "withdrew_amount": "{{amount}} {{symbol}} abgehoben", "status_completed": "Abgeschlossen", "status_failed": "Fehlgeschlagen", "status_pending": "Ausstehend" @@ -2051,6 +2055,16 @@ "referral_code_text": "Verwenden Sie meinen Empfehlungscode und erhalten Sie zusätzliche Belohnungen." } }, + "market_insights": { + "title": "Markteinblicke", + "updated_ago": "{{time}} aktualisiert", + "disclaimer": "KI-gestützte Erkenntnisse. Keine Finanzberatung.", + "whats_driving_price": "Was treibt den Preis an?", + "what_people_saying": "Was die Leute sagen", + "trade_button": "Trade", + "sources_count": "+{{count}} Quellen", + "sources_title": "Quellen" + }, "predict": { "title": "MetaMask Predictions", "prediction_markets": "Prognosemärkte", @@ -2384,8 +2398,8 @@ "no_available_tokens": "Sehen Sie Ihr Token nicht?", "add_tokens": "Token importieren", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "{{tokenCount}} neue {{tokensLabel}} in diesem Konto gefunden", "token_toast": { "tokens_imported_title": "Importierte Tokens", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "Token-Dezimalzahlen können nicht leer sein.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "Wir konnten keine Token mit diesem Namen finden.", + "tokens_empty_description": "Search for any token and import it", "select_token": "Token auswählen", "address_must_be_smart_contract": "Privatadresse erkannt. Geben Sie die Token-Contract-Adresse ein.", "billion_abbreviation": "Mrd.", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "Alle Konten trennen", "deceptive_site_ahead": "Täuschende Website voraus", "deceptive_site_desc": "Die Website, die Sie besuchen wollen, ist nicht sicher. Angreifer könnten Sie dazu verleiten, etwas Gefährliches zu tun.", + "malicious_site_detected": "Schadhafte Website erkannt", + "malicious_site_warning": "Wenn Sie diese Website besuchen, könnten Sie all Ihre Assets verlieren.", + "connect_anyway": "Trotzdem verbinden", "learn_more": "Mehr erfahren", "advisory_by": "Hinweis von Ethereum Phishing-Detektor und PhishFor", "potential_threat": "Mögliche Bedrohungen umfassen", @@ -2846,7 +2864,11 @@ "permissions": "Genehmigungen", "card_title": "MetaMask-Karte", "settings": "Einstellungen", - "log_out": "Abmelden" + "networks": "Netzwerke", + "log_out": "Abmelden", + "notifications": "Benachrichtigungen", + "buy": "Kaufen", + "scan": "Scan" }, "app_settings": { "enabling_notifications": "Benachrichtigungen aktivieren ...", @@ -2870,6 +2892,8 @@ "state_logs": "Statusprotokolle", "add_network_title": "Ein Netzwerk hinzufügen", "auto_lock": "Automatische Sperrung", + "enable_device_authentication": "Geräteauthentifizierung aktivieren", + "enable_device_authentication_desc": "Entsperren Sie MetaMask mithilfe der Biometrie oder des Passcodes Ihres Geräts.", "auto_lock_desc": "Legen Sie den Zeitraum fest, bevor die App automatisch gesperrt wird.", "state_logs_desc": "Dies hilft MetaMask dabei, Fehler zu beseitigen, auf die Sie stoßen könnten. Bitte senden Sie Fehler über das Hamburger-Symbol > „Feedback senden“ an den MetaMask-Support oder antworten Sie auf ein existierendes Ticket, falls Sie eines haben.", "autolock_immediately": "Sofort", @@ -2975,6 +2999,11 @@ "add_rpc_url": "RPC-URL hinzufügen", "add_block_explorer_url": "Block-Explorer-URL hinzufügen", "networks_desc": "Benutzerdefinierte RPC-Netzwerke hinzufügen und verwalten", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "Netzwerk suchen", + "networks_no_results": "Keine Netzwerke gefunden", "network_name_label": "Netzwerkname", "network_name_placeholder": "Netzwerkname (optional)", "network_rpc_url_label": "RPC-URL", @@ -2991,7 +3020,16 @@ "network_other_networks": "Sonstige Netzwerke", "network_rpc_networks": "RPC-Netzwerke", "network_add_network": "Netzwerk hinzufügen", + "add_chain_title": "Ein Netzwerk hinzufügen", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "Erneut versuchen", + "add_chain_added": "Added", + "add_chain_or": "oder", + "add_chain_custom_link": "Benutzerdefiniertes Netzwerk hinzufügen", "network_add_custom_network": "Benutzerdefiniertes Netzwerk hinzufügen", + "network_add_test_network": "Add a test network", "network_add": "Hinzufügen", "network_save": "Speichern", "remove_network_title": "Möchten Sie dieses Netzwerk entfernen?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "OK", "title": "Konto konnte keine Verbindung herstellen", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "Bitte scannen Sie den QR-Code auf der Website, um die Verbindung zu MetaMask wiederherzustellen." }, "app_information": { "title": "Information", @@ -3379,6 +3417,7 @@ "sell_description": "Krypto für Bargeld verkaufen" }, "asset_overview": { + "market_closed": "Markt geschlossen", "send_button": "Senden", "buy_button": "Kaufen", "cash_buy_button": "Barkauf", @@ -3399,19 +3438,6 @@ "bridge": "Bridge", "earn": "Verdienen", "convert_to_musd": "Zu mUSD konvertieren", - "merkl_rewards": { - "annual_bonus": "Bonus von {{apy}} %", - "claimable_bonus": "Anspruchsberechtigter Bonus", - "claimable_bonus_tooltip_description": "mUSD-Boni werden auf Linea eingefordert.", - "terms_apply": "Es gelten die Nutzungsbedingungen.", - "ok": "OK", - "claim": "Einfordern", - "processing_claim": "Einforderung in Bearbeitung ...", - "claim_on_linea_title": "Boni auf Linea einfordern", - "claim_on_linea_description": "Ihr Bonus wird auf Linea ausgegeben, gesondert von Ihrem Ethereum mUSD-Guthaben.", - "continue": "Fortfahren", - "unexpected_error": "Unerwarteter Fehler. Bitte erneut versuchen." - }, "tron": { "daily_resource_new_energy": "Neue tägliche Energie", "sufficient_to_cover": "Ausreichend zur Deckung", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "Token-Adresse in die Zwischenablage kopiert" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "Ungültiger QR-Code", "invalid_qr_code_message": "Der QR-Code, den Sie zu scannen versuchen, ist ungültig.", "allow_camera_dialog_title": "Kamerazugriff erlauben", "allow_camera_dialog_message": "Wir benötigen Ihre Berechtigung, um QR-Codes zu scannen", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "Es sieht aus, als würden Sie versuchen, die Erweiterung zu synchronisieren. Um dies zu tun, müssen Sie Ihr aktuelles Waller löschen. \n\nNachdem Sie die App gelöscht oder eine aktuelle Version der App neu installiert haben, wählen Sie die Option „Mit MetaMask-Erweiterung synchronisieren“. Wichtig! Bevor Sie Ihr Wallet löschen, stellen Sie sicher, dass Sie ein Backup Ihrer geheimen Wiederherstellungsphrase haben.", "not_allowed_error_title": "Kamerazugriff einschalten", "not_allowed_error_desc": "Um einen QR-Code zu scannen, müssen Sie MetaMask über das Einstellungsmenü Ihres Geräts Zugriff auf die Kamera gewähren.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "Unbekannter QR-Code", "unrecognized_address_qr_code_desc": "Es tut uns leid, aber dieser QR-Code ist mit keiner Konto- oder Contract-Adresse verbunden.", "url_redirection_alert_title": "Sie sind im Begriff, einen externen Link abzurufen", "url_redirection_alert_desc": "Links können verwendet werden, um versuchen, Personen zu betrügen oder zu phishen, also besuchen Sie nur Webseiten, denen Sie vertrauen.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "Sie besitzen dieses Sammlerstück nicht", "known_asset_contract": "Bekannte Vermögens-Contract-Adresse", "max": "Max", - "recipient_address": "Recipient address", + "recipient_address": "Empfänger-Adresse", "required": "Benötigt", "to": "Bis", "total": "Gesamt", @@ -3641,7 +3667,7 @@ "nevermind": "Macht nichts", "edit_network_fee": "Gas-Gebühr bearbeiten", "edit_priority": "Priorität bearbeiten", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "Gas-Stornogebühr", "gas_speedup_fee": "Gas-Beschleunigungsgebühr", "use_max": "Maximum verwenden", "set_gas": "Festlegen", @@ -3650,7 +3676,7 @@ "transaction_fee": "Gas-Gebühr", "transaction_fee_less": "Keine Gebühr", "total_amount": "Gesamtbetrag", - "view_data": "View data", + "view_data": "Daten anzeigen", "adjust_transaction_fee": "Transaktionsgebühr anpassen", "could_not_resolve_ens": "ENS konnte nicht gelöst werden", "asset": "Vermögenswert", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "Öffnen Sie einen neuen Tab, um im dezentralisieren Web zu surfen.", "got_it": "Verstanden", "max_tabs_title": "Maximale Anzahl an Tabs erreicht", - "max_tabs_desc": "Derzeit können nur fünf Tabs gleichzeitig geöffnet werden. Bitte schließen Sie vorhandene Tabs, bevor Sie neue hinzufügen.", + "max_tabs_desc": "We currently only support 20 open tabs at once. Please close existing tabs before adding new ones.", "failed_to_resolve_ens_name": "Wir konnten diesen ENS-Namen nicht lösen", "remove_bookmark_title": "Favoriten entfernen", "remove_bookmark_msg": "Möchten Sie diese Seite wirklich aus Ihren Favoriten entfernen?", @@ -3828,7 +3854,7 @@ "cancel_button": "Stornieren" }, "approval": { - "title": "Confirm transaction" + "title": "Transaktion bestätigen" }, "approve": { "title": "Genehmigen", @@ -3839,39 +3865,39 @@ "unavailable": "Nicht verfügbar", "tx_review_confirm": "Bestätigen", "tx_review_transfer": "Übertragung", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "Contract-Einsatz", + "tx_review_transfer_from": "Übertragung von", + "tx_review_unknown": "Unbekannte Methode", "tx_review_approve": "Genehmigen", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "Zulage erhöhen", + "tx_review_set_approval_for_all": "Genehmigung für alle festlegen", + "tx_review_staking_claim": "Staking-Anspruch", "tx_review_staking_deposit": "Staking-Einzahlung", "tx_review_staking_unstake": "Unstaken", "tx_review_lending_deposit": "Krediteinzahlung", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "Kreditauszahlung", "tx_review_perps_deposit": "Finanzierte Perps", "tx_review_predict_deposit": "Finanzierte Prognosen", "tx_review_predict_claim": "Eingeforderte Gewinne", "tx_review_predict_withdraw": "Prognosenwiderruf", "tx_review_musd_conversion": "mUSD-Konvertierung", "claim": "Einfordern", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "ETH senden", + "self_sent_ether": "Senden Sie sich selbst ETH", + "received_ether": "ETH empfangen", "sent_dai": "DAI senden", "self_sent_dai": "Senden Sie sich selbst DAI", "received_dai": "DAI empfangen", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "Tokens senden", + "received_tokens": "Token empfangen", "ether": "ETH", "sent_unit": "{{unit}} senden", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "Sich selbst {{unit}} senden", "received_unit": "{{unit}} empfangen", "sent_collectible": "Gesendetes Sammlerstück", "received_collectible": "Empfangenes Sammlerstück", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "ETH senden", + "send_unit": "{{unit}} senden", "send_collectible": "Sammlerstück senden", "receive_collectible": "Sammlerstück empfangen", "sent": "Versendet", @@ -3881,17 +3907,17 @@ "send": "Senden", "redeposit": "Erneute Einzahlung", "interaction": "Interaktion", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "Contract-Einsatz", + "to_contract": "Neuer Contract", + "mint": "Prägung", "tx_details_free": "Kostenlos", "tx_details_not_available": "Nicht verfügbar", "smart_contract_interaction": "Smart Contract-Interaktion", "swaps_transaction": "Swaps-Transaktion", "bridge_transaction": "Bridge", "approve": "Genehmigen", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "Zulage erhöhen", + "set_approval_for_all": "Genehmigung für alle festlegen", "hash": "Hash", "from": "Von", "to": "Bis", @@ -3899,15 +3925,15 @@ "amount": "Betrag", "fee": { "transaction_fee_in_ether": "Transaktionsgebühr", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "Transaktionsgebühr (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "Verbrauchtes Gas (Einheiten)", + "gas_limit": "Gas-Limit (Einheiten)", + "gas_price": "Gas-Preis (GWEI)", + "base_fee": "Grundgebühr (GWEI)", + "priority_fee": "Prioritätsgebühr (GWEI)", "multichain_priority_fee": "Prioritätsgebühr", - "max_fee": "Max fee per gas", + "max_fee": "Max. Gebühr pro Gas", "total": "Gesamt", "view_on": "Ansehen auf", "view_on_etherscan": "Auf Etherscan ansehen", @@ -3923,13 +3949,13 @@ "nonce": "Nonce", "from_device_label": "von diesem Gerät", "import_wallet_row": "Konto zu diesem Gerät hinzugefügt", - "import_wallet_label": "Account added", + "import_wallet_label": "Konto hinzugefügt", "import_wallet_tip": "Alle zukünftigen Transaktionen von diesem Gerät enthalten ein Label „von diesem Gerät“ neben dem Zeitstempel. Für Transaktionen, die vor dem Hinzufügen des Kontos datiert wurden, zeigt diese Historie nicht an, welche ausgehenden Transaktionen von diesem Gerät ausgingen.", "sign_title_scan": "Scan ", "sign_title_device": "mit Ihrer Hardware-Wallet", "sign_description_1": "Nachdem Sie sich mit Ihrer Hardware-Wallet angemeldet haben,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "Auf Signatur abrufen tippen", + "sign_get_signature": "Signatur abrufen", "transaction_id": "Transaktions-ID", "network": "Netzwerk", "request_from": "Anfrage von", @@ -4032,7 +4058,7 @@ "title": "Netzwerke", "other_networks": "Sonstige Netzwerke", "close": "Schließen", - "status_ok": "All systems operational", + "status_ok": "Alle Systeme laufen", "status_not_ok": "Das Netzwerk hat einige Probleme.", "want_to_add_network": "Möchten Sie dieses Netzwerk hinzufügen?", "add_custom_network": "Benutzerdefiniertes Netzwerk hinzufügen", @@ -4051,7 +4077,7 @@ "review": "Überprüfen", "view_details": "Details anzeigen", "network_details": "Netzwerkdetails", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "Durch Auswahl von „Bestätigen“ wird die Überprüfung der Netzwerkdetails aktiviert. Sie können die Überprüfung der Netzwerkdetails deaktivieren in ", "network_settings_security_privacy": "Einstellungen > Sicherheit und Datenschutz", "network_currency_symbol": "Währungszeichen", "network_block_explorer_url": "Block-Explorer-URL", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "Ein betrügerischer Netzwerkanbieter kann bezüglich des Status der Blockchain täuschen und Ihre Netzwerkaktivitäten aufzeichnen. Fügen Sie nur vertrauenswürdige benutzerdefinierte Netzwerke hinzu.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Netzwerkinformationen", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "Zusätzliche Netzwerkinformationen", "network_warning_desc": "Diese Netzwerkverbindung ist von Dritten abhängig. Die Verbindung könnte weniger zuverlässig sein oder Dritten erlauben, Ihre Aktivitäten zu verfolgen.", "additonial_network_information_desc": "Einige dieser Netzwerke werden von Dritten betrieben. Die Verbindungen können weniger zuverlässig sein oder Dritten die Verfolgung von Aktivitäten ermöglichen.", "connect_more_networks": "Mehr Netzwerke verbinden", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "Dieses Netzwerk ist veraltet", "network_deprecated_description": "Das Netzwerk, zu dem Sie eine Verbindung herstellen möchten, wird von MetaMask nicht mehr unterstützt.", "edit_networks_title": "Netzwerke bearbeiten", - "no_network_fee": "No network fee" + "no_network_fee": "Keine Netzwerkgebühr" }, "permissions": { "title_this_site_wants_to": "Diese Website möchte:", @@ -4111,11 +4137,11 @@ "network_connected": "Netzwerk verbunden ", "see_your_accounts": "Ihre Konten einsehen und Transaktionen vorschlagen", "connected_to": "Verbunden mit ", - "manage_permissions": "Manage permissions", + "manage_permissions": "Berechtigungen verwalten", "edit": "Bearbeiten", "cancel": "Stornieren", "got_it": "Verstanden", - "connection_details_title": "Connection details", + "connection_details_title": "Verbindungsdetails", "connection_details_description": "Sie haben am {{connectionDateTime}} mit dem MetaMask-Browser eine Verbindung zu dieser Website hergestellt", "title_add_network_permission": "Netzwerkgenehmigung hinzufügen", "add_this_network": "Dieses Netzwerk hinzufügen", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "Mit Geräte-PIN entsperren?" }, "authentication": { - "auth_prompt_title": "Authentifizierung benötigt", - "auth_prompt_desc": "Bitte Authentifizieren, um MetaMask zu verwenden", - "fingerprint_prompt_title": "Authentifizierung benötigt", - "fingerprint_prompt_desc": "Benutzen Sie Ihren Fingerabdruck, um MetaMask zu entsperren", - "fingerprint_prompt_cancel": "Stornieren" + "auth_prompt_desc": "Bitte Authentifizieren, um MetaMask zu verwenden" }, "accountApproval": { "title": "VERBINDUNGSANFRAGE", "walletconnect_title": "WALLET-VERBINDUNGSANFRAGE", "action": "Mit dieser Seite verbinden?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "Um die Verbindung wiederherzustellen, wählen Sie die Nummer, die auf der Website angezeigt wird.", + "action_reconnect_deeplink": "Möchten Sie sich erneut mit dieser Website verbinden?", "connect": "Verbinden", "resume": "Fortsetzen", "cancel": "Stornieren", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "Diese Website-Verbindung nicht merken", "disconnect": "Verbindung trennen", "permission": "Anzeigen:", "address": "öffentliche adresse", @@ -4218,7 +4240,7 @@ "error_title": "Etwas ist schiefgelaufen", "error_message": "Wir konnten den privaten Key nicht importieren. Bitte überprüfen Sie, ob Sie ihn korrekt eingegeben haben.", "error_empty_message": "Sie müssen Ihren privaten Key eingeben.", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "oder scannen Sie einen QR-Code." }, "import_private_key_success": { "title": "Konto erfolgreich importiert!", @@ -4229,18 +4251,18 @@ "import_wallet_title": "Wallet importieren", "enter_srp_subtitle": "Geben Sie Ihre geheime Wiederherstellungsphrase ein", "textarea_placeholder": "Fügen Sie zwischen jedem Wort ein Leerzeichen ein und stellen Sie sicher, dass niemand zusieht", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "Geben Sie die geheime Wiederherstellungsphrase Ihrer Wallet ein. Sie können jede geheime Wiederherstellungsphrase von Ethereum, Solana oder Bitcoin importieren.", + "subtitle": "Fügen Sie Ihre geheime Wiederherstellungsphrase ein", "cta_text": "Fortfahren", "paste": "Einfügen", "clear": "Alle löschen", "srp_number_of_words_option_title": "Anzahl der Wörter", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "Ich habe eine 12-Wort-Phrase", + "24_word_option": "Ich habe eine 24-Wort-Phrase", "error_title": "Hoppla! Etwas ist schiefgelaufen ...", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "Die geheime Wiederherstellungsphrase konnte nicht importiert werden. Bitte stellen Sie sicher, dass Sie sie korrekt eingegeben haben.", + "error_empty_message": "Sie müssen Ihre geheime Wiederherstellungsphrase eingeben.", + "error_number_of_words_error_message": "Geheime Wiederherstellungsphrasen enthalten 12 oder 24 Wörter", "error_srp_is_case_sensitive": "Ungültige Eingabe! Bei der geheimen Wiederherstellungsphrase wird zwischen Groß- und Kleinschreibung unterschieden.", "error_srp_word_error_1": "Wort ", "error_srp_word_error_2": " ist falsch oder falsch geschrieben.", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " sind falsch oder falsch geschrieben.", "error_invalid_srp": "Geheime Wiederherstellungsphrase ungültig", "error_duplicate_srp": "Diese geheime Wiederherstellungsphrase wurde bereits importiert.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "Das Konto, das Sie importieren möchten, ist ein Duplikat.", + "invalid_qr_code_title": "Ungültiger QR-Code", + "invalid_qr_code_message": "Der QR-Code enthält keine gültige geheime Wiederherstellungsphrase", "success_1": "Wallet", "success_2": "importiert" }, @@ -4665,7 +4687,7 @@ "button": "Wallet schützen" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "Transaktionsaktualisierung fehlgeschlagen", "text": "Möchten Sie es erneut versuchen?", "cancel_button": "Stornieren", "retry_button": "Erneut versuchen" @@ -4684,13 +4706,13 @@ "next": "Weiter", "amount_placeholder": "0,00", "link_copied": "Link in die Zwischenablage kopiert", - "send_link_title": "Send link", + "send_link_title": "Link senden", "description_1": "Ihr Anfragelink kann jetzt gesendet werden!", "description_2": "Senden Sie diesen Link an eine/n Freund/in und er/sie wird gebeten, zu senden", "copy_to_clipboard": "In die Zwischenablage kopieren", "qr_code": "QR-Code", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "Link senden", + "request_qr_code": "QR-Code der Zahlungsanfrage", "balance": "Guthaben" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "Sie haben keine aktiven Sitzungen", - "end_session_title": "End session", + "end_session_title": "Sitzung beenden", "end": "Beenden", "cancel": "Stornieren", - "session_ended_title": "Session ended", + "session_ended_title": "Sitzung beendet", "session_ended_desc": "Die ausgewählte Sitzung wurde beendet", "session_already_exist": "Diese Sitzung ist bereits verbunden.", "close_current_session": "Schließen Sie die aktuelle Sitzung, bevor Sie eine neue beginnen." @@ -4765,15 +4787,14 @@ "on_network": "auf {{networkName}}", "debit_card": "Debitkarte", "select_payment_method": "Zahlungsmethode auswählen", - "loading_quote": "Loading quote...", "pay_with": "Bezahlen mit", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "Kauf über {{providerName}}.", + "change_provider": "Anbieter wechseln.", "payment_error": "Etwas ist schiefgelaufen! Bitte versuchen Sie es erneut.", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "Es stehen keine Zahlungsmethoden zur Verfügung.", "error_fetching_quotes": "Etwas ist schiefgelaufen! Bitte versuchen Sie es erneut.", "no_quotes_available": "Keine Anbieter verfügbar.", - "providers": "Providers", + "providers": "Anbieter", "continue": "Fortfahren", "powered_by_provider": "Unterstützt von {{provider}}", "purchased_currency": "{{currency}} gekauft", @@ -4871,6 +4892,15 @@ "log_out": "Von {{provider}} abmelden", "logged_out_success": "Erfolgreich abgemeldet", "logged_out_error": "Fehler beim Abmelden" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "niedrigstes Verkaufslimit", "medium_sell_limit": "mittleres Verkaufslimit", "highest_sell_limit": "höchstes Verkaufslimit", - "change": "Change", + "change": "Ändern", "continue_to_amount": "Weiter zum Betrag", "no_payment_methods_title": "Keine Zahlungsmethoden in {{regionName}}", "no_cash_destinations_title": "Keine Bargeldziele in {{regionName}}", @@ -5118,7 +5148,7 @@ "start_swapping": "Mit dem Tausch beginnen" }, "feature_off_title": "Vorrübergehend nicht verfügbar", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "MetaMask Swaps wird derzeit gewartet. Bitte versuchen Sie es später erneut.", "wrong_network_title": "Tauschgeschäfte (Swaps) nicht verfügbar", "wrong_network_body": "Sie können Token nur über das Ethereum-Netzwerk tauschen.", "unallowed_asset_title": "Dieser Token kann nicht umgetauscht werden", @@ -5160,7 +5190,7 @@ "not_enough": "Nicht genügend {{symbol}}, um diesen Tausch abzuschließen", "max_slippage": "Maximale Slippage", "max_slippage_amount": "Maximale Verzögerung {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "Wenn der Wechselkurs sich zwischen dem Zeitpunkt Ihrer Order und deren Bestätigung ändert, bezeichnet man dies als „Slippage“. Ihr Swap wird automatisch storniert, wenn diese Slippage Ihre Einstellung für die „maximale Slippage“ überschreitet.", "slippage_warning": "Vergewissern Sie sich, dass Sie wissen, was Sie tun!", "allows_up_to_decimals": "{{symbol}} erlaubt bis zu {{decimals}} Dezimalzahlen", "get_quotes": "Angebote einholen", @@ -5199,7 +5229,7 @@ "edit": "Bearbeiten", "quotes_include_fee": "Angebote beinhalten eine MetaMask-Gebühr in Höhe von {{fee}} %.", "quotes_include_gas_and_metamask_fee": "Angebot umfasst Gas und eine MetaMask-Gebühr von {{fee}} %", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "Zum Swappen tippen", "swipe_to_swap": "Zum Tauschen wischen", "swipe_to": "Wischen, um zu", "swap": "Swap", @@ -5259,7 +5289,7 @@ "approve": "{{sourceToken}} für Swaps genehmigen: Bis zu {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "Swap ausstehend ({{sourceToken}} zu {{destinationToken}})", "swap_confirmed": "Tausch abgeschlossen ({{sourceToken}} zu {{destinationToken}})", "approve_pending": "Gebe {{sourceToken}} zum Tausch frei", "approve_confirmed": "{{sourceToken}} zum Tausch freigegeben" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "Netzwerk-Dropdown zu Ihren Assets verschoben", "description_2": "Swap und Bridge in einem einfachen Ablauf", - "description_3": "Streamlined send experience", + "description_3": "Sendeerlebnis optimiert", "description_4": "Eine neue Kontoansicht" }, "more_information": "Jetzt können Sie sich auf Ihre Tokens und Aktivitäten konzentrieren, und nicht auf die Netzwerke dahinter.", @@ -5406,21 +5436,21 @@ "aggressive_label": "Aggressiv", "aggressive_text": "Hohe Wahrscheinlichkeit, selbst in volatilen Märkten. Verwenden Sie Aggressiv, um Schwankungen im Netzwerk-Traffic, die z. B. durch den Ausfall beliebter NFTs entstehen, abzudecken.", "market_label": "Markt", - "market_text": "Use market for fast processing at current market price.", + "market_text": "Verwenden Sie Markt für eine schnelle Verarbeitung zum aktuellen Marktpreis.", "low_label": "Niedrig", "low_text": "Verwenden Sie Niedrig, um auf einen günstigeren Preis zu warten. Zeitschätzungen sind viel ungenauer, da die Preise etwas unvorhersehbar sind.", "link": "Erfahren Sie mehr über die Anpassung von Gas." }, "save": "Speichern", "submit": "Absenden", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "Die maximale Prioritätsgebühr ist für aktuelle Netzwerkkonditionen niedrig.", + "max_priority_fee_high": "Maximale Prioritätsgebühr höher als nötig", + "max_priority_fee_speed_up_low": "Die maximale Prioritätsgebühr muss mindestens {{speed_up_floor_value}} GWEI betragen (10 % höher als die ursprüngliche Transaktion).", + "max_priority_fee_cancel_low": "Die maximale Prioritätsgebühr muss mindestens {{cancel_value}} GWEI betragen (50 % höher als die ursprüngliche Transaktion).", + "max_fee_low": "Maximale Gebühr ist für aktuelle Netzwerkkonditionen niedrig", + "max_fee_high": "Maximale Gebühr ist höher als nötig", + "max_fee_speed_up_low": "Die maximale Gebühr muss mindestens {{speed_up_floor_value}} GWEI betragen (10 % höher als die erste Transaktion)", + "max_fee_cancel_low": "Die maximale Gebühr muss mindestens {{cancel_value}} GWEI betragen (50 % höher als die erste Transaktion)", "learn_more_gas_limit": "Das Gas-Limit ist die maximale Anzahl an Gas, die Sie verwenden möchten. Gas-Einheiten sind ein Multiplikator für die „maximale Prioritätsgebühr“ und die „maximale Gebühr“.", "learn_more_max_priority_fee": "Die maximale Prioritätsgebühr (auch als „Miner-Trinkgeld“ bekannt) geht direkt an die Miner und reizt sie dazu an, Ihre Transaktion zu priorisieren. Meistens zahlen Sie Ihre maximale Einstellung. ", "learn_more_max_fee": "Die maximale Gebühr ist der Höchstbetrag, den Sie zahlen müssen (Grundgebühr + Prioritätsgebühr). ", @@ -5530,10 +5560,10 @@ "enable_remember_me_description": "Wenn „Angemeldet bleiben“ aktiviert ist, kann jeder, der Zugriff zu Ihrem Handy hat, auf Ihr MetaMask-Konto zugreifen." }, "turn_off_remember_me": { - "title": "Geben Sie Ihr Passwort ein, um „Angemeldet bleiben“ zu deaktivieren.", - "placeholder": "Passwort", - "description": "Wenn Sie diese Option deaktivieren, benötigen Sie von nun an Ihr Passwort, um MetaMask zu entsperren.", - "action": "„Angemeldet bleiben“ deaktivieren" + "title": "„Angemeldet bleiben“ ausschalten", + "placeholder": "Passwort bestätigen", + "description": "Die Funktion „Angemeldet bleiben“ kann nach dem Ausschalten nicht mehr verwendet werden. Diese Funktion wurde eingestellt. Sie können MetaMask stattdessen mit Ihrem Passwort oder Ihren biometrischen Daten entsperren.", + "action": "„Angemeldet bleiben“ ausschalten" }, "dapp_connect": { "warning": "Um diese Funktion nutzen zu können, aktualisieren Sie bitte die App auf die neueste Version." @@ -5582,7 +5612,7 @@ "learn_more": "Mehr erfahren" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "Drittanbieterdetails verifizieren", "protect_from_scams": "Nehmen Sie sich einen Moment Zeit, um die Drittanbieterdetails zu verifizieren und sich so vor Betrügern zu schützen.", "learn_to_verify": "Erfahren Sie, wie Sie Drittanbieterdetails überprüfen können.", "spending_cap": "ausgabenobergrenze", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "Wiederherstellung erforderlich", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "Etwas ist schiefgelaufen, aber keine Sorge! Versuchen wir, Ihre Wallet wiederherzustellen.", "restore_needed_action": "Wallet wiederherstellen" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Das Schließen der ausgeführten App auf Ihrem Ledger-Gerät ist fehlgeschlagen.", "ethereum_app_not_installed": "Ethereum-App nicht installiert.", "ethereum_app_not_installed_error": "Bitte installieren Sie die Ethereum-App auf Ihrem Ledger-Gerät.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "Ethereum-App nicht geöffnet", + "eth_app_not_open_message": "Bitte öffnen Sie die Ethereum-App auf Ihrem Ledger-Gerät.", "ledger_is_locked": "Ledger ist gesperrt", "unlock_ledger_message": "Bitte entsperren Sie Ihr Ledger-Gerät.", "cannot_get_account": "Konto konnte nicht abgerufen werden.", @@ -5797,8 +5827,8 @@ "error_description": "Installation von {{snap}} fehlgeschlagen." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "Ein jährlicher Bonus, den Sie täglich aus Ihrer Wallet abholen können.", + "earn_a_percentage_bonus": "Verdienen Sie einen Bonus von {{percentage}} %", "claimable_bonus": "Anspruchsberechtigter Bonus", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "Die benötigte Zeit, um Ihren Token aus dem Protokoll zu entnehmen und ihn wieder in Ihre Wallet zurückzubekommen", "receive": "Dieser Token wird zur Überwachung Ihrer Assets und Belohnungen genutzt. Übertragen Sie ihn nicht und handeln Sie nicht damit, sonst können Sie sich Ihre Assets nicht auszahlen lassen.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "Ihr Gesundheitsfaktor misst das Liquidationsrisiko", "above_two_dot_zero": "Über 2,0", "safe_position": "Sichere Position", "between_one_dot_five_and_2_dot_zero": "Zwischen 1,5 und 2,0", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "Mittleres Liquidationsrisiko", "below_one_dot_five": "Unter 1,5", "higher_liquidation_risk": "Höheres Liquidationsrisiko" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "Warum kann ich nicht mein gesamtes Guthaben abheben?", "your_withdrawal_amount_may_be_limited_by": "Ihr Auszahlungsbetrag kann begrenzt sein durch", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "Pool-Liquidität", "not_enough_funds_available_in_the_lending_pool_right_now": "Derzeit sind nicht genügend Mittel im Kreditpool verfügbar.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "Bestehende Kreditpositionen", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "Durch die Auszahlung könnten Ihre bestehenden Kreditpositionen der Gefahr einer Liquidation ausgesetzt sein." } }, @@ -5998,11 +6028,11 @@ "earn_button": "Verdienen" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "TRX staken und verdienen", + "stake_any_amount": "Staken Sie jeden TRX-Betrag.", "earn_trx_rewards": "Verdienen Sie TRX-Belohnungen.", "earn_trx_rewards_description": "Verdienen Sie, sobald Sie Ihre Einlage getätigt haben. Die Prämien werden automatisch gutgeschrieben.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "Jederzeit unstaken. Unstaken dauert in der Regel bis zu 14 Tage." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "Geschätzte Gas-Gebühr", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "Gas-Gebühren werden an Krypto-Miner gezahlt, die Transaktionen im Ethereum-Netzwerk verarbeiten. MetaMask profitiert nicht von den Gas-Gebühren.", "gas_fluctuation": "Die Gas-Gebühren werden geschätzt und aufgrund der Komplexität des Netzwerk-Traffics und der Transaktionskomplexität schwanken.", "gas_learn_more": "Erfahren Sie mehr über Gas-Gebühren" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "Anmelden mit", "spender": "Spender", "now": "Jetzt", - "switching_to": "Switching to", + "switching_to": "Wechseln zu", "bridge_estimated_time": "Geschätzte Zeit", "pay_with": "Bezahlen mit", - "receive_as": "Receive", + "receive_as": "Empfangen", "total": "Insgesamt", - "you_receive": "You'll receive", + "you_receive": "Sie empfangen", "transaction_fee": "Transaktionsgebühr", - "transaction_fees": "Transaction fees", + "transaction_fees": "Transaktionsgebühren", "metamask_fee": "MetaMask-Gebühr", "network_fee": "Netzwerk-Gebühr", "bridge_fee": "Gebühr für Bridge-Anbieter" @@ -6234,7 +6264,7 @@ "transaction_fee": "Wir swappen Ihre Token gegen USDC.e auf Polygon, dem von Predictions verwendeten Netzwerk. Swap-Anbieter erheben möglicherweise eine Gebühr, MetaMask jedoch nicht." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMask swappt den Token für Sie in den gewünschten Token um. Beim Swap in MUSD fallen keine MetaMask-Gebühren an." }, "musd_conversion": { "transaction_fee": "Die Gebühren für die Umstellung von mUSD umfassen Netzwerkkosten und können Anbietergebühren beinhalten." @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "Diese Website ersucht um Ihre Signatur", "transaction_tooltip": "Diese Website fragt nach Ihrer Transaktion", "details": "Details", - "qr_get_sign": "Get signature", + "qr_get_sign": "Signatur abrufen", "qr_scan_text": "Mit Ihrer Hardware-Wallet scannen", "sign_with_ledger": "Mit Ledger unterschreiben", "smart_account": "Smart-Konto", "smart_contract": "Smart Contract", - "standard_account": "Standard account", + "standard_account": "Standardkonto", "siwe_message": { "url": "URL", "network": "Netzwerk", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "Smart-Konto", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "Standardkonto", "switch": "Wechseln", "switchBack": "Zurückwechseln", "includes_transaction": "Umfasst {{transactionCount}} Transaktionen", @@ -6307,9 +6337,9 @@ "cancel": "Stornieren", "description": "Geben Sie den Betrag ein, den Sie gerne in Ihrem Namen ausgeben würden.", "invalid_number_error": "Ausgabenobergrenze muss eine Zahl sein", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "Ausgabenobergrenze darf nicht leer sein", + "no_extra_decimals_error": "Ausgabenobergrenze darf nicht mehr Dezimalstellen enthalten als der Token", + "no_zero_error": "Ausgabenobergrenze darf nicht 0 sein", "no_zero_error_decrease_allowance": "Ausgabenobergrenze von 0 hat keine Auswirkungen auf die Methode ‚decreaseAllowance‘", "no_zero_error_increase_allowance": "Ausgabenobergrenze von 0 hat keine Auswirkungen auf die Methode ‚increaseAllowance‘", "save": "Speichern", @@ -6336,7 +6366,7 @@ "transferRequest": "Übertragungsanfrage", "nested_transaction_heading": "Transaktion {{index}}", "transaction": "Transaktion", - "available_balance": "Available balance: ", + "available_balance": "Verfügbares Guthaben: ", "edit_amount_done": "Fortfahren", "deposit_edit_amount_done": "Gelder hinzufügen", "deposit_edit_amount_predict_withdraw": "Auszahlen", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "Geschäftsbedingungen", "select_token": "Token auswählen", "no_tokens_found": "Keine Tokens gefunden", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "Wir konnten keine Tokens mit diesem Namen finden. Versuchen Sie eine andere Suche.", "select_network": "Netzwerk wählen", "all_networks": "Alle Netzwerke", "num_networks": "{{numNetworks}} Netzwerke", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "Alle abwählen", "see_all": "Alle sehen", "all": "Alle", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}} mehr", "apply": "Anwenden", "slippage": "Slippage", "slippage_info": "Wenn der Preis sich zwischen dem Zeitpunkt Ihrer Order und deren Bestätigung ändert, bezeichnet man dies als „Slippage“. Ihr Swap wird automatisch storniert, wenn diese Slippage die von Ihnen hier festgelegte Toleranz überschreitet.", @@ -6392,7 +6422,7 @@ "quote_info_title": "Rate", "network_fee_info_title": "Netzwerkgebühr", "network_fee_info_content": "Die Netzwerkgebühren richten sich nach der Netzwerkauslastung und der Komplexität Ihrer Transaktion.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "Diese Netzwerkgebühr wird von MetaMask bezahlt, sodass Sie Transaktionen durchführen können, ohne dass sich {{nativeToken}} in Ihrem Konto befindet.", "points": "Geschätzte Punkte", "points_tooltip": "Punkte", "points_tooltip_content_1": "Durch Punkte erhalten Sie MetaMask-Belohnungen für die Durchführung von Transaktionen, z. B. beim Swap, Bridge oder Handeln von Perps.", @@ -6406,7 +6436,7 @@ "select_recipient": "Empfänger auswählen", "external_account": "Externes Konto", "error_banner_description": "Diese Handelsroute ist derzeit nicht verfügbar. Versuchen Sie, den Betrag, das Netzwerk oder das Token zu ändern, und wir finden die beste Option.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "Diese Handelsroute ist momentan nicht verfügbar. Versuchen Sie, den Betrag, das Netzwerk oder den Token zu ändern. Wir finden dann die beste Option für Sie.\n\nBitte beachten Sie: Beim Handel mit Ondo-tokenisierten Aktien können geografische Beschränkungen gelten, z. B. für die USA, die EU, Großbritannien und Brasilien.", "insufficient_funds": "Unzureichende Gelder", "insufficient_gas": "Unzureichendes Gas", "select_amount": "Betrag auswählen", @@ -6417,9 +6447,9 @@ "title": "Bridge", "submitting_transaction": "Absenden", "fetching_quote": "Angebot einholen", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "Beinhaltet {{feePercentage}} % MetaMask-Gebühr.", "no_mm_fee": "Keine MM-Gebühr", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "Beim Swap in {{destTokenSymbol}} fallen keine MetaMask-Gebühren an.", "hardware_wallet_not_supported": "Hardware-Wallets werden noch nicht unterstützt. Verwenden Sie eine Hot Wallet, um fortzufahren.", "hardware_wallet_not_supported_solana": "Hardware-Wallets werden für Solana noch nicht unterstützt. Verwenden Sie ein Hot Wallet, um fortzufahren.", "price_impact_info_title": "Preiseinfluss", @@ -6432,17 +6462,24 @@ "approval_needed": "Genehmigt den Token für den Swap.", "approval_tooltip_title": "Exakten Zugriff gewähren", "approval_tooltip_content": "Sie gewähren Zugriff auf den angegebenen Betrag in Höhe von {{amount}} {{symbol}}. Der Kontrakt hat keinen Zugriff auf weitere Gelder.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "Minimum empfangen", + "minimum_received_tooltip_title": "Minimum empfangen", "minimum_received_tooltip_content": "Der Mindestbetrag, den Sie erhalten, wenn sich der Kurs während der Bearbeitung Ihrer Transaktion entsprechend Ihrer Slippage-Toleranz ändert. Dies ist eine Schätzung unserer Liquiditätsanbieter. Die Endbeträge können abweichen.", + "market_closed": { + "title": "Markt ist geschlossen", + "description": "Der Markt, der diesen Token stützt, ist derzeit geschlossen. Token können jederzeit On-Chain übertragen werden.", + "learn_more": "Mehr erfahren", + "learn_more_url": "https://status.ondo.finance/market", + "done": "Fertig" + }, "submit": "Absenden", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "Ihre Transaktion wird nicht durchgeführt, wenn sich der Preis um mehr als den Slippage-Prozentsatz ändert.", "cancel": "Stornieren", "confirm": "Bestätigen", "exceeding_upper_slippage_warning": "Hohe Slippage, was zu einem ungünstigen Swap führen kann", "exceeding_lower_slippage_warning": "Niedrige Slippage, was zu einem ungünstigen Swap führen kann", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "Geben Sie einen Wert ein, der größer als {{value}}% ist.", + "exceeding_upper_slippage_error": "Sie können keinen Wert eingeben, der größer als {{value}} % ist.", "custom": "Benutzerdefiniert" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "Wallet-Wiederherstellung", "login_with_social": "Mit Social-Media-Konten anmelden", "setup": "Einrichten", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "Geheime Wiederherstellungsphrase {{num}}", "back_up": "Backup", "reveal": "Offenlegen", "social_recovery_title": "{{authConnection}}-WIEDERHERSTELLUNG", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "Passwort eingeben", "description": "Geben Sie Ihr Wallet-Passwort ein, um die Kartendetails anzuzeigen.", + "description_unfreeze": "Geben Sie Ihr Wallet-Passwort ein, um mit Ihrer Karte weiter bezahlen zu können.", "placeholder": "Passwort", "confirm": "Bestätigen", "cancel": "Stornieren", @@ -7001,6 +7039,7 @@ "enable_card_error": "Aktivierung der Karte fehlgeschlagen. Bitte versuchen Sie es später erneut.", "view_card_details_error": "Kartendaten konnten nicht geladen werden. Bitte versuchen Sie es erneut.", "biometric_verification_required": "Zur Anzeige der Kartendetails ist eine Authentifizierung erforderlich.", + "unfreeze_auth_required": "Zur Fortsetzung Ihrer Kartenzahlungen ist eine Authentifizierung erforderlich.", "warnings": { "close_spending_limit": { "title": "Sie sind Ihrem Ausgabenlimit nahe", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "Ihre Karte ist gesperrt", - "description": "Bitte wenden Sie sich an den Support, um Ihre Karte zu entsperren." + "description": "Ihre Karte ist vorübergehend gesperrt. Sie können die Sperre jederzeit entsperren." }, "blocked": { "title": "Ihre Karte ist blockiert", @@ -7068,7 +7107,14 @@ "travel_description": "Hotels mit bis zu 70 % Rabatt buchen", "card_tos_title": "Geschäftsbedingungen", "order_metal_card": "Metallkarte", - "order_metal_card_description": "Bestellen Sie jetzt Ihre physische Metallkarte" + "order_metal_card_description": "Bestellen Sie jetzt Ihre physische Metallkarte", + "freeze_card": "Karte sperren", + "unfreeze_card": "Karte entsperren", + "freeze_card_description": "Alle Ausgaben mit Ihrer Karte pausieren", + "unfreeze_card_description": "Alle Ausgaben mit Ihrer Karte fortsetzen", + "freeze_error": "Kartenstatus konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.", + "freeze_success": "Karte erfolgreich gesperrt", + "unfreeze_success": "Karte erfolgreich entsperrt" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "Erneutes Senden in {{seconds}} Sekunden verfügbar" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "Zu {{walletName}} hinzufügen", + "adding_to_wallet": "Hinzufügen zu {{walletName}} ...", + "continue_setup": "{{walletName}}-Einrichtung fortsetzen", + "wallet_not_available": "{{walletName}} nicht verfügbar", + "already_in_wallet": "Bereits in {{walletName}}", + "success_title": "Karte hinzugefügt!", + "success_message": "Ihre MetaMask-Karte wurde zu {{walletName}} hinzugefügt.", + "error_title": "Karte konnte nicht hinzugefügt werden", + "error_wallet_not_available": "{{walletName}} ist auf diesem Gerät nicht verfügbar. Bitte stellen Sie sicher, dass Sie {{walletName}} eingerichtet haben.", + "error_wallet_not_initialized": "{{walletName}} ist nicht initialisiert. Bitte richten Sie Ihre Wallet ein und versuchen Sie es erneut.", "error_card_already_in_wallet": "Diese Karte wurde bereits zu {{walletName}} hinzugefügt.", "error_card_pending": "Ihre Karte wird gerade in {{walletName}} eingerichtet. Schauen Sie bitte in ein paar Minuten wieder vorbei.", "error_card_suspended": "Ihre Karte in {{walletName}} wurde gesperrt. Wenden Sie sich für Unterstützung bitte an den Support.", "error_card_not_eligible": "Diese Karte ist nicht für die Bereitstellung einer mobilen Wallet geeignet.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "Die Verschlüsselung der Kartendaten ist fehlgeschlagen. Bitte versuchen Sie es erneut.", "error_invalid_card_data": "Ungültige Kartendaten. Bitte überprüfen Sie Ihre Kartendaten und versuchen Sie es erneut.", "error_card_not_found": "Karte nicht gefunden. Bitte versuchen Sie es erneut.", "error_card_provider_not_found": "Der Kartenanbieter ist für Ihre Region nicht verfügbar.", "error_card_id_mismatch": "Kartenüberprüfung fehlgeschlagen. Bitte versuchen Sie es erneut.", "error_card_not_active": "Ihre Karte ist nicht aktiv. Aktivieren Sie bitte zuerst Ihre Karte.", "error_network": "Ein Netzwerkfehler ist aufgetreten. Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "Die Anfrage ist abgelaufen. Bitte versuchen Sie es erneut.", + "error_server": "Es ist ein Serverfehler aufgetreten. Bitte versuchen Sie es später erneut.", + "error_unknown": "Es ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support.", + "error_platform_not_supported": "Diese Plattform unterstützt keine Bereitstellung von mobilen Wallets.", "try_again": "Erneut versuchen", "cancel": "Stornieren" } @@ -7299,7 +7345,7 @@ "main_title": "Belohnungen", "referral_title": "Empfehlungen", "tab_overview_title": "Übersicht", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "Schnappschüsse", "tab_activity_title": "Aktivität", "referral_stats_earned_from_referrals": "Durch Empfehlungen verdient", "referral_stats_referrals": "Empfehlungen", @@ -7353,7 +7399,7 @@ "verifying_rewards": "Wir stellen sicher, dass alles korrekt ist, bevor Sie Ihre Belohnungen einfordern." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "Verdiente Punkte" }, "onboarding": { "not_supported_region_title": "Region wird nicht unterstützt", @@ -7431,7 +7477,7 @@ "show_less": "Weniger anzeigen", "linking_progress": "Konten werden hinzufügen ... ({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}} angemeldet", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "Alle Konten hinzufügen" }, "referred_by_code": { "title": "Empfehlungscode", @@ -7514,7 +7560,7 @@ "claim_label": "Einfordern", "claimed_label": "Eingefordert", "reward_claimed": "Belohnung eingefordert", - "time_left": "{{time}} left", + "time_left": "{{time}} verbleibend", "expired": "Abgelaufen" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "Einlösung fehlgeschlagen", "redeem_failure_description": "Bitte versuchen Sie es später erneut.", "reward_details": "Belohnungsdetails", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "Wählen Sie das Konto aus, an das diese Belohnung gesendet werden soll." }, "animation": { "could_not_load": "Laden fehlgeschlagen" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", - "pill_calculating": "Calculating", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "starts_date": "Beginnt am {{date}}", + "ends_date": "Endet am {{date}}", + "results_coming_soon": "Ergebnisse folgen in Kürze", + "tokens_on_the_way": "Tokens sind unterwegs", + "pill_up_next": "Als Nächstes", + "pill_live_now": "Jetzt live", + "pill_calculating": "Berechnungsvorgang", + "pill_results_ready": "Ergebnisse bereit", + "pill_complete": "Abgeschlossen" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "Schnappschüsse", + "error_title": "Schnappschüsse konnten nicht geladen werden", + "error_description": "Die Schnappschüsse konnten nicht geladen werden. Bitte versuchen Sie es erneut.", "retry_button": "Erneut versuchen" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "Aktiv", + "upcoming_title": "Bevorstehend", + "previous_title": "Vorherige", + "empty_state": "Keine Schnappschüsse verfügbar", + "error_title": "Schnappschüsse konnten nicht geladen werden", + "error_description": "Die Schnappschüsse konnten nicht geladen werden. Bitte versuchen Sie es erneut.", "retry_button": "Erneut versuchen", - "refreshing": "Refreshing..." + "refreshing": "Aktualisierung ..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "{{approveSymbol}} genehmigen", "bridge_approval_loading": "Genehmigen", "bridge_send": "{{sourceSymbol}} von {{sourceChain}} bridgen", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "Bridge gesendet", "bridge_receive": "{{targetSymbol}} auf {{targetChain}} empfangen", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "Bridge empfangen", "default": "Transaktion", "musd_convert_send": "{{sourceSymbol}} von {{sourceChain}} gesendet", "musd_claim": "mUSD beanspruchen", @@ -7607,20 +7653,20 @@ "description": "Verbindungsherstellung mit {{dappName}} ..." }, "show_error": { - "title": "Connection error", + "title": "Verbindungsfehler", "description": "Verbindungsherstellung fehlgeschlagen. Bitte versuchen Sie es erneut." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "Genehmigung abgelehnt", + "description": "Der Nutzer hat die Anfrage abgelehnt." }, "show_return_to_app": { "title": "Erfolg", "description": "Kehren Sie zur App zurück, um fortzufahren." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "Verbindung nicht gefunden", + "description": "Bitte stellen Sie über die App eine neue Verbindung her, um fortzufahren." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "Entdecken", + "trending_tokens": "Trendige Tokens", "price_change": "Preisänderung", "all_networks": "Alle Netzwerke", - "24h": "24h", + "24h": "24 Stunden", "time": "Zeit", "24_hours": "24 Stunden", "6_hours": "6 Stunden", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 Stunde", + "5_minutes": "5 Minuten", "networks": "Netzwerke", "sort_by": "Sortieren nach", "volume": "Volumen", @@ -7650,32 +7696,48 @@ "high_to_low": "Hoch bis niedrig", "low_to_high": "Niedrig bis hoch", "apply": "Anwenden", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "Tokens, Websites, URLs suchen", "cancel": "Stornieren", "perps": "Perps", "predictions": "Prognosen", - "no_results": "No results found", + "no_results": "Keine Ergebnisse gefunden", "sites": "Websites", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "Beliebte Websites", + "search_sites": "Websites durchsuchen", + "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.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "Trendige Tokens sind nicht verfügbar", + "description": "Diese Seite kann momentan nicht abgerufen werden.", "try_again": "Erneut versuchen" }, "empty_search_result_state": { "title": "Keine Tokens gefunden", - "description": "We were not able to find this token" + "description": "Wir konnten dieses Token nicht finden." } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "Update bereit", + "description_ios": "Wir haben einige wichtige Fehler behoben. Laden Sie die Seite neu, um die neueste Version von MetaMask zu sehen.", + "description_android": "Wir haben einige wichtige Fehler behoben. Schließen Sie MetaMask und öffnen Sie es erneut, um das Update anzuwenden.", "primary_action_reload": "Erneut laden", "primary_action_acknowledge": "Verstanden" + }, + "homepage": { + "sections": { + "tokens": "Token", + "perpetuals": "Perpetuals", + "predictions": "Prognosen", + "defi": "DeFi", + "nfts": "NFTs", + "import_nfts": "NFTs importieren", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/el.json b/locales/languages/el.json index a970cfd7251..fae042f2886 100644 --- a/locales/languages/el.json +++ b/locales/languages/el.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "διαγραφούν οριστικά", "reset_wallet_desc_2": "από το MetaMask σε αυτήν τη συσκευή. Η ενέργεια αυτή δεν μπορεί να αναιρεθεί.", "reset_wallet_desc_login": "Για να επαναφέρετε το πορτοφόλι σας, μπορείτε να χρησιμοποιήσετε τη Μυστική Φράση Ανάκτησης ή τον κωδικό πρόσβασης του λογαριασμού σας Google ή Apple. Το MetaMask δεν διαθέτει αυτές τις πληροφορίες.", - "reset_wallet_desc_srp": "Για να επαναφέρετε το πορτοφόλι σας, βεβαιωθείτε ότι διαθέτετε τη Μυστική Φράση Ανάκτησης. Το MetaMask δεν διαθέτει αυτές τις πληροφορίες." + "reset_wallet_desc_srp": "Για να επαναφέρετε το πορτοφόλι σας, βεβαιωθείτε ότι διαθέτετε τη Μυστική Φράση Ανάκτησης. Το MetaMask δεν διαθέτει αυτές τις πληροφορίες.", + "biometric_authentication_cancelled": "Η επαλήθευση βιομετρικών δεδομένων ακυρώθηκε", + "biometric_authentication_cancelled_title": "Η ρύθμιση βιομετρικών δεδομένων απέτυχε", + "biometric_authentication_cancelled_description": "Παρακαλούμε ρυθμίστε εκ νέου τον έλεγχο ταυτότητας βιομετρικών δεδομένων από τις ρυθμίσεις.", + "biometric_authentication_cancelled_button": "Επιβεβαίωση" }, "connect_hardware": { "title_select_hardware": "Συνδέστε ένα πορτοφόλι υλικού", @@ -1040,7 +1044,7 @@ "title": "Ποσό προς κατάθεση", "get_usdc_hyperliquid": "Αποκτήστε USDC • Hyperliquid", "insufficient_funds": "Ανεπαρκές ποσό", - "no_funds_available": "Δεν υπάρχουν διαθέσιμα κεφάλαια. Παρακαλούμε καταθέστε πρώτα.", + "no_funds_available": "Δεν υπάρχουν αρκετά διαθέσιμα χρήματα. Καταθέστε χρήματα ή επιλέξτε διαφορετική μέθοδο πληρωμής", "enter_amount": "Εισαγωγή ποσού", "fetching_quote": "Λήψη προσφοράς", "submitting": "Υποβολή συναλλαγής", @@ -1970,8 +1974,8 @@ "trade_again": "Επανάληψη συναλλαγής", "activity": { "deposit_title": "Κατάθεση", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "Κατατέθηκαν {{amount}} {{symbol}}", + "withdrew_amount": "Αναλήφθηκαν {{amount}} {{symbol}}", "status_completed": "Ολοκληρώθηκε", "status_failed": "Απέτυχε", "status_pending": "Εκκρεμεί" @@ -2051,6 +2055,16 @@ "referral_code_text": "Χρησιμοποιήστε τον κωδικό παραπομπής μου για να κερδίσετε επιπλέον ανταμοιβές." } }, + "market_insights": { + "title": "Πληροφορίες αγοράς", + "updated_ago": "Ενημερώθηκε {{time}}", + "disclaimer": "Πληροφορίες ΤΝ. Όχι οικονομικές συμβουλές.", + "whats_driving_price": "Τι καθορίζει την τιμή;", + "what_people_saying": "Τι λένε οι άνθρωποι", + "trade_button": "Συναλλαγές", + "sources_count": "+{{count}} πηγές", + "sources_title": "Πηγές ρευστοποίησης" + }, "predict": { "title": "Προβλέψεις στο MetaMask", "prediction_markets": "Αγορές προβλέψεων", @@ -2384,8 +2398,8 @@ "no_available_tokens": "Δεν βλέπετε το token σας;", "add_tokens": "Εισαγωγή tokens", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "{{tokenCount}} νέο {{tokensLabel}} βρέθηκε σε αυτόν τον λογαριασμό", "token_toast": { "tokens_imported_title": "Εισαγόμενα tokens", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "Τα δεκαδικά ψηφία του token δεν μπορεί να είναι κενά.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "Δεν μπορέσαμε να βρούμε κανένα token με αυτό το όνομα.", + "tokens_empty_description": "Search for any token and import it", "select_token": "Επιλέξτε token", "address_must_be_smart_contract": "Εντοπίστηκε προσωπική διεύθυνση. Εισαγάγετε τη διεύθυνση συμβολαίου του token.", "billion_abbreviation": "Δ", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "Αποσύνδεση όλων των λογαριασμών", "deceptive_site_ahead": "Παραπλανητικός ιστότοπος εν όψει", "deceptive_site_desc": "Ο ιστότοπος που προσπαθείτε να επισκεφθείτε δεν είναι ασφαλής. Οι εισβολείς μπορεί να σας εξαπατήσουν για να κάνουν κάτι επικίνδυνο.", + "malicious_site_detected": "Εντοπίστηκε κακόβουλος ιστότοπος", + "malicious_site_warning": "Εάν συνδεθείτε σε αυτόν τον ιστότοπο, ενδέχεται να χάσετε όλα τα περιουσιακά σας στοιχεία.", + "connect_anyway": "Σύνδεση ούτως ή άλλως", "learn_more": "Μάθετε περισσότερα", "advisory_by": "Συμβουλές που παρέχονται από το Ethereum Phishing Detector και PhishFort", "potential_threat": "Πιθανές απειλές περιλαμβάνουν", @@ -2846,7 +2864,11 @@ "permissions": "Άδειες", "card_title": "MetaMask Card", "settings": "Ρυθμίσεις", - "log_out": "Αποσύνδεση" + "networks": "Δίκτυα", + "log_out": "Αποσύνδεση", + "notifications": "Ειδοποιήσεις", + "buy": "Αγορά", + "scan": "Σάρωση" }, "app_settings": { "enabling_notifications": "Ενεργοποίηση ειδοποιήσεων...", @@ -2870,6 +2892,8 @@ "state_logs": "Αρχεία καταγραφής κατάστασης", "add_network_title": "Προσθήκη δικτύου", "auto_lock": "Αυτόματο κλείδωμα", + "enable_device_authentication": "Ενεργοποίηση ελέγχου ταυτότητας συσκευής", + "enable_device_authentication_desc": "Χρησιμοποιήστε τα βιομετρικά δεδομένα ή τον κωδικό πρόσβασης της συσκευής σας για να ξεκλειδώσετε το MetaMask.", "auto_lock_desc": "Επιλέξτε το χρονικό διάστημα πριν κλειδώσει αυτόματα η εφαρμογή.", "state_logs_desc": "Αυτό θα βοηθήσει το MetaMask να εντοπίσει προβλήματα που ενδέχεται να αντιμετωπίσετε. Στείλτε τα στην υποστήριξη του MetaMask μέσω του εικονιδίου χάμπουργκερ > Αποστολή Σχολίων ή απαντήστε στο υπάρχον αίτημα υποστήριξης, αν έχετε.", "autolock_immediately": "Αμέσως", @@ -2975,6 +2999,11 @@ "add_rpc_url": "Προσθήκη διεύθυνσης URL του RPC", "add_block_explorer_url": "Προσθήκη διεύθυνσης URL στο block explorer", "networks_desc": "Προσθέστε και επεξεργαστείτε προσαρμοσμένα δίκτυα RPC", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "Αναζήτηση δικτύων", + "networks_no_results": "Δεν βρέθηκαν δίκτυα", "network_name_label": "Όνομα δικτύου", "network_name_placeholder": "Όνομα δικτύου (προαιρετικά)", "network_rpc_url_label": "Διεύθυνση URL του RPC", @@ -2991,7 +3020,16 @@ "network_other_networks": "Άλλα δίκτυα", "network_rpc_networks": "Δίκτυα RPC", "network_add_network": "Προσθήκη δικτύου", + "add_chain_title": "Προσθήκη δικτύου", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "Επανάληψη", + "add_chain_added": "Added", + "add_chain_or": "ή", + "add_chain_custom_link": "Προσθήκη προσαρμοσμένου δικτύου", "network_add_custom_network": "Προσθήκη προσαρμοσμένου δικτύου", + "network_add_test_network": "Add a test network", "network_add": "Προσθήκη", "network_save": "Αποθήκευση", "remove_network_title": "Θέλετε να αφαιρέσετε αυτό το δίκτυο;", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "Εντάξει", "title": "Ο λογαριασμός δεν μπόρεσε να συνδεθεί", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "Σαρώστε τον κωδικό QR στον ιστότοπο για να συνδεθείτε εκ νέου στο MetaMask" }, "app_information": { "title": "Πληροφορίες", @@ -3379,6 +3417,7 @@ "sell_description": "Πώληση κρυπτονομισμάτων για μετρητά" }, "asset_overview": { + "market_closed": "Κλειστή αγορά", "send_button": "Αποστολή", "buy_button": "Αγορά", "cash_buy_button": "Αγορά με μετρητά", @@ -3399,19 +3438,6 @@ "bridge": "Διασύνδεση", "earn": "Κερδίστε", "convert_to_musd": "Μετατροπή σε mUSD", - "merkl_rewards": { - "annual_bonus": "{{apy}}% μπόνους", - "claimable_bonus": "Μπόνους προς εξαργύρωση", - "claimable_bonus_tooltip_description": "Τα μπόνους σε mUSD εξαργυρώνονται στο δίκτυο Linea.", - "terms_apply": "Ισχύουν όροι.", - "ok": "Εντάξει", - "claim": "Εξαργύρωση", - "processing_claim": "Επεξεργασία αιτήματος…", - "claim_on_linea_title": "Εξαργυρώστε τα μπόνους στο Linea", - "claim_on_linea_description": "Το μπόνους σας θα πιστωθεί στο Linea, ξεχωριστά από το υπόλοιπό σας σε mUSD στο Ethereum.", - "continue": "Συνεχίστε", - "unexpected_error": "Απρόσμενο σφάλμα. Παρακαλούμε δοκιμάστε ξανά." - }, "tron": { "daily_resource_new_energy": "Νέα ημερήσια ενέργεια", "sufficient_to_cover": "Επαρκές για να καλύψει", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "Η διεύθυνση του token αντιγράφηκε στο πρόχειρο" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "Μη έγκυρος κωδικός QR", "invalid_qr_code_message": "Ο κωδικός QR που προσπαθείτε να σαρώσετε δεν είναι έγκυρος.", "allow_camera_dialog_title": "Επιτρέψτε την πρόσβαση στην κάμερα", "allow_camera_dialog_message": "Χρειαζόμαστε την άδειά σας για να σαρώσουμε κωδικούς QR", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "Φαίνεται ότι προσπαθείτε να συγχρονιστείτε με την επέκταση. Για να το κάνετε αυτό, θα πρέπει να διαγράψετε το τρέχον πορτοφόλι σας. \n\nΑφού διαγράψετε ή εγκαταστήσετε εκ νέου μια νέα έκδοση της εφαρμογής, επιλέξτε «Συγχρονισμός με την Επέκταση του MetaMask». Σημαντικό! Πριν διαγράψετε το πορτοφόλι σας, σιγουρευτείτε ότι έχετε δημιουργήσει αντίγραφο ασφαλείας της Μυστικής Φράσης Ανάκτησης.", "not_allowed_error_title": "Ενεργοποίηση της πρόσβασης στην κάμερα", "not_allowed_error_desc": "Για να σαρώσετε έναν κωδικό QR, θα πρέπει να δώσετε πρόσβαση στην κάμερα στο MetaMask από το μενού ρυθμίσεων της συσκευής σας.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "Μη αναγνωρίσιμος κωδικός QR", "unrecognized_address_qr_code_desc": "Λυπούμαστε, αυτός ο κωδικός QR δεν συνδέεται με μια διεύθυνση λογαριασμού ή διεύθυνση συμβολαίου.", "url_redirection_alert_title": "Πρόκειται να ανοίξετε έναν εξωτερικό σύνδεσμο", "url_redirection_alert_desc": "Οι σύνδεσμοι μπορούν να χρησιμοποιηθούν με σκοπό να παραπλανήσουν ή να εξαπατήσουν τους χρήστες, γι' αυτό φροντίστε να επισκέπτεστε μόνο ιστότοπους που εμπιστεύεστε.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "Δεν σας ανήκει αυτό το συλλεκτικό στοιχείο", "known_asset_contract": "Γνωστή διεύθυνση συμβολαίου περιουσιακού στοιχείου", "max": "Μέγ", - "recipient_address": "Recipient address", + "recipient_address": "Διεύθυνση παραλήπτη", "required": "Απαιτείται", "to": "Προς", "total": "Σύνολο", @@ -3641,7 +3667,7 @@ "nevermind": "Άκυρο", "edit_network_fee": "Επεξεργασία τέλους συναλλαγής", "edit_priority": "Επεξεργασία προτεραιότητας", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "Τέλος ακύρωσης συναλλαγής", "gas_speedup_fee": "Τέλος επιτάχυνσης συναλλαγής", "use_max": "Χρήση μέγιστου", "set_gas": "Ορισμός", @@ -3650,7 +3676,7 @@ "transaction_fee": "Τέλος Συναλλαγής", "transaction_fee_less": "Χωρίς τέλος συναλλαγής", "total_amount": "Συνολικό ποσό", - "view_data": "View data", + "view_data": "Προβολή Δεδομένων", "adjust_transaction_fee": "Προσαρμογή τέλους συναλλαγής", "could_not_resolve_ens": "Δεν ήταν δυνατή η επίλυση του ENS", "asset": "Περιουσιακό στοιχείο", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "Για να περιηγηθείτε στον αποκεντρωμένο ιστό, προσθέστε μια νέα καρτέλα", "got_it": "Κατανοητό", "max_tabs_title": "Επιτεύχθηκε ο μέγιστος αριθμός καρτελών", - "max_tabs_desc": "Προς το παρόν υποστηρίζουμε μόνο 5 ανοιχτές καρτέλες ταυτόχρονα. Κλείστε τις υπάρχουσες καρτέλες πριν προσθέσετε νέες.", + "max_tabs_desc": "Προς το παρόν υποστηρίζουμε μόνο 20 ανοιχτές καρτέλες ταυτόχρονα. Κλείστε τις υπάρχουσες καρτέλες πριν προσθέσετε νέες.", "failed_to_resolve_ens_name": "Δεν μπορέσαμε να επιλύσουμε αυτό το όνομα ENS", "remove_bookmark_title": "Αφαίρεση αγαπημένου", "remove_bookmark_msg": "Θέλετε πραγματικά να αφαιρέσετε αυτή την ιστοσελίδα από τα αγαπημένα σας;", @@ -3828,7 +3854,7 @@ "cancel_button": "Άκυρο" }, "approval": { - "title": "Confirm transaction" + "title": "Επιβεβαίωση Συναλλαγής" }, "approve": { "title": "Έγκριση", @@ -3839,39 +3865,39 @@ "unavailable": "Μη διαθέσιμο", "tx_review_confirm": "Επιβεβαίωση", "tx_review_transfer": "Μεταφορά", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "Ανάπτυξη Συμβολαίου", + "tx_review_transfer_from": "Μεταφορά Από", + "tx_review_unknown": "Άγνωστη Μέθοδος", "tx_review_approve": "Έγκριση", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "Αύξηση των αποδοχών", + "tx_review_set_approval_for_all": "Ορίστε την έγκριση για όλους", + "tx_review_staking_claim": "Διεκδίκηση πονταρίσματος", "tx_review_staking_deposit": "Κατάθεση για δέσμευση κρυπτονομισμάτων", "tx_review_staking_unstake": "Ακύρωση πονταρίσματος", "tx_review_lending_deposit": "Κατάθεση για δανεισμό", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "Ανάληψη Δανεισμού", "tx_review_perps_deposit": "Χρηματοδοτούμενα perps", "tx_review_predict_deposit": "Χρηματοδοτούμενες προβλέψεις", "tx_review_predict_claim": "Κατοχυρωμένες νίκες", "tx_review_predict_withdraw": "Ανάληψη προβλέψεων", "tx_review_musd_conversion": "Μετατροπή σε mUSD", "claim": "Εξαργύρωση", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "Αποστολή ETH", + "self_sent_ether": "Στείλτε ETH στον εαυτό σας", + "received_ether": "Ληφθέντα ETH", "sent_dai": "Αποστολή DAI", "self_sent_dai": "Στείλτε DAI στον εαυτό σας", "received_dai": "Ληφθέντα DAI", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "Απεσταλμένα token", + "received_tokens": "Ληφθέντα token", "ether": "ETH", "sent_unit": "Στείλτε {{unit}}", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "Στείλτε {{unit}} στον εαυτό σας", "received_unit": "Λήφθησαν {{unit}}", "sent_collectible": "Στάλθηκαν συλλεκτικά tokens", "received_collectible": "Παραλήφθησαν συλλεκτικά tokens", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "Αποστολή ETH", + "send_unit": "Στείλτε {{unit}}", "send_collectible": "Αποστολή συλλεκτικών tokens", "receive_collectible": "Λήψη συλλεκτικών tokens", "sent": "Εστάλη", @@ -3881,17 +3907,17 @@ "send": "Αποστολή", "redeposit": "Κατάθεση εκ νέου", "interaction": "Αλληλεπίδραση", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "Ανάπτυξη Συμβολαίου", + "to_contract": "Νέο Συμβόλαιο", + "mint": "Έκδοση", "tx_details_free": "Δωρεάν", "tx_details_not_available": "Μη διαθέσιμο", "smart_contract_interaction": "Αλληλεπίδραση έξυπνου συμβολαίου", "swaps_transaction": "Συναλλαγή ανταλλαγής", "bridge_transaction": "Διασύνδεση", "approve": "Έγκριση", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "Αύξηση των αποδοχών", + "set_approval_for_all": "Ορίστε την έγκριση για όλους", "hash": "Κατακερματισμός", "from": "Από", "to": "Προς", @@ -3899,15 +3925,15 @@ "amount": "Ποσό", "fee": { "transaction_fee_in_ether": "Τέλος συναλλαγής", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "Τέλη συναλλαγών (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "Τέλη συναλλαγών που χρησιμοποιήθηκαν (μονάδες)", + "gas_limit": "Όριο τελών συναλλαγής (Μονάδες)", + "gas_price": "Τιμή τελών συναλλαγής (GWEI)", + "base_fee": "Βασικό Τέλος (GWEI)", + "priority_fee": "Τέλος προτεραιότητας (GWEI)", "multichain_priority_fee": "Τέλος προτεραιότητας", - "max_fee": "Max fee per gas", + "max_fee": "Μέγιστη χρέωση ανά τέλος", "total": "Σύνολο", "view_on": "Προβολη στο", "view_on_etherscan": "Προβολή στο Etherscan", @@ -3923,13 +3949,13 @@ "nonce": "Αριθμός συναλλαγής (nonce)", "from_device_label": "από αυτή τη συσκευή", "import_wallet_row": "Λογαριασμός που προστέθηκε σε αυτή τη συσκευή", - "import_wallet_label": "Account added", + "import_wallet_label": "Προστέθηκε λογαριασμός", "import_wallet_tip": "Όλες οι μελλοντικές συναλλαγές που θα πραγματοποιούνται από αυτή τη συσκευή θα περιλαμβάνουν την ένδειξη «από αυτή τη συσκευή» δίπλα στη χρονοσήμανση. Για συναλλαγές που χρονολογούνται πριν από την προσθήκη του λογαριασμού, το ιστορικό αυτό δεν θα υποδεικνύει ποιες εξερχόμενες συναλλαγές προέρχονται από αυτή τη συσκευή.", "sign_title_scan": "Σάρωση ", "sign_title_device": "με το πορτοφόλι υλικολογισμικού", "sign_description_1": "Αφού υπογράψετε με το πορτοφόλι υλικολογισμικού σας,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "Πατήστε λήψη υπογραφής", + "sign_get_signature": "Λήψη υπογραφής", "transaction_id": "Αναγνωριστικό συναλλαγής", "network": "Δίκτυο", "request_from": "Ζητήθηκε από", @@ -4032,7 +4058,7 @@ "title": "Δίκτυα", "other_networks": "Άλλα δίκτυα", "close": "Κλείσιμο", - "status_ok": "All systems operational", + "status_ok": "Όλα τα συστήματα λειτουργούν", "status_not_ok": "Το δίκτυο έχει κάποια προβλήματα", "want_to_add_network": "Θέλετε να προσθέσετε αυτό το δίκτυο;", "add_custom_network": "Προσθήκη προσαρμοσμένου δικτύου", @@ -4051,7 +4077,7 @@ "review": "Επισκόπηση", "view_details": "Δείτε λεπτομέρειες", "network_details": "Λεπτομέρειες δικτύου", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "Η επιλογή επιβεβαίωσης ενεργοποιεί τον έλεγχο λεπτομερειών δικτύου. Μπορείτε να απενεργοποιήσετε τον έλεγχο λεπτομερειών δικτύου στο ", "network_settings_security_privacy": "Ρυθμίσεις > Ασφάλεια και απόρρητο", "network_currency_symbol": "Σύμβολο νομίσματος", "network_block_explorer_url": "Διεύθυνση URL του Block Explorer", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "Ένας κακόβουλος πάροχος δικτύου μπορεί να πει ψέματα για την κατάσταση του blockchain και να καταγράψει τη δραστηριότητα του δικτύου σας. Να προσθέτετε μόνο προσαρμοσμένα δίκτυα που εμπιστεύεστε.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Πληροφορίες δικτύου", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "Πρόσθετες πληροφορίες δικτύων", "network_warning_desc": "Αυτή η σύνδεση δικτύου βασίζεται σε τρίτους. H σύνδεση ενδέχεται να είναι λιγότερο αξιόπιστη ή να επιτρέπει σε τρίτους να παρακολουθούν τη δραστηριότητα.", "additonial_network_information_desc": "Ορισμένα από αυτά τα δίκτυα βασίζονται σε τρίτους. Οι συνδέσεις μπορεί να είναι λιγότερο αξιόπιστες ή να επιτρέπουν σε τρίτους να παρακολουθούν τη δραστηριότητα.", "connect_more_networks": "Σύνδεση περισσότερων δικτύων", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "Αυτό το δίκτυο έχει καταργηθεί", "network_deprecated_description": "Το δίκτυο στο οποίο προσπαθείτε να συνδεθείτε δεν υποστηρίζεται πλέον από το MetaMask.", "edit_networks_title": "Επεξεργασία δικτύων", - "no_network_fee": "No network fee" + "no_network_fee": "Χωρίς χρέωση δικτύου" }, "permissions": { "title_this_site_wants_to": "Αυτός ο ιστότοπος θέλει να:", @@ -4111,11 +4137,11 @@ "network_connected": "δίκτυο συνδεδεμένο ", "see_your_accounts": "Δείτε τους λογαριασμούς σας και προτείνετε συναλλαγές", "connected_to": "Συνδέθηκε σε ", - "manage_permissions": "Manage permissions", + "manage_permissions": "Διαχείριση αδειών", "edit": "Επεξεργασία", "cancel": "Ακύρωση", "got_it": "Κατανοητό", - "connection_details_title": "Connection details", + "connection_details_title": "Λεπτομέρειες σύνδεσης", "connection_details_description": "Συνδεθήκατε σε αυτόν τον ιστότοπο χρησιμοποιώντας το πρόγραμμα περιήγησης του MetaMask στις {{connectionDateTime}}", "title_add_network_permission": "Προσθήκη άδειας χρήσης δικτύου", "add_this_network": "Προσθήκη αυτού του δικτύου", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "Ξεκλείδωμα με PIN συσκευής;" }, "authentication": { - "auth_prompt_title": "Απαιτείται ταυτοποίηση", - "auth_prompt_desc": "Παρακαλούμε κάντε έλεγχο ταυτοποίησης για να χρησιμοποιήσετε το MetaMask", - "fingerprint_prompt_title": "Απαιτείται ταυτοποίηση", - "fingerprint_prompt_desc": "Χρησιμοποιήστε το δακτυλικό σας αποτύπωμα για να ξεκλειδώσετε το MetaMask", - "fingerprint_prompt_cancel": "Άκυρο" + "auth_prompt_desc": "Παρακαλούμε κάντε έλεγχο ταυτοποίησης για να χρησιμοποιήσετε το MetaMask" }, "accountApproval": { "title": "ΑΙΤΗΜΑ ΣΥΝΔΕΣΗΣ", "walletconnect_title": "ΑΙΤΗΜΑ WALLETCONNECT", "action": "Σύνδεση σε αυτήν την ιστοσελίδα;", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "Για να συνεχίσετε τη σύνδεση, επιλέξτε τον αριθμό που βλέπετε στον ιστότοπο", + "action_reconnect_deeplink": "Θέλετε να επανασυνδεθείτε σε αυτόν τον ιστότοπο;", "connect": "Σύνδεση", "resume": "Συνέχεια ", "cancel": "Άκυρο", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "Δεν θυμάμαι αυτήν τη σύνδεση ιστότοπου", "disconnect": "Αποσύνδεση", "permission": "Δείτε τη", "address": "δημόσια διεύθυνσή σας", @@ -4218,7 +4240,7 @@ "error_title": "Κάτι πήγε στραβά", "error_message": "Δεν μπορούσαμε να εισάγουμε αυτό το ιδιωτικό κλειδί. Βεβαιωθείτε ότι το καταχωρήσατε σωστά.", "error_empty_message": "Πρέπει να καταχωρήσετε το ιδιωτικό σας κλειδί.", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "ή σαρώστε έναν κωδικό QR" }, "import_private_key_success": { "title": "Ο λογαριασμός εισήχθη με επιτυχία!", @@ -4229,18 +4251,18 @@ "import_wallet_title": "Εισαγωγή πορτοφολιού", "enter_srp_subtitle": "Εισάγετε τη Μυστική Φράση Ανάκτησης", "textarea_placeholder": "Προσθέστε ένα κενό ανάμεσα σε κάθε λέξη και βεβαιωθείτε ότι κανείς δεν σας παρακολουθεί", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "Εισαγάγετε τη Μυστική Φράση Ανάκτησης του πορτοφολιού σας. Μπορείτε να εισαγάγετε οποιαδήποτε Μυστική Φράση Ανάκτησης Ethereum, Solana ή Bitcoin.", + "subtitle": "Επικολλήστε τη Μυστική Φράση Ανάκτησής σας", "cta_text": "Συνεχίστε", "paste": "Επικόλληση", "clear": "Εκκαθάριση όλων", "srp_number_of_words_option_title": "Αριθμός λέξεων", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "Έχω μια φράση 12 λέξεων", + "24_word_option": "Έχω μια φράση 24 λέξεων", "error_title": "Κάτι πήγε στραβά", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "Δεν ήταν δυνατή η εισαγωγή αυτής της Μυστικής Φράσης Ανάκτησης. Βεβαιωθείτε ότι την εισαγάγατε σωστά.", + "error_empty_message": "Πρέπει να εισαγάγετε τη Μυστική Φράση Ανάκτησης.", + "error_number_of_words_error_message": "Οι Μυστικες Φράσεις Ανάκτησης περιέχουν 12 ή 24 λέξεις", "error_srp_is_case_sensitive": "Μη έγκυρη καταχώριση! Η Μυστική Φράση Ανάκτησης διακρίνει τα πεζά/κεφαλαία γράμματα.", "error_srp_word_error_1": "Η λέξη ", "error_srp_word_error_2": " είναι λανθασμένη ή ανορθόγραφη.", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " είναι λανθασμένα ή ανορθόγραφα.", "error_invalid_srp": "Μη έγκυρη Μυστική Φράση Ανάκτησης", "error_duplicate_srp": "Έχει ήδη γίνει εισαγωγή αυτής της Μυστικής Φράσης Ανάκτησης.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "Ο λογαριασμός που προσπαθείτε να εισαγάγετε είναι διπλότυπος.", + "invalid_qr_code_title": "Μη έγκυρος κωδικός QR", + "invalid_qr_code_message": "Ο κωδικός QR δεν περιέχει έγκυρη φράση μυστικής ανάκτησης", "success_1": "Πορτοφόλι", "success_2": "έγινε εισαγωγή" }, @@ -4665,7 +4687,7 @@ "button": "Προστασία πορτοφολιού" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "Η ενημέρωση της συναλλαγής απέτυχε", "text": "Θέλετε να προσπαθήσετε ξανά;", "cancel_button": "Άκυρο", "retry_button": "Ξαναδοκιμάστε" @@ -4684,13 +4706,13 @@ "next": "Επόμενο", "amount_placeholder": "0,00", "link_copied": "Ο σύνδεσμος αντιγράφηκε στο πρόχειρο", - "send_link_title": "Send link", + "send_link_title": "Αποστολή συνδέσμου", "description_1": "Ο σύνδεσμος του αιτήματός σας είναι έτοιμος για αποστολή!", "description_2": "Στείλτε αυτόν τον σύνδεσμο σε έναν φίλο και θα του ζητήσει να στείλει", "copy_to_clipboard": "Αντιγραφή στο πρόχειρο", "qr_code": "Κωδικός QR", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "Αποστολή συνδέσμου", + "request_qr_code": "Κωδικός QR αιτήματος πληρωμής", "balance": "Υπόλοιπο" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "Δεν έχετε ενεργές συνεδρίες", - "end_session_title": "End session", + "end_session_title": "Λήξη συνεδρίας", "end": "Τέλος", "cancel": "Άκυρο", - "session_ended_title": "Session ended", + "session_ended_title": "Η συνεδρία έληξε", "session_ended_desc": "Η επιλεγμένη συνεδρία τερματίστηκε", "session_already_exist": "Αυτή η συνεδρία είναι ήδη συνδεδεμένη.", "close_current_session": "Κλείστε την τρέχουσα συνεδρία πριν ξεκινήσετε μια νέα." @@ -4765,15 +4787,14 @@ "on_network": "στο {{networkName}}", "debit_card": "Χρεωστική κάρτα", "select_payment_method": "Επιλέξτε μέθοδο πληρωμής", - "loading_quote": "Loading quote...", "pay_with": "Πληρωμή με", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "Αγορά μέσω {{providerName}}.", + "change_provider": "Αλλαγή παρόχου.", "payment_error": "Κάτι πήγε στραβά. Παρακαλούμε προσπαθήστε ξανά.", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "Δεν υπάρχουν διαθέσιμες μέθοδοι πληρωμής.", "error_fetching_quotes": "Κάτι πήγε στραβά. Παρακαλούμε προσπαθήστε ξανά.", "no_quotes_available": "Δεν υπάρχουν διαθέσιμοι πάροχοι.", - "providers": "Providers", + "providers": "Πάροχοι", "continue": "Συνεχίστε", "powered_by_provider": "Με την υποστήριξη του {{provider}}", "purchased_currency": "Αγοράστηκαν {{currency}}", @@ -4871,6 +4892,15 @@ "log_out": "Αποσύνδεση από το {{provider}}", "logged_out_success": "Αποσυνδεθήκατε με επιτυχία", "logged_out_error": "Σφάλμα κατά την αποσύνδεση" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "κατώτατο όριο πώλησης", "medium_sell_limit": "μέτριο όριο πώλησης", "highest_sell_limit": "ανώτατο όριο πώλησης", - "change": "Change", + "change": "Αλλαγή", "continue_to_amount": "Συνέχεια στο ποσό", "no_payment_methods_title": "Δεν υπάρχουν διαθέσιμες μέθοδοι πληρωμής στην {{regionName}}", "no_cash_destinations_title": "Δεν υπάρχουν προορισμοί μετρητών στην {{regionName}}", @@ -5118,7 +5148,7 @@ "start_swapping": "Ξεκινήστε τις ανταλλαγές" }, "feature_off_title": "Προσωρινά μη διαθέσιμο", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "Οι ανταλλαγές MetaMask βρίσκονται υπό συντήρηση. Ελέγξτε ξανά αργότερα.", "wrong_network_title": "Οι ανταλλαγές δεν είναι διαθέσιμες", "wrong_network_body": "Μπορείτε να ανταλλάξετε tokens μόνο στο Κύριο Δίκτυο του Ethereum.", "unallowed_asset_title": "Δεν είναι δυνατή η ανταλλαγή αυτού του token", @@ -5160,7 +5190,7 @@ "not_enough": "Δεν υπάρχουν αρκετά {{symbol}} για να ολοκληρωθεί η ανταλλαγή", "max_slippage": "Μέγιστη απόκλιση", "max_slippage_amount": "Μέγιστη απόκλιση {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "Εάν η τιμή αλλάξει μεταξύ της στιγμής υποβολής της παραγγελίας σας και της στιγμής επιβεβαίωσης, αυτό ονομάζεται «απόκλιση». Η ανταλλαγή σας θα ακυρωθεί αυτόματα εάν η απόκλιση υπερβεί τη ρύθμιση «μέγιστη απόκλιση» που έχετε ορίσει.", "slippage_warning": "Βεβαιωθείτε ότι γνωρίζετε τι κάνετε!", "allows_up_to_decimals": "Το {{symbol}} επιτρέπει έως και {{decimals}} δεκαδικά", "get_quotes": "Πάρτε προσφορές", @@ -5199,7 +5229,7 @@ "edit": "Επεξεργασία", "quotes_include_fee": "Οι προσφορές περιλαμβάνουν μια χρέωση {{fee}}% στο MetaMask", "quotes_include_gas_and_metamask_fee": "Η προσφορά περιλαμβάνει τέλη συναλλαγών και μια χρέωση {{fee}}% στο MetaMask", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "Πατήστε για ανταλλαγή", "swipe_to_swap": "Σύρετε για ανταλλαγή", "swipe_to": "Σύρετε για", "swap": "Ανταλλαγή", @@ -5259,7 +5289,7 @@ "approve": "Έγκριση {{sourceToken}} για ανταλλαγές: Μέχρι και {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "Εκκρεμής ανταλλαγή ({{sourceToken}} σε {{destinationToken}})", "swap_confirmed": "Ολοκλήρωση Ανταλλαγής ({{sourceToken}} σε {{destinationToken}})", "approve_pending": "Έγκριση {{sourceToken}} για ανταλλαγές", "approve_confirmed": "{{sourceToken}} εγκρίθηκαν για ανταλλαγές" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "Το μενού επιλογής δικτύου μεταφέρθηκε στα οικονομικά σας στοιχεία", "description_2": "Ανταλλαγή και Μεταφορά σε μία απλή διαδικασία", - "description_3": "Streamlined send experience", + "description_3": "Βελτιστοποιημένη εμπειρία αποστολής", "description_4": "Ανανεωμένη εμφάνιση λογαριασμού" }, "more_information": "Τώρα μπορείτε να εστιάσετε στα token και τη δραστηριότητά σας, χωρίς να σας απασχολούν τα δίκτυα στο παρασκήνιο.", @@ -5406,21 +5436,21 @@ "aggressive_label": "Υψηλότερη αγοραστική τιμή", "aggressive_text": "Μεγάλη πιθανότητα, ακόμα και σε ασταθείς αγορές. Χρησιμοποιήστε το «Επιθετικά» για να καλύψετε απότομες αυξήσεις της κίνησης στο δίκτυο που οφείλονται σε καταστάσεις όπως απότομες πτώσεις δημοφιλών NFT.", "market_label": "Τιμή αγοράς", - "market_text": "Use market for fast processing at current market price.", + "market_text": "Χρησιμοποιήστε την αγορά για γρήγορη επεξεργασία στην τρέχουσα τιμή της αγοράς.", "low_label": "Χαμηλό", "low_text": "Χρησιμοποιήστε την επιλογή “χαμηλό” για να περιμένετε μια χαμηλότερη τιμή. Οι εκτιμήσεις χρόνου είναι λιγότερο ακριβείς, καθώς οι τιμές είναι κάπως απρόβλεπτες.", "link": "Μάθετε περισσότερα για την προσαρμογή των τελών." }, "save": "Αποθήκευση", "submit": "Υποβολή", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "Το μέγιστο τέλος προτεραιότητας είναι χαμηλό για τις συνθήκες του τρέχοντος δικτύου", + "max_priority_fee_high": "Το μεγιστο τελος προτεραιότητας είναι υψηλότερο από το απαραίτητο", + "max_priority_fee_speed_up_low": "Το μεγιστο τελος προτεραιότητας πρέπει να είναι τουλάχιστον {{speed_up_floor_value}} GWEI (10% υψηλότερο από την αρχική συναλλαγή)", + "max_priority_fee_cancel_low": "Το μεγιστο τελος προτεραιότητας πρέπει να είναι τουλάχιστον {{cancel_value}} GWEI (50% υψηλότερο από την αρχική συναλλαγή)", + "max_fee_low": "Το μεγιστο τελος είναι χαμηλό για τις τρέχουσες συνθήκες δικτύου", + "max_fee_high": "Το μεγιστο τελος είναι υψηλότερο από το απαραίτητο", + "max_fee_speed_up_low": "Το μεγιστο τελος πρέπει να είναι τουλάχιστον {{speed_up_floor_value}} GWEI (10% υψηλότερο από την αρχική συναλλαγή)", + "max_fee_cancel_low": "Τα μέγιστα τέλη πρέπει να είναι τουλάχιστον {{cancel_value}} GWEI (50% υψηλότερη από την αρχική συναλλαγή)", "learn_more_gas_limit": "Το όριο τέλους συναλλαγής είναι οι μέγιστες μονάδες τελών που είστε διατεθειμένοι να χρησιμοποιήσετε. Οι μονάδες τελών είναι ένας πολλαπλασιαστής στο «Τέλος Μέγιστης Προτεραιότητας» και το «Μέγιστο Τέλος».", "learn_more_max_priority_fee": "Το μέγιστο τέλος προτεραιότητας (ή αλλιώς «φιλοδώρημα του αναλυτή») πηγαίνει απευθείας στους αναλυτές και τους παρακινεί να δώσουν προτεραιότητα στη συναλλαγή σας. Τις περισσότερες φορές θα πληρώσετε τη μέγιστη ρύθμιση.", "learn_more_max_fee": "Το μέγιστο τέλος είναι το μέγιστο που θα πληρώσετε (βασικό τέλος + τέλος προτεραιότητας).", @@ -5530,9 +5560,9 @@ "enable_remember_me_description": "Όταν η λειτουργία «Να με θυμάστε» είναι ενεργοποιημένη, οποιοσδήποτε έχει πρόσβαση στο τηλέφωνό σας μπορεί να αποκτήσει πρόσβαση στον λογαριασμό σας στο MetaMask." }, "turn_off_remember_me": { - "title": "Εισαγάγετε τον κωδικό πρόσβασής σας για να απενεργοποιήσετε την λειτουργία «Να με θυμάστε»", - "placeholder": "Κωδικός πρόσβασης", - "description": "Εάν απενεργοποιήσετε αυτή την επιλογή, από εδώ και πέρα θα χρειάζεστε τον κωδικό πρόσβασής σας για να ξεκλειδώσετε το MetaMask.", + "title": "Απενεργοποίηστε την λειτουργία «Να με θυμάστε»", + "placeholder": "Επιβεβαίωση κωδικού πρόσβασης", + "description": "Μόλις απενεργοποιηθεί, η λειτουργία \"Να με θυμάστε\" δεν μπορεί να χρησιμοποιηθεί ξανά. Αυτή η λειτουργία έχει καταργηθεί, επομένως μπορείτε να ξεκλειδώσετε το MetaMask με τον κωδικό πρόσβασής σας ή τα βιομετρικά σας δεδομένα.", "action": "Απενεργοποίηστε την λειτουργία «Να με θυμάστε»" }, "dapp_connect": { @@ -5582,7 +5612,7 @@ "learn_more": "Μάθετε περισσότερα" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "Επαλήθευση στοιχείων τρίτων", "protect_from_scams": "Για να προστατευτείτε από τους απατεώνες, αφιερώστε λίγο χρόνο για να επαληθεύσετε τα στοιχεία τρίτων.", "learn_to_verify": "Μάθετε πώς να επαληθεύετε τα στοιχεία τρίτων", "spending_cap": "όριο δαπανών", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "Απαιτείται επαναφορά", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "Κάτι πήγε στραβά, αλλά μην ανησυχείτε! Ας προσπαθήσουμε να επαναφέρουμε το πορτοφόλι σας.", "restore_needed_action": "Επαναφορά πορτοφολιού" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Το κλείσιμο της τρέχουσας εφαρμογής στη συσκευή Ledger απέτυχε.", "ethereum_app_not_installed": "Η εφαρμογή Ethereum δεν έχει εγκατασταθεί.", "ethereum_app_not_installed_error": "Εγκαταστήστε την εφαρμογή Ethereum στη συσκευή Ledger.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "Η εφαρμογή Ethereum δεν είναι ανοιχτή", + "eth_app_not_open_message": "Παρακαλούμε ανοίξτε την εφαρμογή Ethereum στη συσκευή σας Ledger.", "ledger_is_locked": "Το Ledger είναι κλειδωμένο", "unlock_ledger_message": "Ξεκλειδώστε τη συσκευή Ledger", "cannot_get_account": "Δεν είναι δυνατή η λήψη λογαριασμού", @@ -5797,8 +5827,8 @@ "error_description": "Η εγκατάσταση του {{snap}} απέτυχε." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "Ένα ετήσιο μπόνους που μπορείτε να διεκδικήσετε καθημερινά από το πορτοφόλι σας.", + "earn_a_percentage_bonus": "Κερδίστε ένα μπόνους {{percentage}}%", "claimable_bonus": "Μπόνους προς εξαργύρωση", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "Ο χρόνος που χρειάζεται για να αποσύρετε το token σας από το πρωτόκολλο και να το λάβετε πίσω στο πορτοφόλι σας", "receive": "Αυτό το token χρησιμοποιείται για την παρακολούθηση των περιουσιακών στοιχείων και των ανταμοιβών σας. Μην το μεταφέρετε ή το ανταλλάξετε, διαφορετικά δεν θα μπορείτε να κάνετε ανάληψη στα περιουσιακά σας στοιχεία.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "Ο Δείκτης Ασφάλειάς σας αξιολογεί τον κίνδυνο ρευστοποίησης", "above_two_dot_zero": "Πάνω από 2,0", "safe_position": "Ασφαλής θέση", "between_one_dot_five_and_2_dot_zero": "Μεταξύ 1,5-2,0", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "Μέτριος κίνδυνος ρευστοποίησης", "below_one_dot_five": "Κάτω από 1,5", "higher_liquidation_risk": "Υψηλότερος κίνδυνος ρευστοποίησης" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "Γιατί δεν μπορώ να κάνω ανάληψη όλο το ποσό που έχω στο υπόλοιπό μου;", "your_withdrawal_amount_may_be_limited_by": "Το ποσό ανάληψης ενδέχεται να επηρεάζεται από", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "“Δεξαμενή” ρευστότητας", "not_enough_funds_available_in_the_lending_pool_right_now": "Δεν υπάρχουν επαρκή διαθέσιμα κεφάλαια στην \"δεξαμενή\" δανεισμού αυτή τη στιγμή.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "Υφιστάμενες Θέσεις Δανεισμού", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "Η ανάληψη χρημάτων μπορεί να θέσει τις υφιστάμενες θέσεις δανεισμού σας σε κίνδυνο ρευστοποίησης." } }, @@ -5998,11 +6028,11 @@ "earn_button": "Κερδίστε" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "Ποντάρετε σε TRX και κερδίστε", + "stake_any_amount": "Ποντάρετε οποιοδήποτε ποσό σε TRX.", "earn_trx_rewards": "Κερδίστε ανταμοιβές σε TRX.", "earn_trx_rewards_description": "Ξεκινάτε να κερδίζετε μόλις δεσμεύσετε τα tokens. Οι ανταμοιβές συσσωρεύονται αυτόματα.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "Ακύρωση πονταρίσματος ανά πάσα στιγμή. Η διαδικασία διαρκεί 14 ημέρες για επεξεργασία." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "Εκτιμώμενο τέλος συναλλαγής", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "Τα τέλη συναλλαγών καταβάλλονται σε λογισμικό κρυπτονομισμάτων που επεξεργάζεται τις συναλλαγές στο δίκτυο Ethereum. Το MetaMask δεν επωφελείται από τα τέλη συναλλαγών.", "gas_fluctuation": "Τα τέλη συναλλαγών είναι κατ' εκτίμηση και αυξομειώνονται ανάλογα με την κίνηση του δικτύου και την πολυπλοκότητα των συναλλαγών.", "gas_learn_more": "Μάθετε περισσότερα για τα τέλη συναλλαγών" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "Σύνδεση με", "spender": "Επενδυτής", "now": "Τώρα", - "switching_to": "Switching to", + "switching_to": "Αλλαγή σε", "bridge_estimated_time": "Εκτ. χρόνος", "pay_with": "Πληρωμή με", - "receive_as": "Receive", + "receive_as": "Λήψη", "total": "Σύνολο", - "you_receive": "You'll receive", + "you_receive": "Θα λάβετε", "transaction_fee": "Τέλος συναλλαγής", - "transaction_fees": "Transaction fees", + "transaction_fees": "Τέλος συναλλαγής", "metamask_fee": "Χρεώσεις στο MetaMask", "network_fee": "Τέλη δικτύου", "bridge_fee": "Τέλος παρόχου για την μεταφορά" @@ -6234,7 +6264,7 @@ "transaction_fee": "Θα ανταλλάξουμε τα tokens σας με USDC.e στο δίκτυο Polygon, που χρησιμοποιείται για τις Προβλέψεις. Οι πάροχοι ανταλλαγών ενδέχεται να χρεώσουν προμήθεια, αλλά το MetaMask δεν χρεώνει." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "Το MetaMask θα κάνει την αλλαγή στο επιθυμητό συμβολικό token για εσάς. Δεν ισχύει χρέωση MetaMask όταν κάνετε αλλαγή σε MUSD." }, "musd_conversion": { "transaction_fee": "Τα τέλη μετατροπής σε mUSD περιλαμβάνουν τις χρεώσεις δικτύου και ενδέχεται να περιλαμβάνουν χρεώσεις παρόχου." @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "Αυτός ο ιστότοπος ζητάει την υπογραφή σας", "transaction_tooltip": "Αυτός ο ιστότοπος ζητά κάτι για τη συναλλαγή σας", "details": "Λεπτομέρειες", - "qr_get_sign": "Get signature", + "qr_get_sign": "Λήψη υπογραφής", "qr_scan_text": "Σαρώστε με το πορτοφόλι υλικού σας", "sign_with_ledger": "Είσοδος με το Ledger", "smart_account": "Έξυπνος λογαριασμός", "smart_contract": "Έξυπνο συμβόλαιο", - "standard_account": "Standard account", + "standard_account": "Βασικός λογαριασμός", "siwe_message": { "url": "Διεύθυνση URL", "network": "Δίκτυο", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "Έξυπνος λογαριασμός", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "Βασικός λογαριασμός", "switch": "Αλλαγή", "switchBack": "Επιστροφή", "includes_transaction": "Περιλαμβάνει {{transactionCount}} συναλλαγές", @@ -6307,9 +6337,9 @@ "cancel": "Ακύρωση", "description": "Εισάγετε το ποσό που αισθάνεστε άνετα ότι μπορείτε να δαπανήσετε.", "invalid_number_error": "Το όριο δαπανών πρέπει να είναι αριθμός", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "Το όριο δαπανών δεν μπορεί να είναι κενό", + "no_extra_decimals_error": "Το όριο δαπανών δεν μπορεί να έχει περισσότερα δεκαδικά ψηφία από το token", + "no_zero_error": "Το όριο δαπανών δεν μπορεί να είναι 0", "no_zero_error_decrease_allowance": "Το όριο δαπανών 0 δεν επηρεάζει τη μέθοδο 'decreaseAllowance'", "no_zero_error_increase_allowance": "Το όριο δαπανών 0 δεν επηρεάζει τη μέθοδο 'increaseAllowance'", "save": "Αποθήκευση", @@ -6336,7 +6366,7 @@ "transferRequest": "Αίτημα μεταφοράς", "nested_transaction_heading": "Συναλλαγή {{index}}", "transaction": "Προστασία", - "available_balance": "Available balance: ", + "available_balance": "Διαθέσιμο υπόλοιπο: ", "edit_amount_done": "Συνεχίστε", "deposit_edit_amount_done": "Προσθήκη κεφαλαίων", "deposit_edit_amount_predict_withdraw": "Ανάληψη", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "Όροι & Προϋποθέσεις", "select_token": "Επιλέξτε token", "no_tokens_found": "Δεν βρέθηκαν token", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "Δεν βρέθηκαν token με αυτό το όνομα “{}”. Δοκιμάστε άλλη αναζήτηση.", "select_network": "Επιλέξτε δίκτυο", "all_networks": "Όλα τα δίκτυα", "num_networks": "{{numNetworks}} δίκτυα", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "Αποεπιλογή όλων", "see_all": "Προβολή όλων", "all": "Όλα", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}} περισσότερα", "apply": "Εφαρμογή", "slippage": "Απόκλιση", "slippage_info": "Εάν η τιμή αλλάξει τη στιγμή που η εντολή σας υποβάλλεται και επιβεβαιώνεται, αυτό ονομάζεται «απόκλιση». Η ανταλλαγή σας θα ακυρωθεί αυτόματα εάν η απόκλιση υπερβαίνει τα όρια ανοχής που έχετε ορίσει εδώ.", @@ -6392,7 +6422,7 @@ "quote_info_title": "Χρέωση", "network_fee_info_title": "Τέλη δικτύου", "network_fee_info_content": "Τα τέλη δικτύου εξαρτώνται από το φόρτο του δικτύου και από το πόσο περίπλοκη είναι η συναλλαγή σας.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "Αυτό το τέλος δικτύου καταβάλλεται από την MetaMask, επομένως μπορείτε να πραγματοποιείτε συναλλαγές χωρίς το {{nativeToken}} στον λογαριασμό σας.", "points": "Εκτ. πόντοι", "points_tooltip": "Πόντοι", "points_tooltip_content_1": "Οι πόντοι είναι ο τρόπος με τον οποίο κερδίζετε ανταμοιβές στο MetaMask Rewards, ολοκληρώνοντας συναλλαγές όπως ανταλλαγές, μεταφορές ή συμβόλαια αορίστου διάρκειας (perps).", @@ -6406,7 +6436,7 @@ "select_recipient": "Επιλέξτε παραλήπτη", "external_account": "Εξωτερικός λογαριασμός", "error_banner_description": "Αυτή η διαδρομή συναλλαγών δεν είναι διαθέσιμη προς το παρόν. Δοκιμάστε να αλλάξετε το ποσό, το δίκτυο ή το token και θα βρούμε την καλύτερη επιλογή.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "Αυτή η διαδρομή συναλλαγών δεν είναι διαθέσιμη αυτήν τη στιγμή. Δοκιμάστε να αλλάξετε το ποσό, το δίκτυο ή το token και θα βρούμε την καλύτερη επιλογή.\n\nΛάβετε υπόψη ότι εάν προσπαθείτε να κάνετε συναλλαγές με μετοχές Ondo Tokenised, ενδέχεται να έχετε γεωγραφικούς περιορισμούς, π.χ. μέσω ΗΠΑ, ΕΕ, Ηνωμένου Βασιλείου και Βραζιλίας.", "insufficient_funds": "Ανεπαρκές ποσό", "insufficient_gas": "Δεν έχετε αρκετά χρήματα για τα τέλη συναλλαγών", "select_amount": "Επιλέξτε ποσό", @@ -6417,9 +6447,9 @@ "title": "Διασύνδεση", "submitting_transaction": "Υποβολή", "fetching_quote": "Λήψη προσφοράς", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "Περιλαμβάνει προμήθεια {{feePercentage}}% MetaMask.", "no_mm_fee": "Χωρίς προμήθεια MM", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "Χωρίς προμήθεια MetaMask για ανταλλαγή σε {{destTokenSymbol}}.", "hardware_wallet_not_supported": "Τα πορτοφόλια υλικού δεν υποστηρίζονται ακόμη. Χρησιμοποιήστε ένα θερμό πορτοφόλι για να συνεχίσετε.", "hardware_wallet_not_supported_solana": "Τα πορτοφόλια υλικού δεν υποστηρίζουν ακόμα την Solana. Χρησιμοποιήστε ένα θερμό πορτοφόλι για να συνεχίσετε.", "price_impact_info_title": "Αντίκτυπος στην τιμή", @@ -6432,17 +6462,24 @@ "approval_needed": "Εγκρίνει το token για ανταλλαγή.", "approval_tooltip_title": "Παραχώρηση ακριβούς πρόσβασης", "approval_tooltip_content": "Επιτρέπετε την πρόσβαση για συγκεκριμένο ποσό, {{amount}} {{symbol}}. Το συμβόλαιο δεν θα έχει πρόσβαση σε επιπλέον κεφάλαια.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "Ελάχιστο ποσό που θα λάβετε", + "minimum_received_tooltip_title": "Ελάχιστο ποσό που θα λάβετε", "minimum_received_tooltip_content": "Το ελάχιστο ποσό που θα λάβετε, αν η τιμή αλλάξει όσο η συναλλαγή σας εκτελείται, με βάση την ανοχή σας στην απόκλιση. Πρόκειται για εκτίμηση από τους παρόχους ρευστότητας. Τα τελικά ποσά ενδέχεται να διαφέρουν.", + "market_closed": { + "title": "Η αγορά είναι κλειστή", + "description": "Η αγορά που υποστηρίζει αυτό το token είναι προς το παρόν κλειστή. Τα token μπορούν να μεταφερθούν εντός αλυσίδας ανά πάσα στιγμή.", + "learn_more": "Μάθετε περισσότερα", + "learn_more_url": "https://status.ondo.finance/market", + "done": "Τέλος" + }, "submit": "Υποβολή", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "Η συναλλαγή σας δεν θα ολοκληρωθεί εάν η τιμή αλλάξει περισσότερο από το ποσοστό απόκλισης.", "cancel": "Άκυρο", "confirm": "Επιβεβαίωση", "exceeding_upper_slippage_warning": "Υψηλή απόκλιση— αυτό μπορεί να οδηγήσει σε μη ευνοϊκή ανταλλαγή", "exceeding_lower_slippage_warning": "Χαμηλή απόκλιση— αυτό μπορεί να οδηγήσει σε μη ευνοϊκή ανταλλαγή", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "Εισαγάγετε μια τιμή μεγαλύτερη από {{value}}%", + "exceeding_upper_slippage_error": "Δεν μπορείτε να εισαγάγετε τιμή μεγαλύτερη από {{value}}%", "custom": "Προσαρμογή" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "Επαναφορά πορτοφολιού", "login_with_social": "Σύνδεση μέσω κοινωνικών δικτύων", "setup": "Διαμόρφωση", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "Μυστική φράση ανάκτησης {{num}}", "back_up": "Aντιγράφο ασφαλείας", "reveal": "Εμφάνιση", "social_recovery_title": "ΑΝΑΚΤΗΣΗ ΜΕ {{authConnection}}", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "Πληκτρολογήστε τον κωδικό πρόσβασης", "description": "Πληκτρολογήστε τον κωδικό του πορτοφολιού σας για να δείτε τα στοιχεία της κάρτας.", + "description_unfreeze": "Εισαγάγετε τον κωδικό πρόσβασης του πορτοφολιού σας για να συνεχίσετε τις δαπάνες με την κάρτα σας.", "placeholder": "Κωδικός πρόσβασης", "confirm": "Επιβεβαίωση", "cancel": "Ακύρωση", @@ -7001,6 +7039,7 @@ "enable_card_error": "Αποτυχία ενεργοποίησης της κάρτας. Προσπαθήστε ξανά αργότερα.", "view_card_details_error": "Δεν ήταν δυνατή η φόρτωση των στοιχείων της κάρτας. Παρακαλούμε δοκιμάστε ξανά.", "biometric_verification_required": "Απαιτείται έλεγχος ταυτότητας για να δείτε τα στοιχεία της κάρτας.", + "unfreeze_auth_required": "Απαιτείται έλεγχος ταυτότητας για να συνεχίσετε τις δαπάνες στην κάρτα σας.", "warnings": { "close_spending_limit": { "title": "Έχετε σχεδόν φτάσει το όριο δαπανών σας", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "Η κάρτα σας έχει προσωρινά απενεργοποιηθεί", - "description": "Επικοινωνήστε με την υποστήριξη για να ενεργοποιήσετε ξανά την κάρτα σας" + "description": "Η κάρτα σας έχει προσωρινά παγωμενη. Μπορείτε να την ξεπαγωσετε οποιαδήποτε στιγμή." }, "blocked": { "title": "Η κάρτα σας είναι μπλοκαρισμένη", @@ -7068,7 +7107,14 @@ "travel_description": "Κάντε κράτηση σε ξενοδοχεία με έκπτωση έως και 70%", "card_tos_title": "Όροι και προϋποθέσεις", "order_metal_card": "Metal Card", - "order_metal_card_description": "Παραγγείλετε τώρα τη φυσική Metal Card σας" + "order_metal_card_description": "Παραγγείλετε τώρα τη φυσική Metal Card σας", + "freeze_card": "Πάγωμα κάρτας", + "unfreeze_card": "Ξεπάγωμα κάρτας", + "freeze_card_description": "Παύση όλων των δαπανών στην κάρτα σας", + "unfreeze_card_description": "Συνέχιση όλων των δαπανών στην κάρτα σας", + "freeze_error": "Αποτυχία δημιουργίας πληρωμής. Παρακαλούμε δοκιμάστε ξανά.", + "freeze_success": "Η κάρτα πάγωσε επιτυχώς", + "unfreeze_success": "Η κάρτα ξεπαγώθηκε με επιτυχία" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "Η επαναποστολή θα είναι διαθέσιμη σε {{seconds}} δευτερόλεπτα" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "Προσθήκη στο {{walletName}}", + "adding_to_wallet": "Προσθήκη στο {{walletName}}...", + "continue_setup": "Συνέχεια εγκατάστασης {{walletName}}", + "wallet_not_available": "Το {{walletName}} δεν είναι διαθέσιμο", + "already_in_wallet": "Ήδη στο {{walletName}}", + "success_title": "Η κάρτα προστέθηκε!", + "success_message": "Η κάρτα MetaMask σας έχει προστεθεί στο {{walletName}}.", + "error_title": "Δεν είναι δυνατή η προσθήκη κάρτας", + "error_wallet_not_available": "Το {{walletName}} δεν είναι διαθέσιμο σε αυτήν τη συσκευή. Βεβαιωθείτε ότι έχετε ρυθμίσει το {{walletName}}.", + "error_wallet_not_initialized": "Το {{walletName}} δεν έχει αρχικοποιηθεί. Ρυθμίστε το πορτοφόλι σας και προσπαθήστε ξανά.", "error_card_already_in_wallet": "Αυτή η κάρτα έχει ήδη προστεθεί στο {{walletName}}.", "error_card_pending": "Η διαδικασία ρύθμισης της κάρτας σας στο {{walletName}} βρίσκεται σε εξέλιξη. Παρακαλούμε ελέγξτε ξανά σε λίγα λεπτά.", "error_card_suspended": "Η κάρτα σας στο {{walletName}} έχει ανασταλεί. Παρακαλούμε επικοινωνήστε με την υποστήριξη για βοήθεια.", "error_card_not_eligible": "Αυτή η κάρτα δεν είναι επιλέξιμη για προσθήκη σε ηλεκτρονικό πορτοφόλι στο κινητό.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "Αποτυχία κρυπτογράφησης δεδομένων κάρτας. Παρακαλούμε δοκιμάστε ξανά.", "error_invalid_card_data": "Μη έγκυρα στοιχεία κάρτας. Παρακαλούμε ελέγξτε τα στοιχεία σας και δοκιμάστε ξανά.", "error_card_not_found": "Η κάρτα δεν βρέθηκε. Παρακαλούμε δοκιμάστε ξανά.", "error_card_provider_not_found": "Ο πάροχος της κάρτας δεν είναι διαθέσιμος στην περιοχή σας.", "error_card_id_mismatch": "Η επαλήθευση της κάρτας απέτυχε. Παρακαλούμε δοκιμάστε ξανά.", "error_card_not_active": "Η κάρτα σας δεν είναι ενεργή. Παρακαλούμε ενεργοποιήστε την πρώτα.", "error_network": "Παρουσιάστηκε σφάλμα δικτύου. Παρακαλούμε ελέγξτε τη σύνδεσή σας και δοκιμάστε ξανά.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "Το χρονικό όριο του αιτήματος έληξε. Παρακαλούμε δοκιμάστε ξανά.", + "error_server": "Παρουσιάστηκε σφάλμα διακομιστή. Παρακαλούμε δοκιμάστε ξανά.", + "error_unknown": "Παρουσιάστηκε ένα μη αναμενόμενο σφάλμα. Παρακαλούμε δοκιμάστε ξανά ή επικοινωνήστε με την υποστήριξη.", + "error_platform_not_supported": "Αυτή η πλατφόρμα δεν υποστηρίζει την παροχή πορτοφολιού για κινητά.", "try_again": "Προσπαθήστε ξανά", "cancel": "Ακύρωση" } @@ -7299,7 +7345,7 @@ "main_title": "Ανταμοιβές", "referral_title": "Συστάσεις", "tab_overview_title": "Επισκόπηση", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "Στιγμιότυπα", "tab_activity_title": "Δραστηριότητα", "referral_stats_earned_from_referrals": "Κερδίσατε από συστάσεις", "referral_stats_referrals": "Συστάσεις", @@ -7353,7 +7399,7 @@ "verifying_rewards": "Βεβαιωνόμαστε ότι όλα είναι σωστά πριν διεκδικήσετε τις ανταμοιβές σας." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "Πόντοι που κερδίσατε" }, "onboarding": { "not_supported_region_title": "Η περιοχή δεν υποστηρίζεται", @@ -7431,7 +7477,7 @@ "show_less": "Εμφάνιση λιγότερων", "linking_progress": "Προσθήκη λογαριασμών… ({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}} εγγεγραμμένοι", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "Προσθήκη όλων των λογαριασμών" }, "referred_by_code": { "title": "Κωδικός παραπομπής", @@ -7514,7 +7560,7 @@ "claim_label": "Εξαργύρωση", "claimed_label": "Διεκδικήθηκε", "reward_claimed": "Η ανταμοιβή εξαργυρώθηκε", - "time_left": "{{time}} left", + "time_left": "Απομένει {{time}}", "expired": "Έληξε" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "Η εξαργύρωση απέτυχε", "redeem_failure_description": "Παρακαλούμε δοκιμάστε ξανά αργότερα.", "reward_details": "Λεπτομέρειες Ανταμοιβών", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "Επιλέξτε τον λογαριασμό στον οποίο θέλετε να σταλεί αυτή η ανταμοιβή." }, "animation": { "could_not_load": "Δεν ήταν δυνατή η φόρτωση" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", + "starts_date": "Αρχίζει {{date}}", + "ends_date": "Λήγει {{date}}", + "results_coming_soon": "Τα αποτελέσματα έρχονται σύντομα", + "tokens_on_the_way": "Token καθ' οδόν", + "pill_up_next": "Επόμενο", + "pill_live_now": "Ζωντανά τώρα", "pill_calculating": "Υπολογισμός", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "pill_results_ready": "Τα αποτελέσματα είναι έτοιμα", + "pill_complete": "Ολοκληρώθηκε" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "Στιγμιότυπα", + "error_title": "Δεν είναι δυνατή η φόρτωση στιγμιότυπων", + "error_description": "Δεν ήταν δυνατή η φόρτωση των στιγμιότυπων. Παρακαλούμε δοκιμάστε ξανά.", "retry_button": "Επανάληψη" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "Ενεργό", + "upcoming_title": "Επερχόμενο", + "previous_title": "Προηγούμενο", + "empty_state": "Δεν υπάρχουν διαθέσιμα στιγμιότυπα", + "error_title": "Δεν είναι δυνατή η φόρτωση στιγμιότυπων", + "error_description": "Δεν ήταν δυνατή η φόρτωση των στιγμιότυπων. Παρακαλούμε δοκιμάστε ξανά.", "retry_button": "Επανάληψη", - "refreshing": "Refreshing..." + "refreshing": "Ανανεώνεται..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "Έγκριση {{approveSymbol}}", "bridge_approval_loading": "Έγκριση", "bridge_send": "Μεταφορά {{sourceSymbol}} από {{sourceChain}}", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "Αποστολή γέφυρας", "bridge_receive": "Θα λάβετε {{targetSymbol}} στο {{targetChain}}", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "Λήψη γέφυρας", "default": "Συναλλαγή", "musd_convert_send": "Εστάλη {{sourceSymbol}} από το {{sourceChain}}", "musd_claim": "Κάντε εξαργύρωση σε mUSD", @@ -7607,20 +7653,20 @@ "description": "Γίνεται σύνδεση με το {{dappName}}…" }, "show_error": { - "title": "Connection error", + "title": "Σφάλμα σύνδεσης", "description": "Απέτυχε η προσπάθεια σύνδεσης. Παρακαλώ δοκιμάστε ξανά." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "Η έγκριση απορρίφθηκε", + "description": "Ο χρήστης απέρριψε το αίτημα." }, "show_return_to_app": { "title": "Επιτυχία", "description": "Επιστρέψτε στην εφαρμογή για να συνεχίσετε." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "Δεν βρέθηκε σύνδεση", + "description": "Παρακαλούμε δημιουργήστε μια νέα σύνδεση από την εφαρμογή για να συνεχίσετε." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "Εξερεύνηση", + "trending_tokens": "Δημοφιλή token", "price_change": "Αλλαγή τιμής", "all_networks": "Όλα τα δίκτυα", - "24h": "24h", + "24h": "24ω", "time": "Ώρα", "24_hours": "24 ώρες", "6_hours": "6 ώρες", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 ώρα", + "5_minutes": "5 λεπτά", "networks": "Δίκτυα", "sort_by": "Ταξινόμηση κατά", "volume": "Όγκος", @@ -7650,32 +7696,48 @@ "high_to_low": "Από το υψηλότερο στο χαμηλότερο", "low_to_high": "Από το χαμηλότερο στο υψηλότερο", "apply": "Εφαρμογή", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "Αναζήτηση token, ιστότοπων, διευθύνσεων URL", "cancel": "Άκυρο", "perps": "Συμβ.αορ.", "predictions": "Προβλέψεις", - "no_results": "No results found", + "no_results": "Δεν βρέθηκαν αποτελέσματα", "sites": "Ιστότοποι", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "Δημοφιλείς ιστότοποι", + "search_sites": "Αναζήτηση ιστότοπων", + "enable_basic_functionality": "Ενεργοποίηση βασικής λειτουργικότητας", + "basic_functionality_disabled_title": "Η Εξερεύνηση δεν είναι διαθέσιμη", + "basic_functionality_disabled_description": "Δεν είναι δυνατή η ανάκτηση των απαιτούμενων μεταδεδομένων όταν η βασική λειτουργικότητα είναι απενεργοποιημένη.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "Τα δημοφιλή token δεν είναι διαθέσιμα", + "description": "Δεν είναι δυνατή η ανάκτηση αυτής της σελίδας αυτήν τη στιγμή", "try_again": "Προσπαθήστε ξανά" }, "empty_search_result_state": { "title": "Δεν βρέθηκαν token", - "description": "We were not able to find this token" + "description": "Δεν μπορέσαμε να βρούμε αυτό το token" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "Έτοιμη ενημέρωση", + "description_ios": "Έχουμε κάνει κάποιες σημαντικές διορθώσεις. Επαναλάβετε τη φόρτωση για την πιο πρόσφατη έκδοση του MetaMask.", + "description_android": "Έχουμε κάνει κάποιες σημαντικές διορθώσεις. Κλείστε και ανοίξτε ξανά το MetaMask για να εφαρμόσετε την ενημέρωση.", "primary_action_reload": "Επαναφόρτωση", "primary_action_acknowledge": "Κατανοητό" + }, + "homepage": { + "sections": { + "tokens": "Token", + "perpetuals": "Συμβόλαια αορίστου διάρκειας", + "predictions": "Προβλέψεις", + "defi": "DeFi", + "nfts": "NFT", + "import_nfts": "Εισαγωγή NFT", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/es.json b/locales/languages/es.json index 1bad15d97bd..49ed78fea46 100644 --- a/locales/languages/es.json +++ b/locales/languages/es.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "borrarán permanentemente", "reset_wallet_desc_2": "de MetaMask en este dispositivo. Esta acción es irreversible.", "reset_wallet_desc_login": "Para restaurar tu billetera, puedes usar tu frase secreta de recuperación o la contraseña de tu cuenta de Google o Apple. MetaMask no tiene esta información.", - "reset_wallet_desc_srp": "Para restaurar tu billetera, asegúrate de tener tu frase secreta de recuperación. MetaMask no tiene esta información." + "reset_wallet_desc_srp": "Para restaurar tu billetera, asegúrate de tener tu frase secreta de recuperación. MetaMask no tiene esta información.", + "biometric_authentication_cancelled": "Se canceló la autenticación biométrica", + "biometric_authentication_cancelled_title": "Configuración biométrica fallida", + "biometric_authentication_cancelled_description": "Vuelve a configurar la autenticación biométrica desde la configuración.", + "biometric_authentication_cancelled_button": "Confirmar" }, "connect_hardware": { "title_select_hardware": "Conectar un monedero físico", @@ -1040,7 +1044,7 @@ "title": "Monto a depositar", "get_usdc_hyperliquid": "Obtén USDC • Hyperliquid", "insufficient_funds": "Fondos insuficientes", - "no_funds_available": "No hay fondos disponibles. Primero realiza un depósito.", + "no_funds_available": "No hay suficientes fondos disponibles. Deposita fondos o selecciona otro método de pago", "enter_amount": "Ingrese una cantidad", "fetching_quote": "Obteniendo cotización", "submitting": "Enviando transacción", @@ -1970,8 +1974,8 @@ "trade_again": "Operar de nuevo", "activity": { "deposit_title": "Depósito", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "Se depositaron {{amount}} {{symbol}}", + "withdrew_amount": "Se retiraron {{amount}} {{symbol}}", "status_completed": "Completado", "status_failed": "Fallido", "status_pending": "Pendiente" @@ -2051,6 +2055,16 @@ "referral_code_text": "Usa mi código de recomendación para ganar recompensas adicionales." } }, + "market_insights": { + "title": "Perspectivas del mercado", + "updated_ago": "Actualizado {{time}}", + "disclaimer": "Perspectivas de IA. No es asesoramiento financiero.", + "whats_driving_price": "¿Qué determina el precio?", + "what_people_saying": "Qué dice la gente", + "trade_button": "Operar", + "sources_count": "+{{count}} fuentes", + "sources_title": "De liquiDez" + }, "predict": { "title": "Predicciones de MetaMask", "prediction_markets": "Mercados de predicción", @@ -2384,8 +2398,8 @@ "no_available_tokens": "¿No ve su token?", "add_tokens": "Agregar activo", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "{{tokenCount}} nuevos {{tokensLabel}} se han encontrado en esta cuenta", "token_toast": { "tokens_imported_title": "Tokens importados", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "Los decimales del token no pueden estar vacíos.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "No se encontró ningún token con ese nombre.", + "tokens_empty_description": "Search for any token and import it", "select_token": "Selecciona un token", "address_must_be_smart_contract": "Se detectó una dirección personal. Escriba la dirección de contrato del token.", "billion_abbreviation": "B", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "Desconectar todas las cuentas", "deceptive_site_ahead": "Sitio engañoso más adelante", "deceptive_site_desc": "El sitio que intenta visitar no es seguro. Los atacantes pueden engañarlo para que haga algo peligroso.", + "malicious_site_detected": "Sitio malicioso detectado", + "malicious_site_warning": "Si te conectas a este sitio, podrías perder todos tus activos.", + "connect_anyway": "Conectarse de todos modos", "learn_more": "Conozca más", "advisory_by": "Aviso proporcionado por Ethereum Phishing Detector y PhishFort", "potential_threat": "Las amenazas potenciales incluyen", @@ -2846,7 +2864,11 @@ "permissions": "Permisos", "card_title": "Tarjeta MetaMask", "settings": "Configuración", - "log_out": "Cerrar sesión" + "networks": "Redes", + "log_out": "Cerrar sesión", + "notifications": "Notificaciones", + "buy": "Comprar", + "scan": "Escanear" }, "app_settings": { "enabling_notifications": "Activando notificaciones...", @@ -2870,6 +2892,8 @@ "state_logs": "Registros de estado", "add_network_title": "Agregar una red", "auto_lock": "Bloqueo automático", + "enable_device_authentication": "Habilitar la autenticación del dispositivo", + "enable_device_authentication_desc": "Utiliza la biometría o el código de acceso de tu dispositivo para desbloquear MetaMask.", "auto_lock_desc": "Elija la cantidad de tiempo antes de que la aplicación se bloquee automáticamente.", "state_logs_desc": "Esto ayudará a que MetaMask depure cualquier problema que se pueda encontrar. Envíelo al soporte de MetaMask a través del icono de tres barras > Enviar comentarios, o responda a la incidencia existente, si la tiene.", "autolock_immediately": "Inmediatamente", @@ -2975,6 +2999,11 @@ "add_rpc_url": "Agregar URL de RPC", "add_block_explorer_url": "Añadir URL del explorador de bloques", "networks_desc": "Agregar y editar redes RPC personalizadas", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "Buscar redes", + "networks_no_results": "No se encontraron redes", "network_name_label": "Nombre de la red", "network_name_placeholder": "Nombre de la red (opcional)", "network_rpc_url_label": "URL de RPC", @@ -2991,7 +3020,16 @@ "network_other_networks": "Otras redes", "network_rpc_networks": "Redes RPC", "network_add_network": "Añadir red", + "add_chain_title": "Agregar una red", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "Reintentar", + "add_chain_added": "Added", + "add_chain_or": "o", + "add_chain_custom_link": "Agregar una red personalizada", "network_add_custom_network": "Agregar una red personalizada", + "network_add_test_network": "Add a test network", "network_add": "Agregar", "network_save": "Guardar", "remove_network_title": "¿Quiere quitar esta red?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "OK", "title": "No se pudo conectar la cuenta", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "Escanea el código QR del sitio para volver a conectarte a MetaMask" }, "app_information": { "title": "Información", @@ -3379,6 +3417,7 @@ "sell_description": "Vender cripto por efectivo" }, "asset_overview": { + "market_closed": "Mercado cerrado", "send_button": "Enviar", "buy_button": "Comprar", "cash_buy_button": "Compra en efectivo", @@ -3399,19 +3438,6 @@ "bridge": "Puentear", "earn": "Ganar", "convert_to_musd": "Convertir a mUSD", - "merkl_rewards": { - "annual_bonus": "Bonificación del {{apy}} %", - "claimable_bonus": "Bonificación reclamable", - "claimable_bonus_tooltip_description": "Los bonos en mUSD se reclaman en Linea.", - "terms_apply": "Se aplican términos y condiciones.", - "ok": "OK", - "claim": "Reclamar", - "processing_claim": "Procesando solicitud...", - "claim_on_linea_title": "Reclama bonificaciones en Linea", - "claim_on_linea_description": "Tu bono se emitirá en Linea, independiente de tu saldo de mUSD en Ethereum.", - "continue": "Continuar", - "unexpected_error": "Error inesperado. Inténtalo de nuevo." - }, "tron": { "daily_resource_new_energy": "Nueva energía diaria", "sufficient_to_cover": "Suficiente para cubrir", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "Dirección del token copiada en el portapapeles" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "Código QR no válido", "invalid_qr_code_message": "El código QR que intenta escanear no es válido.", "allow_camera_dialog_title": "Permitir acceso a la cámara", "allow_camera_dialog_message": "Necesitamos su permiso para escanear códigos QR", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "Parece que intenta sincronizar con la extensión. debe borrar el monedero actual. \n\nUna vez que haya borrado o vuelto a instalar una versión nueva de la aplicación, seleccione la opción \"Sincronizar con la extensión MetaMask\". Importante: Antes de borrar el monedero, asegúrese de haber hecho una copia de seguridad de la frase secreta de recuperación.", "not_allowed_error_title": "Activar el acceso a la cámara", "not_allowed_error_desc": "Para escanear un código QR, deberá otorgar acceso a la cámara a MetaMask desde el menú de configuración de su dispositivo.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "Código QR no reconocido", "unrecognized_address_qr_code_desc": "Lo sentimos, este código QR no está asociado con una dirección de cuenta o una dirección de contrato.", "url_redirection_alert_title": "Estás a punto de visitar un enlace externo", "url_redirection_alert_desc": "Los enlaces se pueden usar para tratar de estafar o hacer phishing a las personas, así que asegúrese de visitar solo sitios web en los que confíe.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "Este coleccionable no es de su propiedad", "known_asset_contract": "Dirección de contrato de activo conocida", "max": "Máx.", - "recipient_address": "Recipient address", + "recipient_address": "Dirección del destinatario", "required": "Requerido", "to": "Para", "total": "Total", @@ -3641,7 +3667,7 @@ "nevermind": "No importa", "edit_network_fee": "Editar tarifa de gas", "edit_priority": "Editar prioridad", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "Tarifa de cancelación de gas", "gas_speedup_fee": "Cuota por aceleración de gas", "use_max": "Usar máx.", "set_gas": "Establecer", @@ -3650,7 +3676,7 @@ "transaction_fee": "Tarifa de gas", "transaction_fee_less": "Sin tarifa", "total_amount": "Importe total", - "view_data": "View data", + "view_data": "Ver datos", "adjust_transaction_fee": "Ajustar tarifa de transacción", "could_not_resolve_ens": "No se pudo resolver ENS", "asset": "Activo", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "Para explorar la Web descentralizada, agregue una pestaña nueva", "got_it": "Entendido", "max_tabs_title": "Se alcanzó el número máximo de pestañas", - "max_tabs_desc": "Actualmente solo admitimos 5 pestañas abiertas a la vez. Cierre las pestañas existentes antes de añadir otras nuevas.", + "max_tabs_desc": "Actualmente solo admitimos 20 pestañas abiertas a la vez. Cierre las pestañas existentes antes de añadir otras nuevas.", "failed_to_resolve_ens_name": "No se pudo resolver ese nombre de ENS", "remove_bookmark_title": "Quitar favorito", "remove_bookmark_msg": "¿Está seguro de que quiere quitar este sitio de sus favoritos?", @@ -3828,7 +3854,7 @@ "cancel_button": "Cancelar" }, "approval": { - "title": "Confirm transaction" + "title": "Confirmar la transacción" }, "approve": { "title": "Aprobar", @@ -3839,39 +3865,39 @@ "unavailable": "No disponible", "tx_review_confirm": "Confirmar", "tx_review_transfer": "Transferir", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "Implementación de contrato", + "tx_review_transfer_from": "Transferir desde", + "tx_review_unknown": "Método desconocido", "tx_review_approve": "Aprobar", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "Aumentar la asignación", + "tx_review_set_approval_for_all": "Establecer aprobación para todos", + "tx_review_staking_claim": "Solicitud de staking", "tx_review_staking_deposit": "Depósito en staking", "tx_review_staking_unstake": "Dejar de hacer staking", "tx_review_lending_deposit": "Depósito en préstamo", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "Retiro de préstamo", "tx_review_perps_deposit": "Contratos perpetuos financiados", "tx_review_predict_deposit": "Predicciones financiadas", "tx_review_predict_claim": "Victorias reclamadas", "tx_review_predict_withdraw": "Predicciones retiradas", "tx_review_musd_conversion": "Conversión de mUSD", "claim": "Reclamar", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "ETH enviado", + "self_sent_ether": "ETH enviado a ti mismo", + "received_ether": "ETH recibido", "sent_dai": "DAI enviado", "self_sent_dai": "DAI enviado a usted mismo", "received_dai": "DAI recibido", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "Tokens enviados", + "received_tokens": "Tokens recibidos", "ether": "ETH", "sent_unit": "{{unit}} enviado", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "Enviado a ti mismo {{unit}}", "received_unit": "{{unit}} recibido", "sent_collectible": "Coleccionable enviado", "received_collectible": "Coleccionable recibido", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "Enviar ETH", + "send_unit": "Enviar {{unit}}", "send_collectible": "Enviar coleccionable", "receive_collectible": "Recibir coleccionable", "sent": "Enviado", @@ -3881,17 +3907,17 @@ "send": "Enviar", "redeposit": "Volver a depositar", "interaction": "Interacción", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "Implementación de contrato", + "to_contract": "Contrato nuevo", + "mint": "Acuñar", "tx_details_free": "Gratis", "tx_details_not_available": "No disponible", "smart_contract_interaction": "Interacción con el contrato inteligente", "swaps_transaction": "Transacción de intercambios", "bridge_transaction": "Puentear", "approve": "Aprobar", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "Aumentar la asignación", + "set_approval_for_all": "Establecer aprobación para todos", "hash": "Hash", "from": "De", "to": "Para", @@ -3899,15 +3925,15 @@ "amount": "Cantidad", "fee": { "transaction_fee_in_ether": "Tarifa de transacción", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "Tarifa de transacción (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "Gas usado (unidades)", + "gas_limit": "Límite de gas (unidades)", + "gas_price": "Precio de gas (GWEI)", + "base_fee": "Tarifa base (GWEI)", + "priority_fee": "Tarifa de prioridad (GWEI)", "multichain_priority_fee": "Tarifa de prioridad", - "max_fee": "Max fee per gas", + "max_fee": "Tarifa máxima por gas", "total": "Total", "view_on": "Ver en", "view_on_etherscan": "Ver en Etherscan", @@ -3923,13 +3949,13 @@ "nonce": "Mientras tanto", "from_device_label": "de este dispositivo", "import_wallet_row": "Cuenta agregada a este dispositivo", - "import_wallet_label": "Account added", + "import_wallet_label": "Cuenta agregada", "import_wallet_tip": "Todas las transacciones futuras que se realicen desde este dispositivos tendrán la etiqueta \"de este dispositivo\" junto a la marca de tiempo. Para las transacciones con fecha anterior a la que se agregó la cuenta, el historial no indicará qué transacciones salientes se originaron de este dispositivo.", "sign_title_scan": "Escanear ", "sign_title_device": "con su monedero físico", "sign_description_1": "Después de haber firmado con su monedero físico,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "Pulsa en obtener firma", + "sign_get_signature": "Obtener firma", "transaction_id": "ID de transacción", "network": "Red", "request_from": "Solicitud de", @@ -4032,7 +4058,7 @@ "title": "Redes", "other_networks": "Otras redes", "close": "Cerrar", - "status_ok": "All systems operational", + "status_ok": "Todos los sistemas operativos", "status_not_ok": "La red presenta algunos problemas", "want_to_add_network": "¿Desea agregar esta red?", "add_custom_network": "Agregar red personalizada", @@ -4051,7 +4077,7 @@ "review": "Revisar", "view_details": "Ver detalles", "network_details": "Detalles de la red", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "Al seleccionar Confirmar, se activa la verificación de detalles de la red. Puedes desactivar el registro de detalles de la red ", "network_settings_security_privacy": "Configuración > Seguridad y privacidad", "network_currency_symbol": "Símbolo de moneda", "network_block_explorer_url": "Dirección URL del explorador de bloques", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "Un proveedor de red malintencionado puede mentir sobre el estado de la cadena de bloques y registrar su actividad de red. Agregue solo redes personalizadas de confianza.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Información de red", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "Información adicional sobre las redes", "network_warning_desc": "Esta conexión de red depende de terceros. Esta conexión puede ser menos confiable o permite que terceros rastreen la actividad.", "additonial_network_information_desc": "Algunas de estas redes dependen de terceros. Las conexiones pueden ser menos confiables o permitir que terceros realicen un seguimiento de la actividad.", "connect_more_networks": "Conectar más redes", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "Esta red está en desuso", "network_deprecated_description": "La red a la que está intentando conectarse ya no es compatible con MetaMask.", "edit_networks_title": "Editar redes", - "no_network_fee": "No network fee" + "no_network_fee": "Sin tarifa de red" }, "permissions": { "title_this_site_wants_to": "Este sitio quiere:", @@ -4111,11 +4137,11 @@ "network_connected": "red conectada ", "see_your_accounts": "Ver sus cuentas y sugerir transacciones", "connected_to": "Conectado a ", - "manage_permissions": "Manage permissions", + "manage_permissions": "Administrar permisos", "edit": "Editar", "cancel": "Cancelar", "got_it": "Entendido", - "connection_details_title": "Connection details", + "connection_details_title": "Detalles de la conexión", "connection_details_description": "Se conectó a este sitio utilizando el navegador de MetaMask el {{connectionDateTime}}", "title_add_network_permission": "Agregar permiso de red", "add_this_network": "Agregar esta red", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "Desbloquear con el PIN del dispositivo?" }, "authentication": { - "auth_prompt_title": "Autenticación requerida", - "auth_prompt_desc": "Autentíquese para usar MetaMask", - "fingerprint_prompt_title": "Autenticación requerida", - "fingerprint_prompt_desc": "Use su huella dactilar para desbloquear MetaMask", - "fingerprint_prompt_cancel": "Cancelar" + "auth_prompt_desc": "Autentíquese para usar MetaMask" }, "accountApproval": { "title": "SOLICITUD DE CONEXIÓN", "walletconnect_title": "SOLICITUD DE WALLETCONNECT", "action": "¿Conectarse a este sitio?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "Para reanudar la conexión, elige el número que ves en el sitio", + "action_reconnect_deeplink": "¿Deseas volver a conectarte a este sitio?", "connect": "Conectar", "resume": "Reanudar", "cancel": "Cancelar", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "No recordar la conexión a este sitio", "disconnect": "Desconectar", "permission": "Ver su", "address": "dirección pública", @@ -4218,7 +4240,7 @@ "error_title": "Algo salió mal", "error_message": "No se pudo importar esa clave privada. Asegúrese de que la escribió correctamente.", "error_empty_message": "Debe escribir su clave privada.", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "o escanear un código QR" }, "import_private_key_success": { "title": "La cuenta se importó correctamente.", @@ -4229,8 +4251,8 @@ "import_wallet_title": "Importar una billetera", "enter_srp_subtitle": "Ingresa tu frase secreta de recuperación", "textarea_placeholder": "Añade un espacio entre cada palabra y asegúrate de que nadie esté mirando", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "Ingresa la frase secreta de recuperación de tu billetera. Puedes importar cualquier frase secreta de recuperación de Ethereum, Solana o Bitcoin.", + "subtitle": "Pega tu frase secreta de recuperación", "cta_text": "Continuar", "paste": "Pegar", "clear": "Borrar todo", @@ -4239,7 +4261,7 @@ "24_word_option": "Tengo una frase de 24 palabras", "error_title": "Algo salió mal", "error_message": "No se pudo importar esa frase secreta de recuperación. Asegúrese de haberla ingresado correctamente.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", + "error_empty_message": "Debes ingresar tu frase secreta de recuperación.", "error_number_of_words_error_message": "Las frases secretas de recuperación contienen 12 o 24 palabras", "error_srp_is_case_sensitive": "¡Entrada no válida! La frase secreta de recuperación distingue entre mayúsculas y minúsculas.", "error_srp_word_error_1": "Palabra ", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " son incorrectas o están mal escritas.", "error_invalid_srp": "Frase secreta de recuperación no válida", "error_duplicate_srp": "Esta frase secreta de recuperación ya se importó.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "La cuenta que estás intentando importar es un duplicado.", + "invalid_qr_code_title": "Código QR no válido", + "invalid_qr_code_message": "Este código QR no contiene ninguna frase secreta de recuperación válida", "success_1": "Billetera", "success_2": "importado" }, @@ -4665,7 +4687,7 @@ "button": "Proteger monedero" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "Error en la actualización de la transacción", "text": "¿Quiere intentarlo de nuevo?", "cancel_button": "Cancelar", "retry_button": "Intente de nuevo" @@ -4684,13 +4706,13 @@ "next": "Siguiente", "amount_placeholder": "0.00", "link_copied": "Vínculo copiado en el Portapapeles", - "send_link_title": "Send link", + "send_link_title": "Enviar vínculo", "description_1": "El vínculo de la solicitud está listo para enviarlo", "description_2": "Envíele este vínculo a un amigo y le pedirá que envíe", "copy_to_clipboard": "Copiar al Portapapeles", "qr_code": "Código QR", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "Enviar vínculo", + "request_qr_code": "Código QR de solicitud de pago", "balance": "Saldo" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "No tiene sesiones activas", - "end_session_title": "End session", + "end_session_title": "Finalizar sesión", "end": "Finalizar", "cancel": "Cancelar", - "session_ended_title": "Session ended", + "session_ended_title": "Sesión finalizada", "session_ended_desc": "La sesión seleccionada terminó", "session_already_exist": "Esta sesión ya está conectada.", "close_current_session": "Cierre la sesión actual antes de iniciar una nueva." @@ -4765,15 +4787,14 @@ "on_network": "en {{networkName}}", "debit_card": "Tarjeta de débito", "select_payment_method": "Seleccionar método de pago", - "loading_quote": "Loading quote...", "pay_with": "Pagar con", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "Comprando a través de {{providerName}}.", + "change_provider": "Cambiar de proveedor.", "payment_error": "Se produjo un error. Inténtalo de nuevo.", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "No hay métodos de pago disponibles.", "error_fetching_quotes": "Se produjo un error. Inténtalo de nuevo.", "no_quotes_available": "No hay proveedores disponibles.", - "providers": "Providers", + "providers": "Proveedores", "continue": "Continuar", "powered_by_provider": "Desarrollado por {{provider}}", "purchased_currency": "{{currency}} comprado", @@ -4871,6 +4892,15 @@ "log_out": "Cerrar sesión en {{provider}}", "logged_out_success": "La sesión se cerró correctamente", "logged_out_error": "Error al cerrar sesión" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "menor límite de venta", "medium_sell_limit": "límite medio de venta", "highest_sell_limit": "mayor límite de venta", - "change": "Change", + "change": "Cambiar", "continue_to_amount": "Continuar al monto", "no_payment_methods_title": "No hay métodos de pago en {{regionName}}", "no_cash_destinations_title": "No hay destinos de efectivo en {{regionName}}", @@ -5118,7 +5148,7 @@ "start_swapping": "Comenzar a canjear" }, "feature_off_title": "Temporalmente no disponible", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "MetaMask Swaps está en mantenimiento. Vuelve a comprobarlo más tarde.", "wrong_network_title": "Los intercambios no están disponibles", "wrong_network_body": "Solo puede canjear tokens en la red principal de Ethereum.", "unallowed_asset_title": "No se puede canjear este token", @@ -5160,7 +5190,7 @@ "not_enough": "No hay {{symbol}} suficientes para completar este intercambio", "max_slippage": "Deslizamiento máximo", "max_slippage_amount": "Deslizamiento máximo {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "Si la tasa cambia entre el momento en que realizas la orden y cuando se confirma, se denomina “deslizamiento”. Tu canje se cancelará automáticamente si el deslizamiento supera lo establecido en la configuración “deslizamiento máximo”.", "slippage_warning": "Asegúrese de saber lo que está haciendo.", "allows_up_to_decimals": "{{symbol}} permite hasta {{decimals}} decimales", "get_quotes": "Obtener cotizaciones", @@ -5199,7 +5229,7 @@ "edit": "Editar", "quotes_include_fee": "Las cotizaciones incluyen un {{fee}} % de tarifa de MetaMask", "quotes_include_gas_and_metamask_fee": "La cotización incluye el gas y una tarifa del {{fee}} % de MetaMask", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "Toca para canjear", "swipe_to_swap": "Deslice para canjear", "swipe_to": "Deslice para", "swap": "Canjear", @@ -5259,7 +5289,7 @@ "approve": "Aprobar {{sourceToken}} para intercambios: Hasta {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "Canje pendiente ({{sourceToken}} por {{destinationToken}})", "swap_confirmed": "Intercambio completo ({{sourceToken}} a {{destinationToken}})", "approve_pending": "Aprobación de {{sourceToken}} para swaps", "approve_confirmed": "{{sourceToken}} aprobado para intercambios" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "El menú desplegable de red se ha trasladado a tus activos", "description_2": "Canjea y puentea en un flujo simple", - "description_3": "Streamlined send experience", + "description_3": "Experiencia de envío optimizada", "description_4": "Una nueva vista de la cuenta" }, "more_information": "Ahora puedes concentrarte en tus tokens y actividad, no en las redes que hay detrás de ellos.", @@ -5406,21 +5436,21 @@ "aggressive_label": "Agresivo", "aggressive_text": "Alta probabilidad, incluso en mercados volátiles. Use Agresivo para cubrir los aumentos repentinos en el tráfico de la red debido a cosas como drops de NFT populares.", "market_label": "Mercado", - "market_text": "Use market for fast processing at current market price.", + "market_text": "Utiliza mercado para un procesamiento rápido al precio actual del mercado.", "low_label": "Bajo", "low_text": "Usa mínimo para esperar un precio más bajo. Las estimaciones de tiempo son mucho menos precisas, ya que los precios son un poco imprevisibles.", "link": "Obtenga más información sobre cómo personalizar su cuota de gas." }, "save": "Guardar", "submit": "Enviar", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "La tarifa de prioridad máxima es baja para las condiciones actuales de la red", + "max_priority_fee_high": "La tarifa de prioridad máxima es más alta de lo necesario", + "max_priority_fee_speed_up_low": "La tarifa de prioridad máxima debe ser de al menos {{speed_up_floor_value}} GWEI (10 % más que la transacción inicial)", + "max_priority_fee_cancel_low": "La tarifa de prioridad máxima debe ser de al menos {{cancel_value}} GWEI (50 % más que la transacción inicial)", + "max_fee_low": "La tarifa máxima es baja para las condiciones actuales de la red", + "max_fee_high": "La tarifa máxima es más alta de lo necesario", + "max_fee_speed_up_low": "La tarifa máxima debe ser de al menos {{speed_up_floor_value}} GWEI (10 % más que la transacción inicial)", + "max_fee_cancel_low": "La tarifa máxima debe ser de al menos {{cancel_value}} GWEI (50 % más que la transacción inicial)", "learn_more_gas_limit": "El límite de gas son las unidades máximas que está dispuesto a usar. Las unidades de gas son un multiplicador de la \"Tarifa de prioridad máxima\" y la \"Tarifa máxima\". ", "learn_more_max_priority_fee": "La tarifa de prioridad máxima (también conocida como \"propina del minero\") va directamente a los mineros y los incentiva a priorizar su transacción. La mayoría de las veces pagará su configuración máxima. ", "learn_more_max_fee": "La tarifa máxima es lo máximo que va a pagar (tarifa base + tarifa de prioridad). ", @@ -5530,9 +5560,9 @@ "enable_remember_me_description": "Cuando Recordarme está activado, cualquier persona con acceso a su teléfono puede acceder a su cuenta de MetaMask." }, "turn_off_remember_me": { - "title": "Ingrese su contraseña para desactivar Recordarme", - "placeholder": "Contraseña", - "description": "Si desactiva esta opción, necesitará su contraseña para desbloquear MetaMask de ahora en adelante.", + "title": "Desactivar Recordarme", + "placeholder": "Confirmar contraseña", + "description": "Una vez desactivada, Recordarme no se puede volver a usar. Esta función ya no está disponible, así que puedes desbloquear MetaMask con tu contraseña o datos biométricos.", "action": "Desactivar Recordarme" }, "dapp_connect": { @@ -5582,7 +5612,7 @@ "learn_more": "Conozca más" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "Verificar detalles de terceros", "protect_from_scams": "Para protegerse contra estafadores, tómese un momento para verificar los datos del tercero.", "learn_to_verify": "Más información sobre cómo verificar los datos de terceros", "spending_cap": "límite de gasto", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "Restauración necesaria", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "¡Algo salió mal, pero no te preocupes! Intentemos restaurar tu billetera.", "restore_needed_action": "Restaurar monedero" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Error al cerrar la aplicación en ejecución en su dispositivo Ledger.", "ethereum_app_not_installed": "La aplicación Ethereum no está instalada.", "ethereum_app_not_installed_error": "Instale la aplicación Ethereum en su dispositivo Ledger.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "La aplicación Ethereum no está abierta", + "eth_app_not_open_message": "Abre la aplicación Ethereum en tu dispositivo Ledger.", "ledger_is_locked": "El Ledger está bloqueado", "unlock_ledger_message": "Desbloquee su dispositivo Ledger", "cannot_get_account": "No se puede obtener la cuenta", @@ -5797,8 +5827,8 @@ "error_description": "Instalación fallida de {{snap}}." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "Un bono anual que podrás reclamar diariamente desde tu billetera.", + "earn_a_percentage_bonus": "Gana un bono del {{percentage}} %", "claimable_bonus": "Bonificación reclamable", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "El tiempo que lleva retirar tu token del protocolo y recuperarlo en tu billetera", "receive": "Este token se utiliza para hacer un seguimiento de tus activos y recompensas. No los transfieras ni los intercambies, ya que no podrás retirarlos.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "Tu factor de salud mide el riesgo de liquidación", "above_two_dot_zero": "Por encima de 2,0", "safe_position": "Posición segura", "between_one_dot_five_and_2_dot_zero": "Entre 1,5 y 2,0", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "Riesgo de liquidación medio", "below_one_dot_five": "Por debajo de 1,5", "higher_liquidation_risk": "Mayor riesgo de liquidación" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "¿Por qué no puedo retirar mi saldo completo?", "your_withdrawal_amount_may_be_limited_by": "Tu monto de retiro puede estar limitado por", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "Falta de liquidez", "not_enough_funds_available_in_the_lending_pool_right_now": "No hay suficientes fondos disponibles en el fondo de préstamos en este momento.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "Posiciones de préstamo existentes", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "Retirar tu dinero podría poner en riesgo de liquidación tus posiciones de préstamos existentes." } }, @@ -5998,11 +6028,11 @@ "earn_button": "Ganar" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "Haz staking de TRX y gana", + "stake_any_amount": "Haz staking de cualquier cantidad de TRX.", "earn_trx_rewards": "Gana recompensas de TRX.", "earn_trx_rewards_description": "Empieza a ganar en cuanto realices el staking. Las recompensas se acumulan automáticamente.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "Deja de hacer staking en cualquier momento. Normalmente tarda 14 días en procesarse." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "Tarifa estimada de gas", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "Las tarifas de gas se pagan a los mineros de criptomonedas que procesan transacciones en la red Ethereum. MetaMask no obtiene ganancias de las tarifas de gas.", "gas_fluctuation": "Las tarifas de gas se estiman y fluctuarán según el tráfico de la red y la complejidad de las transacciones.", "gas_learn_more": "Más información acerca de las tarifas de gas" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "Iniciando sesión con", "spender": "Gastador", "now": "Ahora", - "switching_to": "Switching to", + "switching_to": "Cambiando a", "bridge_estimated_time": "Tiempo est.", "pay_with": "Pagar con", - "receive_as": "Receive", + "receive_as": "Recibir", "total": "Total", - "you_receive": "You'll receive", + "you_receive": "Recibirás", "transaction_fee": "Tarifa de transacción", - "transaction_fees": "Transaction fees", + "transaction_fees": "Tarifas de transacción", "metamask_fee": "Tarifa de MetaMask", "network_fee": "Tarifa de red", "bridge_fee": "Tarifa del proveedor del puente" @@ -6234,7 +6264,7 @@ "transaction_fee": "Canjearemos tus tokens por USDC en Polygon, la red que usa Predicciones. Los proveedores de canje pueden cobrar una comisión, pero MetaMask no." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMask se cambiará al token que desees. No se aplica ninguna comisión de MetaMask al cambiar a MUSD." }, "musd_conversion": { "transaction_fee": "Las tarifas de conversión de mUSD incluyen los costos de red y pueden incluir las tarifas del proveedor." @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "Este sitio solicita su firma", "transaction_tooltip": "Este sitio está solicitando su transacción", "details": "Detalles", - "qr_get_sign": "Get signature", + "qr_get_sign": "Obtener firma", "qr_scan_text": "Escanee con su monedero físico", "sign_with_ledger": "Firmar con Ledger", "smart_account": "Cuenta inteligente", "smart_contract": "Contrato inteligente", - "standard_account": "Standard account", + "standard_account": "Cuenta estándar", "siwe_message": { "url": "URL", "network": "Red", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "Cuenta inteligente", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "Cuenta estándar", "switch": "Cambiar", "switchBack": "Volver a cambiar", "includes_transaction": "Incluye {{transactionCount}} transacciones", @@ -6307,9 +6337,9 @@ "cancel": "Cancelar", "description": "Ingresa el monto que consideras cómodo para que se gaste en tu nombre.", "invalid_number_error": "El límite de gasto debe ser un número", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "El límite de gasto no puede estar vacío", + "no_extra_decimals_error": "El límite de gasto no puede tener más decimales que el token", + "no_zero_error": "El límite de gasto no puede ser 0", "no_zero_error_decrease_allowance": "Un límite de gasto de 0 no tiene ningún efecto sobre el método de 'decreaseAllowance'", "no_zero_error_increase_allowance": "Un límite de gasto de 0 no tiene ningún efecto sobre el método de 'increaseAllowance'", "save": "Guardar", @@ -6336,7 +6366,7 @@ "transferRequest": "Solicitud de transferencia", "nested_transaction_heading": "{{index}} de transacción", "transaction": "Transacción", - "available_balance": "Available balance: ", + "available_balance": "Saldo disponible: ", "edit_amount_done": "Continuar", "deposit_edit_amount_done": "Agregar fondos", "deposit_edit_amount_predict_withdraw": "Retirar", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "Términos y condiciones", "select_token": "Seleccione un token", "no_tokens_found": "No se encontraron tokens", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "No encontramos ningún token con este nombre. Prueba con otra búsqueda.", "select_network": "Seleccionar red", "all_networks": "Todas las redes", "num_networks": "{{numNetworks}} redes", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "Deseleccionar todo", "see_all": "Ver todo", "all": "Todas", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}} más", "apply": "Aplicar", "slippage": "Deslizamiento", "slippage_info": "Si el precio cambia entre el momento en que se coloca la orden y cuando se confirma, se denomina \"deslizamiento\". Su intercambio se cancelará automáticamente si el deslizamiento supera la tolerancia que establezca aquí.", @@ -6392,7 +6422,7 @@ "quote_info_title": "Tasa", "network_fee_info_title": "Tarifa de red", "network_fee_info_content": "Las tarifas de red dependen de qué tan ocupada esté la red y de qué tan compleja sea tu transacción.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "Esta tarifa de red la paga MetaMask, por lo que puedes realizar transacciones sin {{nativeToken}} en tu cuenta.", "points": "Puntos estimados", "points_tooltip": "Puntos", "points_tooltip_content_1": "Los puntos son la forma de ganar recompensas de MetaMask por completar transacciones, como cuando canjeas, puenteas u operas con contratos perpetuos.", @@ -6406,7 +6436,7 @@ "select_recipient": "Seleccionar destinatario", "external_account": "Cuenta externa", "error_banner_description": "Esta ruta de operación no está disponible en este momento. Intente cambiar el monto, la red o el token y encontraremos la mejor opción.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "Esta ruta de intercambio no está disponible en este momento. Intenta cambiar la cantidad, la red o el token y encontraremos la mejor opción.\n\nTen en cuenta que si intentas operar con acciones tokenizadas de Ondo, podrías estar sujeto a restricciones geográficas, por ejemplo, en EE. UU., la UE, el Reino Unido y Brasil.", "insufficient_funds": "Fondos insuficientes", "insufficient_gas": "Gas insuficiente", "select_amount": "Seleccionar monto", @@ -6417,9 +6447,9 @@ "title": "Puentear", "submitting_transaction": "Enviando", "fetching_quote": "Obteniendo cotización", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "Incluye {{feePercentage}} % de la tarifa de MetaMask.", "no_mm_fee": "Sin tarifa de MM", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "Sin tarifa de MetaMask al canjear por {{destTokenSymbol}}.", "hardware_wallet_not_supported": "Las billeteras físicas aún no son compatibles. Usa una billetera en caliente para continuar.", "hardware_wallet_not_supported_solana": "Las billeteras físicas aún no son compatibles con Solana. Usa una billetera en caliente para continuar.", "price_impact_info_title": "Impacto sobre el precio", @@ -6432,17 +6462,24 @@ "approval_needed": "Aprueba el token para el canje.", "approval_tooltip_title": "Conceder acceso exacto", "approval_tooltip_content": "Permitirás el acceso a la cantidad especificada, {{amount}} {{symbol}}. El contrato no accederá a fondos adicionales.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "Mínimo recibido", + "minimum_received_tooltip_title": "Mínimo recibido", "minimum_received_tooltip_content": "La cantidad mínima que recibirás si el precio cambia mientras se procesa tu transacción, según tu tolerancia al deslizamiento. Esta es una estimación de nuestros proveedores de liquidez. Los montos finales pueden variar.", + "market_closed": { + "title": "El mercado está cerrado", + "description": "El mercado que respalda este token está actualmente cerrado. Los tokens se pueden transferir dentro de la cadena en cualquier momento.", + "learn_more": "Conozca más", + "learn_more_url": "https://status.ondo.finance/market", + "done": "Hecho" + }, "submit": "Enviar", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "Tu transacción no se realizará si el precio cambia más que el porcentaje de deslizamiento.", "cancel": "Cancelar", "confirm": "Confirmar", "exceeding_upper_slippage_warning": "Alto deslizamiento, lo que puede resultar en un canje desfavorable", "exceeding_lower_slippage_warning": "Bajo deslizamiento, lo que puede resultar en un canje desfavorable", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "Introduce un valor mayor al {{value}} %", + "exceeding_upper_slippage_error": "No puedes introducir un valor mayor al {{value}} %", "custom": "Personalizar" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "Recuperación de billetera", "login_with_social": "Iniciar sesión con cuentas de redes sociales", "setup": "Configuración", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "Frase secreta de recuperación {{num}}", "back_up": "Respaldo", "reveal": "Revelar", "social_recovery_title": "RECUPERACIÓN DE {{authConnection}}", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "Ingresar contraseña", "description": "Ingresa la contraseña de tu billetera para ver los detalles de la tarjeta.", + "description_unfreeze": "Ingresa la contraseña de tu billetera para reanudar el gasto con tu tarjeta.", "placeholder": "Contraseña", "confirm": "Confirmar", "cancel": "Cancelar", @@ -7001,6 +7039,7 @@ "enable_card_error": "No se pudo habilitar la tarjeta. Inténtelo de nuevo más tarde.", "view_card_details_error": "No se pueden cargar los detalles de la tarjeta. Inténtalo de nuevo.", "biometric_verification_required": "Se requiere autenticación para ver los detalles de la tarjeta.", + "unfreeze_auth_required": "Se requiere autenticación para reanudar el gasto con tu tarjeta.", "warnings": { "close_spending_limit": { "title": "Estás cerca de tu límite de gasto", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "Tu tarjeta está suspendida", - "description": "Comunícate con soporte para descongelar tu tarjeta" + "description": "Tu tarjeta está suspendida temporalmente. Puedes reactivarla en cualquier momento." }, "blocked": { "title": "Tu tarjeta está bloqueada", @@ -7068,7 +7107,14 @@ "travel_description": "Reserva hoteles con hasta un 70 % de descuento", "card_tos_title": "Términos y condiciones", "order_metal_card": "Tarjeta Metal", - "order_metal_card_description": "Solicita ya tu tarjeta Metal física" + "order_metal_card_description": "Solicita ya tu tarjeta Metal física", + "freeze_card": "Tarjeta suspendida", + "unfreeze_card": "Reactivar la tarjeta", + "freeze_card_description": "Pausa todos los gastos de tu tarjeta", + "unfreeze_card_description": "Reanuda todos los gastos con tu tarjeta", + "freeze_error": "No se pudo actualizar el estado de la tarjeta. Inténtalo de nuevo.", + "freeze_success": "Tarjeta suspendida correctamente", + "unfreeze_success": "Tarjeta reactivada correctamente" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "Reenvío disponible en {{seconds}} segundos" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "Agregar a {{walletName}}", + "adding_to_wallet": "Agregando a {{walletName}}...", + "continue_setup": "Continuar con la configuración de {{walletName}}", + "wallet_not_available": "{{walletName}} no disponible", + "already_in_wallet": "Ya en {{walletName}}", + "success_title": "¡Tarjeta añadida!", + "success_message": "Tu tarjeta MetaMask se ha agregado a {{walletName}}.", + "error_title": "No se puede agregar la tarjeta", + "error_wallet_not_available": "{{walletName}} no está disponible en este dispositivo. Asegúrate de tener {{walletName}} configurada.", + "error_wallet_not_initialized": "{{walletName}} no se ha inicializado. Configura tu billetera y vuelve a intentarlo.", "error_card_already_in_wallet": "Esta tarjeta ya está agregada a {{walletName}}.", "error_card_pending": "Tu tarjeta se está configurando en {{walletName}}. Vuelve a intentarlo en unos minutos.", "error_card_suspended": "Tu tarjeta en {{walletName}} ha sido suspendida. Para obtener ayuda, contacta al equipo de soporte.", "error_card_not_eligible": "Esta tarjeta no es elegible para el aprovisionamiento de billetera móvil.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "No se pudieron cifrar los datos de la tarjeta. Inténtalo de nuevo.", "error_invalid_card_data": "Datos de tarjeta inválidos. Verifica los datos de tu tarjeta y vuelve a intentarlo.", "error_card_not_found": "No se encontró la tarjeta. Inténtalo de nuevo.", "error_card_provider_not_found": "El proveedor de la tarjeta no está disponible para tu región.", "error_card_id_mismatch": "Error en la verificación de la tarjeta. Inténtalo de nuevo.", "error_card_not_active": "Tu tarjeta no está activa. Actívala primero.", "error_network": "Se produjo un error de red. Comprueba tu conexión e inténtalo de nuevo.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "Se agotó el tiempo de espera de la solicitud. Inténtalo de nuevo.", + "error_server": "Error del servidor. Vuelve a intentarlo más tarde.", + "error_unknown": "Se produjo un error inesperado. Inténtalo de nuevo o contacta con el servicio de asistencia.", + "error_platform_not_supported": "Esta plataforma no admite el aprovisionamiento de billeteras móviles.", "try_again": "Inténtalo de nuevo", "cancel": "Cancelar" } @@ -7299,7 +7345,7 @@ "main_title": "Recompensas", "referral_title": "Referidos", "tab_overview_title": "Resumen general", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "Instantáneas", "tab_activity_title": "Actividad", "referral_stats_earned_from_referrals": "Ganancias por referidos", "referral_stats_referrals": "Recomendados", @@ -7353,7 +7399,7 @@ "verifying_rewards": "Nos aseguramos de que todo sea correcto antes de que reclames tus recompensas." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "Puntos ganados" }, "onboarding": { "not_supported_region_title": "Región no admitida", @@ -7431,7 +7477,7 @@ "show_less": "Mostrar menos", "linking_progress": "Agregando cuentas... ({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}} inscritos", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "Agregar todas las cuentas" }, "referred_by_code": { "title": "Código de recomendación", @@ -7514,7 +7560,7 @@ "claim_label": "Reclamar", "claimed_label": "Reclamada", "reward_claimed": "Recompensa reclamada", - "time_left": "{{time}} left", + "time_left": "{{time}} restante", "expired": "Vencida" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "Canje fallido", "redeem_failure_description": "Inténtalo de nuevo más tarde.", "reward_details": "Detalles de la recompensa", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "Selecciona la cuenta donde deseas enviar esta recompensa." }, "animation": { "could_not_load": "No se pudo cargar" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", + "starts_date": "Empieza el {{date}}", + "ends_date": "Termina el {{date}}", + "results_coming_soon": "Resultados próximamente", + "tokens_on_the_way": "Los tokens están llegando", + "pill_up_next": "A continuación", + "pill_live_now": "En vivo ahora", "pill_calculating": "Calculando", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "pill_results_ready": "Resultados listos", + "pill_complete": "Completado" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "Instantáneas", + "error_title": "No se pueden cargar las instantáneas", + "error_description": "No pudimos cargar las instantáneas. Inténtalo de nuevo.", "retry_button": "Reintentar" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "Activa", + "upcoming_title": "Próxima", + "previous_title": "Previa", + "empty_state": "No hay instantáneas disponibles", + "error_title": "No se pueden cargar las instantáneas", + "error_description": "No pudimos cargar las instantáneas. Inténtalo de nuevo.", "retry_button": "Reintentar", - "refreshing": "Refreshing..." + "refreshing": "Actualizando..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "Aprobar {{approveSymbol}}", "bridge_approval_loading": "Aprobar", "bridge_send": "Puentear {{sourceSymbol}} desde {{sourceChain}}", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "Puente enviado", "bridge_receive": "Recibir {{targetSymbol}} en {{targetChain}}", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "Puente recibido", "default": "Transacción", "musd_convert_send": "{{sourceSymbol}} enviado(s) desde {{sourceChain}}", "musd_claim": "Reclamar mUSD", @@ -7607,20 +7653,20 @@ "description": "Estableciendo conexión con {{dappName}}..." }, "show_error": { - "title": "Connection error", + "title": "Error de conexión", "description": "No se pudo establecer la conexión. Inténtalo de nuevo." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "Aprobación rechazada", + "description": "El usuario rechazó la solicitud." }, "show_return_to_app": { "title": "Éxito", "description": "Regresa a la aplicación para continuar." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "Conexión no encontrada", + "description": "Establece una nueva conexión desde la aplicación para continuar." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "Explorar", + "trending_tokens": "Tokens de tendencia", "price_change": "Cambio de precio", "all_networks": "Todas las redes", "24h": "24h", "time": "Hora", "24_hours": "24 horas", "6_hours": "6 horas", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 hora", + "5_minutes": "5 minutos", "networks": "Redes", "sort_by": "Ordenar por", "volume": "Volumen", @@ -7650,32 +7696,48 @@ "high_to_low": "De máximo a mínimo", "low_to_high": "De mínimo a máximo", "apply": "Aplicar", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "Buscar tokens, sitios, URL", "cancel": "Cancelar", "perps": "Perps", "predictions": "Predicciones", - "no_results": "No results found", + "no_results": "No se encontraron resultados", "sites": "Sitios", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "Sitios populares", + "search_sites": "Sitios de búsqueda", + "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.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "Los tokens de tendencia no están disponibles", + "description": "No podemos obtener esta página en este momento", "try_again": "Inténtalo de nuevo" }, "empty_search_result_state": { "title": "No se encontraron tokens", - "description": "We were not able to find this token" + "description": "No pudimos encontrar este token" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "Actualización lista", + "description_ios": "Hemos realizado algunas correcciones importantes. Actualiza la versión de MetaMask.", + "description_android": "Hemos realizado algunas correcciones importantes. Cierra y vuelve a abrir MetaMask para aplicar la actualización.", "primary_action_reload": "Recargar", "primary_action_acknowledge": "Entendido" + }, + "homepage": { + "sections": { + "tokens": "Tokens", + "perpetuals": "Contratos perpetuos", + "predictions": "Predicciones", + "defi": "DeFi", + "nfts": "NFT", + "import_nfts": "Importar NFT", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/fr.json b/locales/languages/fr.json index b8be1c72a28..708924c24f9 100644 --- a/locales/languages/fr.json +++ b/locales/languages/fr.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "définitivement supprimées", "reset_wallet_desc_2": "de MetaMask sur cet appareil. Cette opération est irréversible.", "reset_wallet_desc_login": "Pour restaurer votre portefeuille, vous pouvez utiliser votre phrase secrète de récupération ou le mot de passe de votre compte Google ou Apple. MetaMask ne dispose pas de ces informations.", - "reset_wallet_desc_srp": "Pour restaurer votre portefeuille, assurez-vous de disposer de votre phrase secrète de récupération. MetaMask ne dispose pas de cette information." + "reset_wallet_desc_srp": "Pour restaurer votre portefeuille, assurez-vous de disposer de votre phrase secrète de récupération. MetaMask ne dispose pas de cette information.", + "biometric_authentication_cancelled": "Authentification biométrique annulée", + "biometric_authentication_cancelled_title": "Échec de la configuration biométrique", + "biometric_authentication_cancelled_description": "Veuillez reconfigurer l’authentification biométrique à partir des paramètres.", + "biometric_authentication_cancelled_button": "Confirmer" }, "connect_hardware": { "title_select_hardware": "Connecter un portefeuille matériel", @@ -1040,7 +1044,7 @@ "title": "Montant à déposer", "get_usdc_hyperliquid": "Obtenez des USDC • Hyperliquid", "insufficient_funds": "Fonds insuffisants", - "no_funds_available": "Aucun fonds disponible. Veuillez d’abord effectuer un dépôt.", + "no_funds_available": "Fonds insuffisants. Effectuez un dépôt ou sélectionnez un autre mode de paiement", "enter_amount": "Saisissez le montant", "fetching_quote": "Récupération de la cotation", "submitting": "En tain de soumettre la transaction", @@ -1970,8 +1974,8 @@ "trade_again": "Trader à nouveau", "activity": { "deposit_title": "Déposer", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "Dépôt de {{amount}} {{symbol}}", + "withdrew_amount": "Retrait de {{amount}} {{symbol}}", "status_completed": "Terminé", "status_failed": "Échec", "status_pending": "En attente" @@ -2051,6 +2055,16 @@ "referral_code_text": "Utilisez mon code de parrainage pour obtenir des récompenses supplémentaires." } }, + "market_insights": { + "title": "Aperçu du marché", + "updated_ago": "Mises à jour à {{time}}", + "disclaimer": "Les perçus du marché générés par l’IA ne constituent pas des conseils financiers.", + "whats_driving_price": "Qu’est-ce qui influence le prix ?", + "what_people_saying": "Ce que les gens en disent", + "trade_button": "Trader", + "sources_count": "+{{count}} sources", + "sources_title": "Sources de liquidités" + }, "predict": { "title": "Prédictions MetaMask", "prediction_markets": "Marchés prédictifs", @@ -2384,8 +2398,8 @@ "no_available_tokens": "Vous ne voyez pas votre jeton ?", "add_tokens": "Importer des jetons", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "{{tokenCount}} nouveaux {{tokensLabel}} trouvés dans ce compte", "token_toast": { "tokens_imported_title": "Jetons importés", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "Le nombre de décimales du jeton ne peut être vide.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "Nous n’avons pas trouvé de jetons portant ce nom.", + "tokens_empty_description": "Search for any token and import it", "select_token": "Sélectionnez un jeton", "address_must_be_smart_contract": "Adresse personnelle détectée. Saisissez l’adresse du contrat de jeton.", "billion_abbreviation": "Mrd", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "Déconnecter tous les comptes", "deceptive_site_ahead": "Site trompeur à venir", "deceptive_site_desc": "Le site que vous essayez de visiter n’est pas sécurisé. Les pirates peuvent vous inciter à agir de façon risquée.", + "malicious_site_detected": "Site malveillant détecté", + "malicious_site_warning": "Si vous vous connectez à ce site, vous risquez de perdre tous vos actifs.", + "connect_anyway": "Se connecter quand même", "learn_more": "En savoir plus", "advisory_by": "Avis fourni par le détecteur d’hameçonnage Ethereum et PhishFort", "potential_threat": "Parmi les menaces potentielles, il y a", @@ -2846,7 +2864,11 @@ "permissions": "Autorisations", "card_title": "MetaMask Card", "settings": "Paramètres", - "log_out": "Déconnexion" + "networks": "Réseaux", + "log_out": "Déconnexion", + "notifications": "Notifications", + "buy": "Acheter", + "scan": "Scanner" }, "app_settings": { "enabling_notifications": "Activation des notifications…", @@ -2870,6 +2892,8 @@ "state_logs": "Journaux d’état", "add_network_title": "Ajouter un réseau", "auto_lock": "Verrouillage automatique", + "enable_device_authentication": "Activer l’authentification de l’appareil", + "enable_device_authentication_desc": "Utilisez l’authentification biométrique ou le code d’accès de votre appareil pour déverrouiller MetaMask.", "auto_lock_desc": "Définissez le délai avant le verrouillage automatique de l’application.", "state_logs_desc": "Cela aidera MetaMask à déboguer tout problème éventuel rencontré. Veuillez l’envoyer au service d’assistance MetaMask via l’icône de hamburger > Envoyer un commentaire, ou répondez à votre ticket existant si vous en avez un.", "autolock_immediately": "Immédiatement", @@ -2975,6 +2999,11 @@ "add_rpc_url": "Ajouter l’URL de l’appel de procédure à distance (RPC)", "add_block_explorer_url": "Ajouter l’URL de l’explorateur de blocs", "networks_desc": "Ajouter et modifier des réseaux RPC personnalisés", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "Rechercher un réseau", + "networks_no_results": "Aucun réseau trouvé", "network_name_label": "Nom du réseau", "network_name_placeholder": "Nom du réseau (facultatif)", "network_rpc_url_label": "URL de l’appel de procédure à distance", @@ -2991,7 +3020,16 @@ "network_other_networks": "Autres réseaux", "network_rpc_networks": "Réseaux RPC", "network_add_network": "Ajouter un réseau", + "add_chain_title": "Ajouter un réseau", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "Réessayer", + "add_chain_added": "Added", + "add_chain_or": "ou", + "add_chain_custom_link": "Ajouter un réseau personnalisé", "network_add_custom_network": "Ajouter un réseau personnalisé", + "network_add_test_network": "Add a test network", "network_add": "Ajouter", "network_save": "Enregistrer", "remove_network_title": "Voulez-vous supprimer ce réseau ?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "OK", "title": "Impossible de connecter le compte", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "Veuillez scanner le code QR affiché sur le site pour vous reconnecter à MetaMask" }, "app_information": { "title": "Informations", @@ -3379,6 +3417,7 @@ "sell_description": "Convertissez des crypto-monnaies en argent liquide" }, "asset_overview": { + "market_closed": "Marché fermé", "send_button": "Envoyer", "buy_button": "Acheter", "cash_buy_button": "Achat au comptant", @@ -3399,19 +3438,6 @@ "bridge": "Passerelle", "earn": "Gagner", "convert_to_musd": "Convertir en mUSD", - "merkl_rewards": { - "annual_bonus": "Bonus de {{apy}} %", - "claimable_bonus": "Bonus réclamable", - "claimable_bonus_tooltip_description": "Les bonus en mUSD sont réclamés sur Linea.", - "terms_apply": "Des conditions s’appliquent.", - "ok": "OK", - "claim": "Réclamer", - "processing_claim": "Traitement de la demande en cours…", - "claim_on_linea_title": "Réclamer les bonus sur Linea", - "claim_on_linea_description": "Votre bonus sera émis sur Linea, séparément de votre solde mUSD sur Ethereum.", - "continue": "Continuer", - "unexpected_error": "Erreur inattendue. Veuillez réessayer." - }, "tron": { "daily_resource_new_energy": "Nouvelle énergie quotidienne", "sufficient_to_cover": "Suffisant pour couvrir", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "Adresse du jeton copiée dans le presse-papiers" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "Code QR non valide", "invalid_qr_code_message": "Le code QR que vous essayez de scanner n’est pas valide.", "allow_camera_dialog_title": "Autoriser l’accès à la caméra", "allow_camera_dialog_message": "Nous avons besoin de votre autorisation pour scanner les codes QR", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "Il semble que vous essayez de vous synchroniser avec l’extension. Pour ce faire, vous devrez effacer votre portefeuille actuel. \n\nAprès avoir effacé ou réinstallé une nouvelle version de l’appli, sélectionnez l’option « Synchroniser avec l’extension MetaMask ». Attention ! Avant d’effacer votre portefeuille, effectuez une copie de votre phrase secrète de récupération.", "not_allowed_error_title": "Activer l’accès à l’appareil photo", "not_allowed_error_desc": "Pour scanner un code QR, vous devez autoriser MetaMask à accéder à l’appareil photo depuis le menu des paramètres de votre appareil.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "Code QR non reconnu", "unrecognized_address_qr_code_desc": "Désolé, ce code QR n’est pas associé à une adresse de compte ou à une adresse de contrat.", "url_redirection_alert_title": "Vous allez consulter un lien externe", "url_redirection_alert_desc": "Sachez que les liens peuvent être utilisés pour tenter d’escroquer ou d’hameçonner les utilisateurs. Faites donc bien attention de ne visiter que des sites Web fiables.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "Vous ne possédez pas cet objet de collection", "known_asset_contract": "Adresse connue du contrat d’actif", "max": "Max", - "recipient_address": "Recipient address", + "recipient_address": "Adresse du destinataire", "required": "Requis", "to": "À", "total": "Total", @@ -3641,7 +3667,7 @@ "nevermind": "Peu importe", "edit_network_fee": "Modifier les frais de gaz", "edit_priority": "Modifier la priorité", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "Frais de gaz d’annulation", "gas_speedup_fee": "Frais de gaz d’accélération", "use_max": "Utiliser le maximum", "set_gas": "Configurer", @@ -3650,7 +3676,7 @@ "transaction_fee": "Frais de gaz", "transaction_fee_less": "Pas de frais", "total_amount": "Montant total", - "view_data": "View data", + "view_data": "Afficher les données", "adjust_transaction_fee": "Ajuster les frais de transaction", "could_not_resolve_ens": "Impossible de résoudre l’ENS", "asset": "Actif", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "Pour naviguer sur le web décentralisé, ouvrez un nouvel onglet", "got_it": "J’ai compris", "max_tabs_title": "Le nombre maximum d’onglets a été atteint", - "max_tabs_desc": "Vous ne pouvez ouvrir que 5 onglets au maximum. Veuillez fermer des onglets avant d’en ouvrir d’autres.", + "max_tabs_desc": "Vous ne pouvez ouvrir que 20 onglets au maximum. Veuillez fermer des onglets avant d’en ouvrir d’autres.", "failed_to_resolve_ens_name": "Nous n’avons pas pu résoudre ce nom ENS", "remove_bookmark_title": "Supprimer le favori", "remove_bookmark_msg": "Souhaitez-vous vraiment supprimer ce site de vos favoris ?", @@ -3828,7 +3854,7 @@ "cancel_button": "Annuler" }, "approval": { - "title": "Confirm transaction" + "title": "Confirmer la transaction" }, "approve": { "title": "Approuver", @@ -3839,39 +3865,39 @@ "unavailable": "Non disponible", "tx_review_confirm": "Confirmer", "tx_review_transfer": "Transférer", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "Déploiement du contrat", + "tx_review_transfer_from": "Transfert depuis", + "tx_review_unknown": "Méthode inconnue", "tx_review_approve": "Approuver", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "Augmenter la provision", + "tx_review_set_approval_for_all": "Définir l’approbation pour tous", + "tx_review_staking_claim": "Réclamation de staking", "tx_review_staking_deposit": "Dépôt de staking", "tx_review_staking_unstake": "Déstaker", "tx_review_lending_deposit": "Dépôt de prêt", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "Retrait de prêt", "tx_review_perps_deposit": "Contrats à terme perpétuels financés", "tx_review_predict_deposit": "Prédictions financées", "tx_review_predict_claim": "Gains réclamés", "tx_review_predict_withdraw": "Retrait des prédictions", "tx_review_musd_conversion": "Conversion en mUSD", "claim": "Réclamer", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "ETH envoyé(s)", + "self_sent_ether": "ETH envoyé(s) à vous-même", + "received_ether": "ETH reçu(s)", "sent_dai": "DAI envoyé(s)", "self_sent_dai": "DAI envoyé(s) à vous-même", "received_dai": "DAI reçu(s)", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "Jetons envoyés", + "received_tokens": "Jetons reçus", "ether": "ETH", "sent_unit": "{{unit}} envoyé(s)", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "{{unit}} envoyé(s) à vous-même", "received_unit": "{{unit}} reçu(s)", "sent_collectible": "Objet de collection envoyé", "received_collectible": "Objet de collection reçu", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "ETH envoyé(s)", + "send_unit": "{{unit}} envoyé(s)", "send_collectible": "Envoyer un objet de collection", "receive_collectible": "Recevoir un objet de collection", "sent": "Envoyé(s)", @@ -3881,17 +3907,17 @@ "send": "Envoyer", "redeposit": "Refaire un dépôt", "interaction": "Interaction", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "Déploiement du contrat", + "to_contract": "Nouveau contrat", + "mint": "Minter", "tx_details_free": "Gratuit", "tx_details_not_available": "Non disponible", "smart_contract_interaction": "Interaction de contrats intelligents", "swaps_transaction": "Transaction de swap", "bridge_transaction": "Passerelle", "approve": "Approuver", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "Augmenter la provision", + "set_approval_for_all": "Définir l’approbation pour tous", "hash": "Hachage", "from": "De", "to": "À", @@ -3899,15 +3925,15 @@ "amount": "Montant", "fee": { "transaction_fee_in_ether": "Frais de transaction", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "Frais de transaction (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "Gaz utilisé (unités)", + "gas_limit": "Limite de gaz (unités)", + "gas_price": "Prix du gaz (GWEI)", + "base_fee": "Frais de base (GWEI)", + "priority_fee": "Frais de priorité (GWEI)", "multichain_priority_fee": "Frais de priorité", - "max_fee": "Max fee per gas", + "max_fee": "Frais maximaux par gaz", "total": "Total", "view_on": "Afficher sur", "view_on_etherscan": "Afficher sur Etherscan", @@ -3923,13 +3949,13 @@ "nonce": "Nonce", "from_device_label": "depuis cet appareil", "import_wallet_row": "Compte ajouté à cet appareil", - "import_wallet_label": "Account added", + "import_wallet_label": "Compte ajouté", "import_wallet_tip": "Toutes les transactions futures effectuées à partir de cet appareil porteront la mention « depuis cet appareil » à côté de l’horodatage. Pour les transactions antérieures à l’ajout du compte, cet historique ne précisera pas quelles transactions sortantes proviennent de cet appareil.", "sign_title_scan": "Scanner ", "sign_title_device": "avec votre portefeuille matériel", "sign_description_1": "Après avoir signé avec votre portefeuille matériel,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "Appuyez sur « Obtenir la signature »", + "sign_get_signature": "Obtenir la signature", "transaction_id": "ID de transaction", "network": "Réseau", "request_from": "Demande de", @@ -4032,7 +4058,7 @@ "title": "Réseaux", "other_networks": "Autres réseaux", "close": "Fermer", - "status_ok": "All systems operational", + "status_ok": "Tous les systèmes sont opérationnels", "status_not_ok": "Le réseau rencontre quelques problèmes", "want_to_add_network": "Vous voulez ajouter ce réseau ?", "add_custom_network": "Ajouter un réseau personnalisé", @@ -4051,7 +4077,7 @@ "review": "Examiner", "view_details": "Voir les détails", "network_details": "Détails du réseau", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "En sélectionnant « Confirmer », vous activez la vérification des détails du réseau. Vous pouvez désactiver la vérification des détails du réseau dans ", "network_settings_security_privacy": "Paramètres > Sécurité et confidentialité", "network_currency_symbol": "Symbole de la devise", "network_block_explorer_url": "URL de l’explorateur de blocs", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "Un fournisseur de réseau malveillant peut mentir quant à l’état de la blockchain et enregistrer votre activité sur le réseau. N’ajoutez que des réseaux personnalisés auxquels vous faites confiance.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Informations réseau", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "Informations supplémentaires sur les réseaux", "network_warning_desc": "Cette connexion réseau est assurée par des tiers. Elle peut être moins fiable ou permettre à des tiers de suivre l’activité des utilisateurs.", "additonial_network_information_desc": "Certains de ces réseaux dépendent de services tiers. Ils peuvent être moins fiables ou permettre à des tiers de suivre l’activité des utilisateurs.", "connect_more_networks": "Connecter plus de réseaux", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "Ce réseau est obsolète", "network_deprecated_description": "Le réseau auquel vous essayez de vous connecter n’est plus pris en charge par MetaMask.", "edit_networks_title": "Modifier les réseaux", - "no_network_fee": "No network fee" + "no_network_fee": "Pas de frais de réseau" }, "permissions": { "title_this_site_wants_to": "Ce site veut :", @@ -4111,11 +4137,11 @@ "network_connected": "réseau connecté ", "see_your_accounts": "Consulter vos comptes et suggérer des transactions", "connected_to": "Connecté à ", - "manage_permissions": "Manage permissions", + "manage_permissions": "Gérer les autorisations", "edit": "Modifier", "cancel": "Annuler", "got_it": "J’ai compris", - "connection_details_title": "Connection details", + "connection_details_title": "Détails de la connexion", "connection_details_description": "Vous vous êtes connecté à ce site le {{connectionDateTime}} en utilisant le navigateur MetaMask", "title_add_network_permission": "Ajouter une autorisation réseau", "add_this_network": "Ajouter ce réseau", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "Déverrouiller avec le code PIN de l’appareil ?" }, "authentication": { - "auth_prompt_title": "Authentification requise", - "auth_prompt_desc": "Veuillez vous authentifier afin d’utiliser MetaMask", - "fingerprint_prompt_title": "Authentification requise", - "fingerprint_prompt_desc": "Utilisez votre empreinte digitale pour déverrouiller MetaMask", - "fingerprint_prompt_cancel": "Annuler" + "auth_prompt_desc": "Veuillez vous authentifier afin d’utiliser MetaMask" }, "accountApproval": { "title": "DEMANDE DE CONNEXION", "walletconnect_title": "DEMANDE WALLETCONNECT", "action": "Se connecter à ce site ?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "Pour reprendre la connexion, choisissez le numéro affiché sur le site", + "action_reconnect_deeplink": "Voulez-vous vous reconnecter à ce site ?", "connect": "Connexion", "resume": "Reprendre", "cancel": "Annuler", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "Ne pas mémoriser la connexion à ce site", "disconnect": "Déconnecter", "permission": "Voir votre", "address": "adresse publique", @@ -4218,7 +4240,7 @@ "error_title": "Quelque chose a mal tourné", "error_message": "Nous n’avons pas pu importer cette clé privée. Veuillez vous assurer de l’avoir saisie correctement.", "error_empty_message": "Vous devez saisir votre clé privée", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "ou scanner un code QR" }, "import_private_key_success": { "title": "Compte importé !", @@ -4229,18 +4251,18 @@ "import_wallet_title": "Importer un portefeuille", "enter_srp_subtitle": "Saisissez votre phrase secrète de récupération", "textarea_placeholder": "Ajoutez une espace entre chaque mot et assurez-vous que personne ne vous épie", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "Saisissez la phrase secrète de récupération de votre portefeuille. Vous pouvez importer la phrase secrète de récupération de n’importe quel portefeuille Ethereum, Solana ou Bitcoin.", + "subtitle": "Collez votre phrase secrète de récupération", "cta_text": "Continuer", "paste": "Coller", "clear": "Tout effacer", "srp_number_of_words_option_title": "Nombre de mots", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "J’ai une phrase composée de 12 mots", + "24_word_option": "J’ai une phrase composée de 24 mots", "error_title": "Quelque chose a mal tourné", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "Nous n’avons pas pu importer cette phrase secrète de récupération. Veuillez vous assurer que vous l’avez saisie correctement.", + "error_empty_message": "Vous devez saisir votre phrase secrète de récupération.", + "error_number_of_words_error_message": "Les phrases secrètes de récupération sont composées de 12 ou 24 mots", "error_srp_is_case_sensitive": "Saisie non valide ! La phrase secrète de récupération est sensible à la casse.", "error_srp_word_error_1": "Les mots ", "error_srp_word_error_2": " est incorrect ou mal orthographié.", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " sont incorrects ou mal orthographiés.", "error_invalid_srp": "phrase secrète de récupération non valide", "error_duplicate_srp": "Cette phrase secrète de récupération a déjà été importée.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "Le compte que vous essayez d’importer est un doublon.", + "invalid_qr_code_title": "Code QR non valide", + "invalid_qr_code_message": "Le code QR ne contient pas de phrase secrète de récupération valide", "success_1": "Portefeuille", "success_2": "importés" }, @@ -4665,7 +4687,7 @@ "button": "Protéger le portefeuille" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "Échec de la mise à jour de la transaction", "text": "Réessayer ?", "cancel_button": "Annuler", "retry_button": "Réessayer" @@ -4684,13 +4706,13 @@ "next": "Suivant", "amount_placeholder": "0,00", "link_copied": "Lien copié dans le presse-papiers", - "send_link_title": "Send link", + "send_link_title": "Envoyer le lien", "description_1": "Votre lien de demande est prêt à être envoyé !", "description_2": "Envoyez ce lien à un(e) ami(e) pour lui demander d’envoyer", "copy_to_clipboard": "Copier dans le presse-papiers", "qr_code": "Code QR", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "Envoyer le lien", + "request_qr_code": "Code QR de demande de paiement", "balance": "Solde" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "Vous n’avez aucune session active", - "end_session_title": "End session", + "end_session_title": "Fermer la session", "end": "Fin", "cancel": "Annuler", - "session_ended_title": "Session ended", + "session_ended_title": "Session terminée", "session_ended_desc": "La session sélectionnée a été interrompue", "session_already_exist": "Cette séance est déjà connectée.", "close_current_session": "Fermez la session en cours avant d’en commencer une nouvelle." @@ -4765,15 +4787,14 @@ "on_network": "sur {{networkName}}", "debit_card": "Carte de débit", "select_payment_method": "Sélectionnez le mode de paiement", - "loading_quote": "Loading quote...", "pay_with": "Payer avec", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "Achat via {{providerName}}.", + "change_provider": "Changer de fournisseur.", "payment_error": "Un problème est survenu, veuillez réessayer.", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "Aucun moyen de paiement n’est disponible.", "error_fetching_quotes": "Un problème est survenu, veuillez réessayer.", "no_quotes_available": "Aucun fournisseur disponible.", - "providers": "Providers", + "providers": "Fournisseurs", "continue": "Continuer", "powered_by_provider": "Propulsé par {{provider}}", "purchased_currency": "{{currency}} acheté", @@ -4871,6 +4892,15 @@ "log_out": "Déconnectez-vous de {{provider}}", "logged_out_success": "Déconnexion réussie", "logged_out_error": "Erreur lors de la déconnexion" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "ordre « Sell Limit » le plus bas", "medium_sell_limit": "moyenne des ordres « Sell Limit »", "highest_sell_limit": "ordre « Sell Limit » le plus élevé", - "change": "Change", + "change": "Changer", "continue_to_amount": "Continuer vers le montant", "no_payment_methods_title": "Aucun moyen de paiement disponible dans la région suivante : {{regionName}}", "no_cash_destinations_title": "Aucun point de retrait disponible dans la région suivante : {{regionName}}", @@ -5118,7 +5148,7 @@ "start_swapping": "Commencez à échanger" }, "feature_off_title": "Temporairement indisponible", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "MetaMask Swaps est en cours de maintenance. Veuillez revenir plus tard.", "wrong_network_title": "Swaps non disponibles", "wrong_network_body": "Vous ne pouvez échanger des jetons que sur le réseau principal d’Ethereum.", "unallowed_asset_title": "Impossible d’échanger ce jeton", @@ -5160,7 +5190,7 @@ "not_enough": "Pas assez de {{symbol}} pour effectuer ce swap", "max_slippage": "Effet de glissement maximal", "max_slippage_amount": "Glissement maximal {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "Si le taux fluctue entre le passage de votre ordre et sa confirmation, on parle alors d’un « effet de glissement » (slippage). Votre swap sera automatiquement annulé si ce phénomène dépasse votre paramètre de « slippage maximal ».", "slippage_warning": "Il est important que vous sachiez ce que vous faites !", "allows_up_to_decimals": "{{symbol}} permet jusqu’à {{decimals}} décimales", "get_quotes": "Obtenir les cotations", @@ -5199,7 +5229,7 @@ "edit": "Modifier", "quotes_include_fee": "Les cotations comprennent une commission de {{fee}} % de MetaMask", "quotes_include_gas_and_metamask_fee": "Les cotations comprennent les frais de gaz et une commission de {{fee}} % de MetaMask", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "Appuyez pour échanger", "swipe_to_swap": "Faites glisser pour échanger", "swipe_to": "Faites glisser pour", "swap": "Échanger", @@ -5259,7 +5289,7 @@ "approve": "Approuver le swap de {{sourceToken}} : jusqu’à {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "Échange en attente ({{sourceToken}} contre {{destinationToken}})", "swap_confirmed": "Swap de {{sourceToken}} contre {{destinationToken}} effectué", "approve_pending": "Approuver le swap de {{sourceToken}}", "approve_confirmed": "Swap de {{sourceToken}} approuvé" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "Le menu déroulant des réseaux a été déplacé vers vos actifs", "description_2": "Échangez et établissez une passerelle en une seule étape simple", - "description_3": "Streamlined send experience", + "description_3": "Expérience d’envoi simplifiée", "description_4": "Une nouvelle mise en page des comptes" }, "more_information": "Vous pouvez désormais vous concentrer sur vos jetons et votre activité, et non sur les réseaux qui les sous-tendent.", @@ -5406,21 +5436,21 @@ "aggressive_label": "Agressif", "aggressive_text": "Probabilité élevée, même sur des marchés volatils. Utilisez l’option « Agressif » pour réaliser l’opération même lorsqu’il y a une augmentation subite du trafic réseau due à un événement majeur comme le parachutage de NFT populaires.", "market_label": "Marché", - "market_text": "Use market for fast processing at current market price.", + "market_text": "Utilisez l’option « Marché » pour une exécution rapide des ordres au prix actuel du marché.", "low_label": "Faible", "low_text": "Sélectionnez « Bas » si vous voulez attendre que le prix baisse. Les estimations de temps sont beaucoup moins précises, car les prix sont quelque peu imprévisibles.", "link": "En savoir plus sur la personnalisation des frais de gaz." }, "save": "Enregistrer", "submit": "Soumettre", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "Les frais de priorité maximums sont faibles par rapport aux conditions actuelles du réseau", + "max_priority_fee_high": "Les frais de priorité maximums sont plus élevés que nécessaire", + "max_priority_fee_speed_up_low": "Les frais de priorité maximums doivent être d’au moins {{speed_up_floor_value}} GWEI (10 % de plus que la transaction initiale)", + "max_priority_fee_cancel_low": "Les frais de priorité maximums doivent être d’au moins {{cancel_value}} GWEI (50 % de plus que la transaction initiale)", + "max_fee_low": "Les frais maximums sont faibles par rapport aux conditions actuelles du réseau", + "max_fee_high": "Les frais maximums sont plus élevés que nécessaire", + "max_fee_speed_up_low": "Les frais maximums doivent être d’au moins {{speed_up_floor_value}} GWEI (10 % de plus que la transaction initiale)", + "max_fee_cancel_low": "Les frais maximums doivent être d’au moins {{cancel_value}} GWEI (50 % de plus que la transaction initiale)", "learn_more_gas_limit": "La limite de gaz correspond au maximum d’unités de gaz que vous consentez à utiliser. Les unités de gaz servent de multiplicateur aux « Frais de priorité maximums » et aux « Frais maximums ».", "learn_more_max_priority_fee": "Les frais de priorité maximums (aussi appelés « pourboire du mineur ») vont directement aux mineurs et les incitent à accorder la priorité à votre transaction. Vous paierez le plus souvent votre réglage maximal.", "learn_more_max_fee": "Les frais maximums correspondent au montant le plus élevé que vous aurez à payer (frais de base + frais de priorité).", @@ -5530,10 +5560,10 @@ "enable_remember_me_description": "Lorsque l'option « Se souvenir de moi » est activée, toute personne ayant accès à votre téléphone peut accéder à votre compte MetaMask." }, "turn_off_remember_me": { - "title": "Saisissez votre mot de passe pour désactiver l'option « Se souvenir de moi »", - "placeholder": "Mot de passe", - "description": "Si vous désactivez cette option, la saisie du mot de passe sera requise pour déverrouiller MetaMask.", - "action": "Désactiver l'option « Se souvenir de moi »" + "title": "Désactiver l’option « Se souvenir de moi »", + "placeholder": "Confirmer le mot de passe", + "description": "Une fois désactivée, la fonction « Se souvenir de moi » ne peut plus être utilisée. Cette fonctionnalité a été supprimée, vous pouvez donc déverrouiller MetaMask à l’aide de votre mot de passe ou de l’authentification biométrique.", + "action": "Désactiver l’option « Se souvenir de moi »" }, "dapp_connect": { "warning": "Veuillez mettre à jour l’application pour utiliser cette fonctionnalité" @@ -5582,7 +5612,7 @@ "learn_more": "En savoir plus" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "Vérifier les coordonnées du tiers", "protect_from_scams": "Pour vous protéger des escrocs, prenez le temps de vérifier les coordonnées du tiers.", "learn_to_verify": "En savoir plus sur la vérification des coordonnées d'un tiers", "spending_cap": "plafond des dépenses", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "Une restauration est nécessaire", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "Quelque chose a mal tourné. Mais ne vous inquiétez pas, nous allons essayer de restaurer votre portefeuille.", "restore_needed_action": "Restaurer le portefeuille" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Échec de la fermeture de l’application en cours sur votre appareil Ledger.", "ethereum_app_not_installed": "L’application Ethereum n’est pas installée.", "ethereum_app_not_installed_error": "Veuillez installer l’application Ethereum sur votre appareil Ledger.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "L’application Ethereum n’est pas ouverte", + "eth_app_not_open_message": "Veuillez ouvrir l’application Ethereum sur votre appareil Ledger.", "ledger_is_locked": "Ledger est verrouillé", "unlock_ledger_message": "Veuillez débloquer votre appareil Ledger.", "cannot_get_account": "Impossible de récupérer le compte", @@ -5797,8 +5827,8 @@ "error_description": "L’installation de {{snap}} a échoué." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "Un bonus annuel que vous pouvez réclamer quotidiennement depuis votre portefeuille.", + "earn_a_percentage_bonus": "Obtenez un bonus de {{percentage}} %", "claimable_bonus": "Bonus réclamable", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "Le délai nécessaire pour retirer votre jeton du protocole et le replacer dans votre portefeuille", "receive": "Ce jeton permet de suivre l’évolution de vos actifs et de vos récompenses. Ne le transférez pas et ne l’échangez pas, sinon vous ne pourrez pas retirer vos actifs.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "Votre facteur de santé financière mesure le risque de liquidation", "above_two_dot_zero": "Au-dessus de 2", "safe_position": "Position sans risque", "between_one_dot_five_and_2_dot_zero": "Entre 1,5 et 2", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "Risque de liquidation modéré", "below_one_dot_five": "En dessous de 1,5", "higher_liquidation_risk": "Risque de liquidation élevé" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "Pourquoi je ne peux pas retirer la totalité de mon solde ?", "your_withdrawal_amount_may_be_limited_by": "Le montant que vous pouvez retirer peut être limité par", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "Pool de liquidités", "not_enough_funds_available_in_the_lending_pool_right_now": "Il n’y a pas suffisamment de fonds disponibles dans le pool de prêt pour le moment.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "Positions d’emprunt existantes", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "Un retrait pourrait exposer vos positions de prêt existantes à un risque de liquidation." } }, @@ -5998,11 +6028,11 @@ "earn_button": "Gagner" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "Dégagez un revenu en stakant des TRX", + "stake_any_amount": "Stakez n’importe quelle quantité de TRX.", "earn_trx_rewards": "Obtenez des TRX en récompense.", "earn_trx_rewards_description": "Commencez à dégager des bénéfices dès que vous stakez. Les récompenses s’accumulent automatiquement.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "Déstakez à tout moment. Les demandes sont généralement traitées dans les 14 jours." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "Frais de gaz estimés", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "Les frais de gaz sont payés aux mineurs de cryptomonnaies qui traitent les transactions sur le réseau Ethereum. MetaMask ne tire aucun profit des frais de gaz.", "gas_fluctuation": "Les frais de gaz sont estimés et fluctueront selon le flux du trafic réseau et la complexité de la transaction.", "gas_learn_more": "En savoir plus sur les frais de gaz" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "Se connecter avec", "spender": "Dépenseur", "now": "Maintenant", - "switching_to": "Switching to", + "switching_to": "Passage à", "bridge_estimated_time": "Délai estimé", "pay_with": "Payer avec", - "receive_as": "Receive", + "receive_as": "Recevoir", "total": "Total", - "you_receive": "You'll receive", + "you_receive": "Vous recevrez", "transaction_fee": "Frais de transaction", - "transaction_fees": "Transaction fees", + "transaction_fees": "Frais de transaction", "metamask_fee": "Commission MetaMask", "network_fee": "Frais de réseau", "bridge_fee": "Frais du fournisseur de passerelles" @@ -6234,7 +6264,7 @@ "transaction_fee": "Nous échangerons vos jetons contre des USDC.e sur Polygon, le réseau utilisé par « Prédictions ». Les fournisseurs de services d’échange peuvent facturer des frais, mais MetaMask ne le fera pas." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMask échangera vos jetons pour vous. Aucuns frais MetaMask ne s’appliquent lorsque vous effectuez un échange contre des MUSD." }, "musd_conversion": { "transaction_fee": "Les frais de conversion en mUSD comprennent les coûts de réseau et peuvent inclure les frais du fournisseur." @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "Ce site demande votre signature", "transaction_tooltip": "Ce site demande votre transaction", "details": "Détails", - "qr_get_sign": "Get signature", + "qr_get_sign": "Obtenir la signature", "qr_scan_text": "Scannez avec votre portefeuille matériel", "sign_with_ledger": "Signer avec Ledger", "smart_account": "Compte intelligent", "smart_contract": "Contrat intelligent", - "standard_account": "Standard account", + "standard_account": "Compte standard", "siwe_message": { "url": "URL", "network": "Réseau", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "Compte intelligent", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "Compte standard", "switch": "Changer", "switchBack": "Rétablir", "includes_transaction": "Inclut {{transactionCount}} transactions", @@ -6307,9 +6337,9 @@ "cancel": "Annuler", "description": "Saisissez le montant que vous autorisez à dépenser en votre nom.", "invalid_number_error": "Le plafond de dépenses doit être un nombre", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "Le champ « Plafond de dépenses » est obligatoire", + "no_extra_decimals_error": "Le plafond de dépenses ne peut pas comporter plus de décimales que le jeton", + "no_zero_error": "Le plafond de dépenses doit être supérieur à 0", "no_zero_error_decrease_allowance": "Un plafond de dépenses de 0 n’a aucun effet sur la méthode « decreaseAllowance »", "no_zero_error_increase_allowance": "Un plafond de dépenses de 0 n’a aucun effet sur la méthode « increaseAllowance »", "save": "Sauvegarder", @@ -6336,7 +6366,7 @@ "transferRequest": "Demande de transfert", "nested_transaction_heading": "Transaction {{index}}", "transaction": "Protection des", - "available_balance": "Available balance: ", + "available_balance": "Solde disponible: ", "edit_amount_done": "Continuer", "deposit_edit_amount_done": "Ajouter des fonds", "deposit_edit_amount_predict_withdraw": "Retirer", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "Conditions générales", "select_token": "Sélectionnez un jeton", "no_tokens_found": "Aucun jeton trouvé", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "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", "all_networks": "Tous les réseaux", "num_networks": "{{numNetworks}} réseaux", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "Désélectionner tout", "see_all": "Tout afficher", "all": "Tous", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}} autres", "apply": "Appliquer", "slippage": "Slippage/effet de glissement", "slippage_info": "Si le prix fluctue entre le moment où vous passez un ordre et le moment où il est confirmé, on parle alors d’un « effet de glissement » (slippage). Votre swap sera automatiquement annulé si le slippage dépasse le seuil de tolérance que vous avez défini ici.", @@ -6392,7 +6422,7 @@ "quote_info_title": "Taux", "network_fee_info_title": "Frais de réseau", "network_fee_info_content": "Les frais de réseau dépendent du trafic sur le réseau et de la complexité de votre transaction.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "Ces frais de réseau sont pris en charge par MetaMask, vous pouvez donc effectuer des transactions même si vous n’avez pas de {{nativeToken}} sur votre compte.", "points": "Points estimés", "points_tooltip": "Points", "points_tooltip_content_1": "Les points vous permettent de gagner des récompenses MetaMask lorsque vous effectuez des transactions, par exemple lorsque vous échangez, transférez ou tradez des contrats à terme.", @@ -6406,7 +6436,7 @@ "select_recipient": "Sélectionner le destinataire", "external_account": "Compte externe", "error_banner_description": "Cette voie d’échange n’est pas disponible pour le moment. Essayez de modifier le montant, le réseau ou le jeton, et nous trouverons la meilleure option.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "Cette voie d’échange n’est pas disponible pour le moment. Essayez de modifier le montant, le réseau ou le jeton, et nous trouverons la meilleure option.\n\nVeuillez noter que si vous essayez d’échanger des actions tokenisées Ondo, vous pouvez être soumis à des restrictions géographiques imposées par les États-Unis, l’Union européenne, le Royaume-Uni, le Brésil ou d’autres pays.", "insufficient_funds": "Fonds insuffisants", "insufficient_gas": "Gaz insuffisant", "select_amount": "Sélectionnez le montant", @@ -6417,9 +6447,9 @@ "title": "Passerelle", "submitting_transaction": "En train d’envoyer", "fetching_quote": "Récupération de la cotation", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "Comprend {{feePercentage}} % de frais MetaMask.", "no_mm_fee": "Pas de frais MM", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "Aucuns frais MetaMask ne seront appliqués si vous échangez des jetons contre des {{destTokenSymbol}}.", "hardware_wallet_not_supported": "Les portefeuilles matériels ne sont pas encore pris en charge. Utilisez un portefeuille connecté pour continuer.", "hardware_wallet_not_supported_solana": "Les portefeuilles matériels ne sont pas encore pris en charge pour Solana. Utilisez un portefeuille connecté pour continuer.", "price_impact_info_title": "Impact sur les prix", @@ -6432,17 +6462,24 @@ "approval_needed": "Approuve le jeton pour l’échange.", "approval_tooltip_title": "Accorder un accès précis", "approval_tooltip_content": "Vous autorisez l’accès au montant spécifié ({{amount}} {{symbol}}). Le contrat n’accédera à aucun fonds supplémentaire.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "Montant minimum reçu", + "minimum_received_tooltip_title": "Montant minimum reçu", "minimum_received_tooltip_content": "Le montant minimum que vous recevrez si le prix change pendant le traitement de votre transaction, en fonction de votre tolérance au slippage. Il s’agit d’une estimation fournie par nos fournisseurs de liquidité. Le montant final peut différer.", + "market_closed": { + "title": "Le marché est fermé", + "description": "Le marché sur lequel repose ce jeton est actuellement fermé. Les jetons peuvent toutefois être transférés sur la blockchain à tout moment.", + "learn_more": "En savoir plus", + "learn_more_url": "https://status.ondo.finance/market", + "done": "Terminé" + }, "submit": "Soumettre", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "Votre transaction n’aboutira pas si la variation de prix dépasse le pourcentage de slippage.", "cancel": "Annuler", "confirm": "Confirmer", "exceeding_upper_slippage_warning": "Slippage élevé, cela peut entraîner un swap défavorable", "exceeding_lower_slippage_warning": "Slippage faible, cela peut entraîner un swap défavorable", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "Saisissez une valeur supérieure à {{value}} %", + "exceeding_upper_slippage_error": "Vous ne pouvez pas saisir une valeur supérieure à {{value}} %", "custom": "Personnalisé" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "Récupération du portefeuille", "login_with_social": "Connectez-vous avec vos comptes sociaux", "setup": "Configurer", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "Phrase secrète de récupération {{num}}", "back_up": "Sauvegarder", "reveal": "Révéler", "social_recovery_title": "RÉCUPÉRATION {{authConnection}}", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "Saisissez votre mot de passe", "description": "Saisissez le mot de passe de votre portefeuille pour afficher les détails de la carte.", + "description_unfreeze": "Saisissez le mot de passe de votre portefeuille pour réactiver les paiements avec votre carte.", "placeholder": "Mot de passe", "confirm": "Confirmer", "cancel": "Annuler", @@ -7001,6 +7039,7 @@ "enable_card_error": "Impossible d’activer la carte. Veuillez réessayer plus tard.", "view_card_details_error": "Impossible de charger les détails de la carte. Veuillez réessayer.", "biometric_verification_required": "Authentification requise pour afficher les détails de la carte.", + "unfreeze_auth_required": "Authentification requise pour réactiver les paiements avec votre carte.", "warnings": { "close_spending_limit": { "title": "Vous avez presque atteint votre limite de dépenses", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "Votre carte est bloquée", - "description": "Veuillez contacter le service d’assistance pour débloquer votre carte" + "description": "Votre carte est temporairement bloquée. Vous pouvez la débloquer à tout moment." }, "blocked": { "title": "Votre carte est bloquée", @@ -7068,7 +7107,14 @@ "travel_description": "Réservez des hôtels avec jusqu’à 70 % de réduction", "card_tos_title": "Conditions générales", "order_metal_card": "Carte Metal", - "order_metal_card_description": "Commandez votre Carte Metal physique dès maintenant" + "order_metal_card_description": "Commandez votre Carte Metal physique dès maintenant", + "freeze_card": "Bloquer la carte", + "unfreeze_card": "Débloquer la carte", + "freeze_card_description": "Suspendre tous les paiements avec votre carte", + "unfreeze_card_description": "Réactiver tous les paiements avec votre carte", + "freeze_error": "Échec de la mise à jour du statut de la carte. Veuillez réessayer.", + "freeze_success": "Carte bloquée avec succès", + "unfreeze_success": "Carte débloquée avec succès" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "La possibilité d’envoyer à nouveau le code ne sera disponible que dans {{seconds}} secondes" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "Ajouter à {{walletName}}", + "adding_to_wallet": "Ajout à {{walletName}} en cours…", + "continue_setup": "Continuer la configuration de {{walletName}}", + "wallet_not_available": "{{walletName}} non disponible", + "already_in_wallet": "Déjà dans {{walletName}}", + "success_title": "La carte a été ajoutée !", + "success_message": "Votre carte MetaMask Card a été ajoutée à {{walletName}}.", + "error_title": "Impossible d’ajouter la carte", + "error_wallet_not_available": "{{walletName}} n’est pas disponible sur cet appareil. Veuillez vous assurer que le portefeuille {{walletName}} a bien été configuré.", + "error_wallet_not_initialized": "{{walletName}} n’a pas été initialisé. Veuillez configurer votre portefeuille et réessayer.", "error_card_already_in_wallet": "Cette carte a déjà été ajoutée à {{walletName}}.", "error_card_pending": "Votre carte est en train d’être configurée dans {{walletName}}. Veuillez revenir dans quelques minutes.", "error_card_suspended": "Votre carte enregistrée dans {{walletName}} a été suspendue. Veuillez contacter le service d’assistance pour obtenir de l’aide.", "error_card_not_eligible": "Cette carte ne peut pas être utilisée pour l’approvisionnement d’un portefeuille mobile.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "Échec du chiffrement des données de la carte. Veuillez réessayer.", "error_invalid_card_data": "Données de carte non valides. Veuillez vérifier les détails de votre carte et réessayer.", "error_card_not_found": "Carte introuvable. Veuillez réessayer.", "error_card_provider_not_found": "Le fournisseur de la carte n’est pas disponible dans votre région.", "error_card_id_mismatch": "Échec de la vérification de la carte. Veuillez réessayer.", "error_card_not_active": "Votre carte n’a pas encore été activée. Veuillez d’abord activer votre carte.", "error_network": "Une erreur réseau s’est produite. Veuillez vérifier votre connexion et réessayer.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "La demande a expiré. Veuillez réessayer.", + "error_server": "Une erreur de serveur s’est produite. Veuillez réessayer plus tard.", + "error_unknown": "Une erreur inattendue s’est produite. Veuillez réessayer ou contacter le service d’assistance.", + "error_platform_not_supported": "Cette plateforme ne prend pas en charge l’ajout de cartes aux portefeuilles mobiles.", "try_again": "Réessayez", "cancel": "Annuler" } @@ -7353,7 +7399,7 @@ "verifying_rewards": "Nous nous assurons que tout est correct avant que vous ne réclamiez vos récompenses." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "Points gagnés" }, "onboarding": { "not_supported_region_title": "Région non prise en charge", @@ -7431,7 +7477,7 @@ "show_less": "Afficher moins", "linking_progress": "Ajout des comptes… ({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}} inscrit(s)", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "Ajouter tous les comptes" }, "referred_by_code": { "title": "Code de parrainage", @@ -7514,7 +7560,7 @@ "claim_label": "Réclamer", "claimed_label": "Réclamé", "reward_claimed": "Récompense réclamée", - "time_left": "{{time}} left", + "time_left": "Il reste {{time}}", "expired": "Expiré" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "L’échange a échoué", "redeem_failure_description": "Veuillez réessayer plus tard.", "reward_details": "Détails de la récompense", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "Sélectionnez le compte vers lequel envoyer cette récompense." }, "animation": { "could_not_load": "Impossible de charger" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", - "pill_calculating": "Calculating", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "starts_date": "Débute le {{date}}", + "ends_date": "Se termine le {{date}}", + "results_coming_soon": "Résultats bientôt disponibles", + "tokens_on_the_way": "Jetons en cours d’envoi", + "pill_up_next": "À venir", + "pill_live_now": "Disponible dès maintenant", + "pill_calculating": "Calcul en cours", + "pill_results_ready": "Résultats disponibles", + "pill_complete": "Terminé" }, "snapshots_section": { "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "error_title": "Impossible de charger les snapshots", + "error_description": "Impossible de charger les snapshots. Veuillez réessayer.", "retry_button": "Réessayer" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "Actif", + "upcoming_title": "À venir", + "previous_title": "Précédent", + "empty_state": "Aucun snapshot disponible", + "error_title": "Impossible de charger les snapshots", + "error_description": "Impossible de charger les snapshots. Veuillez réessayer.", "retry_button": "Réessayer", - "refreshing": "Refreshing..." + "refreshing": "Actualisation en cours…" } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "Approuver {{approveSymbol}}", "bridge_approval_loading": "Approuver", "bridge_send": "Transférer des {{sourceSymbol}} depuis {{sourceChain}}", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "Envoi via une passerelle", "bridge_receive": "Recevez des {{targetSymbol}} sur {{targetChain}}", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "Réception via une passerelle", "default": "Transaction", "musd_convert_send": "{{sourceSymbol}} envoyé depuis {{sourceChain}}", "musd_claim": "Réclamer mes mUSD", @@ -7607,20 +7653,20 @@ "description": "Connexion à {{dappName}}…" }, "show_error": { - "title": "Connection error", + "title": "Erreur de connexion", "description": "La connexion a échoué. Veuillez réessayer." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "Approbation rejetée", + "description": "L’utilisateur a rejeté la demande." }, "show_return_to_app": { "title": "Réussite", "description": "Revenez à l’application pour continuer." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "Connexion introuvable", + "description": "Veuillez établir une nouvelle connexion depuis l’application pour continuer." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "Explorer", + "trending_tokens": "Jetons tendance", "price_change": "Variation du prix", "all_networks": "Tous les réseaux", - "24h": "24h", + "24h": "24 h", "time": "Temps", "24_hours": "24 heures", "6_hours": "6 heures", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 heure", + "5_minutes": "5 minutes", "networks": "Réseaux", "sort_by": "Trier par", "volume": "Volume", @@ -7650,32 +7696,48 @@ "high_to_low": "Du plus élevé au plus bas", "low_to_high": "Du plus bas au plus élevé", "apply": "Appliquer", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "Rechercher des jetons, des sites, des URL", "cancel": "Annuler", "perps": "Perps", "predictions": "Prédictions", - "no_results": "No results found", + "no_results": "Aucun résultat trouvé", "sites": "Sites", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "Sites populaires", + "search_sites": "Rechercher des sites", + "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.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "La section « Jetons tendance » n’est pas disponible", + "description": "Nous ne pouvons pas afficher cette page pour le moment", "try_again": "Réessayez" }, "empty_search_result_state": { "title": "Aucun jeton trouvé", - "description": "We were not able to find this token" + "description": "Nous n’avons pas trouvé ce jeton" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "La mise à jour prête", + "description_ios": "Nous avons apporté quelques corrections importantes à l’application. Rechargez l’application pour utiliser la dernière version de MetaMask.", + "description_android": "Nous avons apporté quelques corrections importantes à l’application. Fermez et rouvrez MetaMask pour appliquer la mise à jour.", "primary_action_reload": "Recharger", "primary_action_acknowledge": "J’ai compris" + }, + "homepage": { + "sections": { + "tokens": "Jetons", + "perpetuals": "Contrats perpétuels", + "predictions": "Prédictions", + "defi": "DeFi", + "nfts": "NFT", + "import_nfts": "Importer des NFT", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/hi.json b/locales/languages/hi.json index 6bb633c9f69..46be99d97fd 100644 --- a/locales/languages/hi.json +++ b/locales/languages/hi.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "डिवाइस", "reset_wallet_desc_2": "पर MetaMask से आपका वॉलेट डेटा हमेशा के लिए मिटा दिया जाएगा। इसे वापस नहीं लाया जा सकता।", "reset_wallet_desc_login": "अपने वॉलेट को रिस्टोर करने के लिए, आप अपने सीक्रेट रिकवरी फ्रेज़, या अपने Google या Apple अकाउंट के पासवर्ड का उपयोग कर सकते हैं। MetaMask के पास यह जानकारी नहीं है।", - "reset_wallet_desc_srp": "अपने वॉलेट को रिस्टोर करने के लिए, सुनिश्चित करें कि आपके पास अपना सीक्रेट रिकवरी फ्रेज़ है। MetaMask के पास यह जानकारी नहीं है।" + "reset_wallet_desc_srp": "अपने वॉलेट को रिस्टोर करने के लिए, सुनिश्चित करें कि आपके पास अपना सीक्रेट रिकवरी फ्रेज़ है। MetaMask के पास यह जानकारी नहीं है।", + "biometric_authentication_cancelled": "बायोमेट्रिक ऑथेंटिकेशन कैंसिल हो गया", + "biometric_authentication_cancelled_title": "बायोमेट्रिक सेटअप नहीं हो पाया", + "biometric_authentication_cancelled_description": "कृपया सेटिंग्स से बायोमेट्रिक ऑथेंटिकेशन को री-सेटअप करें।", + "biometric_authentication_cancelled_button": "कन्फर्म करें" }, "connect_hardware": { "title_select_hardware": "किसी hardware wallet को कनेक्ट करें", @@ -1040,7 +1044,7 @@ "title": "डिपॉज़िट करने के लिए राशि", "get_usdc_hyperliquid": "USDC प्राप्त करें • हाइपरलिक्विड", "insufficient_funds": "अपर्याप्त फंड", - "no_funds_available": "कोई फंड उपलब्ध नहीं है। कृपया पहले डिपॉज़िट करें।", + "no_funds_available": "पर्याप्त फंड उपलब्ध नहीं हैं। फंड जमा करें या पेमेंट का कोई दूसरा तरीका चुनें", "enter_amount": "रकम डालें", "fetching_quote": "कोटेशन लाया जा रहा है", "submitting": "ट्रांसेक्शन सबमिट किया जा रहा है", @@ -1970,8 +1974,8 @@ "trade_again": "फिर से ट्रेड करें", "activity": { "deposit_title": "डिपॉज़िट करें", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "{{amount}} {{symbol}} डिपॉज़िट किया गया", + "withdrew_amount": "{{amount}} {{symbol}} निकाला गया", "status_completed": "पूरा किया गया", "status_failed": "नहीं हो पाया", "status_pending": "अभी पूरा नहीं हुआ" @@ -2051,6 +2055,16 @@ "referral_code_text": "एक्स्ट्रा रिवॉर्ड कमाने के लिए मेरे रेफ़रल कोड का उपयोग करें।" } }, + "market_insights": { + "title": "मार्केट इनसाइट्स", + "updated_ago": "{{time}} अपडेट किया गया", + "disclaimer": "AI इनसाइट्स। फाइनेंशियल सलाह नहीं।", + "whats_driving_price": "कीमत किस वजह से बढ़ रही है?", + "what_people_saying": "लोग क्या कह रहे हैं", + "trade_button": "ट्रेड करें", + "sources_count": "+{{count}} सोर्स", + "sources_title": "सोर्स" + }, "predict": { "title": "MetaMask प्रिडिक्शन्स", "prediction_markets": "प्रेडिक्शन मार्केट", @@ -2384,8 +2398,8 @@ "no_available_tokens": "अपना टोकन नहीं देखें?", "add_tokens": "टोकन इम्पोर्ट करें", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "इस खाते में {{tokenCount}} नया {{tokensLabel}} मिला", "token_toast": { "tokens_imported_title": "इंपोर्ट किए गए टोकन", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "टोकन का दशमलव खाली नहीं रह सकता।", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "हम उस नाम के साथ कोई टोकन नहीं ढ़ूंढ़ सके।", + "tokens_empty_description": "Search for any token and import it", "select_token": "टोकन चुनें", "address_must_be_smart_contract": "व्यक्तिगत पता का पहचान हुई। टोकन अनुबंध पता दर्ज करें।", "billion_abbreviation": "B", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "सभी खाते डिस्कनेक्ट करें", "deceptive_site_ahead": "आगे धोखाधड़ी करने वाली साइट मौजूद है", "deceptive_site_desc": "आप जिस साइट पर जाना चाह रहे हैं वह सुरक्षित नहीं है। हमला करने वाले आपको कुछ खतरनाक काम करने के लिए उकसा सकते हैं।", + "malicious_site_detected": "बुरी नीयत वाली साइट का पता चला", + "malicious_site_warning": "अगर आप इस साइट से कनेक्ट होते हैं, तो आप अपने सभी एसेट खो सकते हैं।", + "connect_anyway": "तब भी कनेक्ट करें", "learn_more": "ज़्यादा जानें", "advisory_by": "Ethereum Phishing Detector और PhishFort द्वारा दी गई एडवाइज़री", "potential_threat": "बड़े ख़तरों में शामिल हैं", @@ -2846,7 +2864,11 @@ "permissions": "अनुमतियां", "card_title": "MetaMask कार्ड", "settings": "सेटिंग्स", - "log_out": "लॉग आउट करें" + "networks": "नेटवर्क", + "log_out": "लॉग आउट करें", + "notifications": "अपने वॉलेट का उपयोग करना", + "buy": "खरीदें", + "scan": "स्कैन करें" }, "app_settings": { "enabling_notifications": "नोटिफिकेशंस चालू किया जा रहा है...", @@ -2870,6 +2892,8 @@ "state_logs": "स्टेट लॉग्स", "add_network_title": "एक नेटवर्क जोड़ें", "auto_lock": "ऑटो-लॉक", + "enable_device_authentication": "डिवाइस ऑथेंटिकेशन चालू करें", + "enable_device_authentication_desc": "MetaMask को अनलॉक करने के लिए अपने डिवाइस के बायोमेट्रिक्स या पासकोड का इस्तेमाल करें।", "auto_lock_desc": "एप्लिकेशन के अपने आप लॉक होने में लगने वाले समय का चयन करें। ", "state_logs_desc": "किसी समस्या को डिबग करने में यह MetaMask की मदद करेगा, जिससे आपका मुकाबला हो सकता है। कृपया इसे हैमबर्गर आइकन > फीडबेक भेजें के जरिए MetaMask समर्थन को भेजें, या अपने वर्तमान टिकट का जवाब दें यदि आपके पास एक है।", "autolock_immediately": "तुरंत ही", @@ -2975,6 +2999,11 @@ "add_rpc_url": "RPC URL जोड़ें", "add_block_explorer_url": "ब्लॉक एक्सप्लोरर URL जोड़ें", "networks_desc": "कस्टम RPC नेटवर्क को जोड़ें और संपादित करें", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "नेटवर्क ढूंढें", + "networks_no_results": "कोई नेटवर्क नहीं मिला", "network_name_label": "नेटवर्क का नाम", "network_name_placeholder": "नेटवर्क का नाम (वैकल्पिक)", "network_rpc_url_label": "RPC URL", @@ -2991,7 +3020,16 @@ "network_other_networks": "अन्य नेटवर्क", "network_rpc_networks": "RPC नेटवर्क", "network_add_network": "नेटवर्क जोड़ें", + "add_chain_title": "एक नेटवर्क जोड़ें", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "फिर से प्रयास करें", + "add_chain_added": "Added", + "add_chain_or": "या", + "add_chain_custom_link": "एक कस्टम नेटवर्क जोड़ें", "network_add_custom_network": "एक कस्टम नेटवर्क जोड़ें", + "network_add_test_network": "Add a test network", "network_add": "जोड़ें", "network_save": "सहेजें", "remove_network_title": "क्या आप इस नेटवर्क को हटाना चाहते हैं?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "ठीक है", "title": "खाता कनेक्ट नहीं हो सका", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "MetaMask से फिर से कनेक्ट करने के लिए कृपया साइट पर क्यूआर कोड स्कैन करें" }, "app_information": { "title": "जानकारी", @@ -3379,6 +3417,7 @@ "sell_description": "कैश के लिए क्रिप्टो बेचें" }, "asset_overview": { + "market_closed": "मार्केट बंद", "send_button": "भेजें", "buy_button": "खरीदें", "cash_buy_button": "कैश में खरीदें", @@ -3399,19 +3438,6 @@ "bridge": "ब्रिज", "earn": "कमाएं", "convert_to_musd": "mUSD में कन्वर्ट करें", - "merkl_rewards": { - "annual_bonus": "{{apy}}% बोनस", - "claimable_bonus": "क्लेम करने योग्य बोनस", - "claimable_bonus_tooltip_description": "mUSD बोनस Linea पर क्लेम किए जाते हैं।", - "terms_apply": "नियम लागू।", - "ok": "ठीक है", - "claim": "क्लेम करें", - "processing_claim": "क्लेम प्रोसेस हो रहा है...", - "claim_on_linea_title": "Linea पर बोनस क्लेम करें", - "claim_on_linea_description": "आपका बोनस Linea पर जारी किया जाएगा, जो आपके Ethereum mUSD बैलेंस से अलग होगा।", - "continue": "जारी रखें", - "unexpected_error": "एक अनचाही गड़बड़ी हुई। कृपया फिर से कोशिश करें।" - }, "tron": { "daily_resource_new_energy": "नई दैनिक ऊर्जा", "sufficient_to_cover": "कवर करने के लिए पर्याप्त", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "टोकन पता क्लिपबोर्ड पर कॉपी किया गया" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "QR कोड ग़लत है", "invalid_qr_code_message": "QR कोड जिसे आप स्कैन करने की कोशिश कर रहे हैं मान्य नहीं है।", "allow_camera_dialog_title": "कैमरे की पहुंच की अनुमति दें", "allow_camera_dialog_message": "QR कोड स्कैन करने के लिए हमें आपकी अनुमति की जरुरत है", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "ऐसा लगता है जैसे कि एक्सटेंशन के साथ आप सिंक करने की कोशिश कर रहे हैं। ऐसा करने के लिए, आपको अपने वर्तमान वॉलेट को मिटाना होगा। \n\nएक बार जब आपने ऐप के नए प्रारुप को मिटा दिया या फिर से स्थापित कर दिया हो, तब \"MetaMask एक्सटेंशन के साथ सिंक करें\" विकल्प को चुनें। महत्वपूर्ण! अपने वॉलेट को मिटाने से पहले, ये सुनिश्चित कर लें कि आपने अपने सीक्रेट रिकवरी फ्रेज का बैकअप ले लिया है।", "not_allowed_error_title": "कैमरे की ऐक्सेस चालू करें", "not_allowed_error_desc": "QR कोड को स्कैन करने के लिए, आपको अपने डिवाइस के सेटिंग्स मेनू से MetaMask कैमरे की ऐक्सेस देनी होगी।", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "अनजाना QR कोड", "unrecognized_address_qr_code_desc": "क्षमा करें, यह QR कोड किसी खाते के पते या अनुबंध के पते से संबद्ध नहीं है।", "url_redirection_alert_title": "आप किसी बाहरी लिंक पर जाने वाले हैं", "url_redirection_alert_desc": "लिंक का उपयोग लोगों को धोखा देने या फिश करने के लिए किया जा सकता है, इसलिए सुनिश्चित करें कि केवल उन्हीं वेबसाइटों पर जाएं जिन पर आप भरोसा करते हैं।", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "आप इस संग्रहणीय वस्तु के मालिक नहीं हैं", "known_asset_contract": "ज्ञात संपत्ति अनुबंध पता", "max": "अधिकतम", - "recipient_address": "Recipient address", + "recipient_address": "प्राप्तकर्ता का पता", "required": "जरुरी", "to": "को", "total": "कुल", @@ -3641,7 +3667,7 @@ "nevermind": "कोई बात नहीं", "edit_network_fee": "गैस शुल्क संपादित करें", "edit_priority": "प्राथमिकता को संपादित करें", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "गैस कैंसिल करने का शुल्क", "gas_speedup_fee": "गैस की गति बढ़ाने का शुल्क", "use_max": "अधिकतम उपयोग करें", "set_gas": "निर्धारित करें", @@ -3650,7 +3676,7 @@ "transaction_fee": "गैस शुल्क", "transaction_fee_less": "कोई शुल्क नहीं", "total_amount": "कुल रकम", - "view_data": "View data", + "view_data": "डेटा देखें", "adjust_transaction_fee": "लेन-देन शुल्क को समायोजित करें", "could_not_resolve_ens": "ENS सुलझा नहीं सके", "asset": "संपत्ति", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "विकेन्द्रीकृत वेब ब्राउज करने के लिए, एक नया टैब जोड़ें", "got_it": "समझ गए", "max_tabs_title": "अधिकतम टैब की सीमा पूरी हो गई", - "max_tabs_desc": "हम वर्तमान में एक बार में केवल 5 ओपन टैब सपोर्ट करते हैं। कृपया नए टैब जोड़ने से पहले मौजूदा टैब बंद करें।", + "max_tabs_desc": "हम वर्तमान में एक बार में केवल 20 ओपन टैब सपोर्ट करते हैं। कृपया नए टैब जोड़ने से पहले मौजूदा टैब बंद करें।", "failed_to_resolve_ens_name": "हम उस ENS नाम को सुलझा नहीं सके", "remove_bookmark_title": "पसंदीदा हटाएं", "remove_bookmark_msg": "क्या आप वास्तव में इस साइट को अपने पसंदीदा से हटाना चाहते हैं?", @@ -3828,7 +3854,7 @@ "cancel_button": "रद्द करें" }, "approval": { - "title": "Confirm transaction" + "title": "ट्रांसेक्शन कन्फर्म करें" }, "approve": { "title": "स्वीकृति दें", @@ -3839,39 +3865,39 @@ "unavailable": "अनुपलब्ध", "tx_review_confirm": "पुष्टि करें", "tx_review_transfer": "स्थानांतरण", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "कॉन्ट्रैक्ट डिप्लॉयमेंट", + "tx_review_transfer_from": "यहाँ से ट्रांसफर करें", + "tx_review_unknown": "अज्ञात तरीका", "tx_review_approve": "स्वीकृति दें", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "भत्ता बढ़ाएं", + "tx_review_set_approval_for_all": "सभी के लिए एप्रूवल सेट करें", + "tx_review_staking_claim": "क्लेम पर दावा ठोकना", "tx_review_staking_deposit": "डिपॉज़िट स्टेक करना", "tx_review_staking_unstake": "अनस्टेक करें", "tx_review_lending_deposit": "डिपॉज़िट उधार दिया जा रहा है", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "प्राप्त उधार को निकालना", "tx_review_perps_deposit": "फंडेड पर्प्स", "tx_review_predict_deposit": "फ़ंड किए गए प्रेडिक्शन", "tx_review_predict_claim": "क्लेम किया गया जीत का ईनाम", "tx_review_predict_withdraw": "प्रेडिक्शन विदड्रॉ", "tx_review_musd_conversion": "mUSD कन्वर्शन", "claim": "क्लेम करें", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "ETH भेजा", + "self_sent_ether": "खुद को ETH भेजा", + "received_ether": "ETH मिला", "sent_dai": "DAI भेजे गए", "self_sent_dai": "खुद को DAI भेजें", "received_dai": "DAI प्राप्त किया", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "टोकन भेजे गए", + "received_tokens": "टोकन प्राप्त हुए", "ether": "ETH", "sent_unit": "{{unit}} भेजे गए", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "खुद को {{unit}} भेजे", "received_unit": "{{unit}} प्राप्त किया", "sent_collectible": "कलेक्टिबल भेजा गया", "received_collectible": "कलेक्टिबल प्राप्त किया गया", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "ETH भेजें", + "send_unit": "{{unit}} भेजें", "send_collectible": "कलेक्टिबल भेजें", "receive_collectible": "कलेक्टिबल प्राप्त करें", "sent": "भेजा गया", @@ -3881,17 +3907,17 @@ "send": "भेजें", "redeposit": "फिर से डिपॉज़िट करें", "interaction": "इंटरैक्शन", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "कॉन्ट्रैक्ट डिप्लॉयमेंट", + "to_contract": "नया कॉन्ट्रैक्ट", + "mint": "मिंट", "tx_details_free": "मुफ्त", "tx_details_not_available": "उपलब्ध नहीं है", "smart_contract_interaction": "स्मार्ट अनुबंध संपर्क", "swaps_transaction": "स्वैप का लेन-देन", "bridge_transaction": "ब्रिज", "approve": "स्वीकृति दें", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "भत्ता बढ़ाएं", + "set_approval_for_all": "सभी के लिए एप्रूवल सेट करें", "hash": "हैश", "from": "से", "to": "को", @@ -3899,15 +3925,15 @@ "amount": "रकम", "fee": { "transaction_fee_in_ether": "ट्रांसेक्शन फीस", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "ट्रांसेक्शन फीस (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "इस्तेमाल की गई गैस (यूनिट्स)", + "gas_limit": "गैस लिमिट (यूनिट्स)", + "gas_price": "गैस प्राइस (GWEI)", + "base_fee": "बेस फी (GWEI)", + "priority_fee": "प्रायोरिटी फी (GWEI)", "multichain_priority_fee": "प्रायोरिटी फी", - "max_fee": "Max fee per gas", + "max_fee": "प्रति गैस अधिकतम फीस", "total": "कुल", "view_on": "देखें", "view_on_etherscan": "Etherscan पर देखें", @@ -3923,13 +3949,13 @@ "nonce": "नॉन्स (nonce)", "from_device_label": "इस डिवाइस से", "import_wallet_row": "अकाउंट इस डिवाइस में जोड़ा गया", - "import_wallet_label": "Account added", + "import_wallet_label": "अकाउंट जोड़ा गया", "import_wallet_tip": "इस डिवाइस से भविष्य के सभी किए गए लेन-देन में टाइमस्टैम्प के बाद एक लेबल \"इस डिवाइस से\" शामिल होगा। अकाउंट को जोड़े जाने से पहले की तिथि के लेन-देन के लिए, इतिहास सूचित नहीं करेगा कि कौन से आउटगोइंग लेन-देन इस डिवाइस से उत्पन्न हुए हैं।", "sign_title_scan": "स्कैन करें ", "sign_title_device": "अपने हार्डवेयर वॉलेट के साथ", "sign_description_1": "अपने हार्डवेयर वॉलेट के साथ साइन करने के बाद,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "\"सिग्नेचर प्राप्त करें\" पर टैप करें", + "sign_get_signature": "सिग्नेचर प्राप्त करें", "transaction_id": "ट्रांसेक्शन आईडी", "network": "नेटवर्क", "request_from": "इनसे मिला अनुरोध", @@ -4032,7 +4058,7 @@ "title": "नेटवर्क", "other_networks": "अन्य नेटवर्क", "close": "बंद करें", - "status_ok": "All systems operational", + "status_ok": "सभी सिस्टम चालू हैं", "status_not_ok": "नेटवर्क में कुछ समस्या हो रही है", "want_to_add_network": "इस नेटवर्क को जोड़ना चाहते हैं?", "add_custom_network": "कस्टम नेटवर्क जोड़ें", @@ -4051,7 +4077,7 @@ "review": "समीक्षा करें", "view_details": "विवरण देखें", "network_details": "नेटवर्क का विवरण", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "कन्फर्म करें को चुनने पर नेटवर्क डिटेल्स की जांच चालू हो जाती है। आप इस में नेटवर्क डिटेल्स की जांच बंद कर सकते हैं ", "network_settings_security_privacy": "सेटिंग्स में > सुरक्षा और गोपनीयता", "network_currency_symbol": "करेंसी सिंबल", "network_block_explorer_url": "एक्सप्लोरर URL को ब्लॉक करें", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "एक दुर्भावनापूर्ण नेटवर्क प्रदाता ब्लॉकचेन की स्थिति के बारे में झूठ बोल सकता है और आपकी नेटवर्क गतिविधि को रिकॉर्ड कर सकता है। केवल ऐसे कस्टम नेटवर्क जोड़ें जिन पर आपको भरोसा हो।", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "नेटवर्क की जानकारी", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "अतिरिक्त नेटवर्क सूचना", "network_warning_desc": "यह नेटवर्क कनेक्शन थर्ड पार्टी पर निर्भर करता है। यह कनेक्शन कम विश्वसनीय हो सकता है या गतिविधि को ट्रैक करने के लिए तृतीय-पक्ष को सक्षम कर सकता है।", "additonial_network_information_desc": "इनमें से कुछ नेटवर्क थर्ड पार्टीज़ पर निर्भर हैं। कनेक्शन कम विश्वसनीय हो सकते हैं या गतिविधि को ट्रैक करने के लिए थर्ड पार्टीज़ को चालू कर सकते हैं।", "connect_more_networks": "अधिक नेटवर्क कनेक्ट करें", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "इस नेटवर्क को हटा दिया गया है", "network_deprecated_description": "जिस नेटवर्क से आप कनेक्ट करने का प्रयास कर रहे हैं उसे अब MetaMask सपोर्ट नहीं करता।", "edit_networks_title": "नेटवर्क बदलें", - "no_network_fee": "No network fee" + "no_network_fee": "कोई नेटवर्क फीस नहीं" }, "permissions": { "title_this_site_wants_to": "यह साइट निम्नलिखित करना चाहती है:", @@ -4111,11 +4137,11 @@ "network_connected": "नेटवर्क कनेक्ट किया गया ", "see_your_accounts": "आपके अकाउंट को देखकर ट्रांसेक्शन का सुझाव देना", "connected_to": "से जुड़ा हुआ ", - "manage_permissions": "Manage permissions", + "manage_permissions": "अनुमतियाँ प्रबंधित करें", "edit": "बदलें", "cancel": "कैंसिल करें", "got_it": "समझ गए", - "connection_details_title": "Connection details", + "connection_details_title": "कनेक्शन का विवरण", "connection_details_description": "आप {{connectionDateTime}} पर MetaMask ब्राउज़र का उपयोग करके इस साइट से कनेक्टेड हैं", "title_add_network_permission": "नेटवर्क अनुमति जोड़ें", "add_this_network": "इस नेटवर्क को जोड़ें", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "डिवाइस के PIN के साथ अनलॉक करें?" }, "authentication": { - "auth_prompt_title": "प्रमाणीकरण की जरुरत है", - "auth_prompt_desc": "MetaMask के उपयोग के लिए कृपया प्रमाणित करें", - "fingerprint_prompt_title": "प्रमाणीकरण की जरुरत है", - "fingerprint_prompt_desc": "MetaMask को अनलॉक करने के लिए अपनी फिंगरप्रिंट का उपयोग करें", - "fingerprint_prompt_cancel": "रद्द करें" + "auth_prompt_desc": "MetaMask के उपयोग के लिए कृपया प्रमाणित करें" }, "accountApproval": { "title": "कनेक्ट अनुरोध ", "walletconnect_title": "वॉलेटकनेक्ट अनुरोध", "action": "इस साइट से कनेक्ट करें?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "कनेक्शन फिर से शुरू करने के लिए, वह नंबर चुनें जो आप साइट पर देखते हैं", + "action_reconnect_deeplink": "क्या आप इस साइट से फिर से कनेक्ट करना चाहते हैं?", "connect": "कनेक्ट", "resume": "फिर से शुरू करें", "cancel": "रद्द करें", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "इस साइट कनेक्शन को याद न रखें", "disconnect": "डिसकनेक्ट करें", "permission": "देखें अपना ", "address": "पब्लिक एड्रेस", @@ -4218,7 +4240,7 @@ "error_title": "कुछ गलत हो गया", "error_message": "हम उस निजी की को इम्पोर्ट नहीं कर सके। कृपया सुनिश्चित करें कि आपने इसे सही ढ़ंग से दर्ज किया है।", "error_empty_message": "आपको अपनी निजी की दर्ज करने की जरुरत है।", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "या एक QR कोड स्कैन करें" }, "import_private_key_success": { "title": "अकाउंट सफलतापूर्वक इम्पोर्ट किया गया!", @@ -4228,19 +4250,19 @@ "title": "सीक्रेट रिकवरी फ्रेज़ इंपोर्ट करें", "import_wallet_title": "एक वॉलेट इंपोर्ट करें", "enter_srp_subtitle": "अपना सीक्रेट रिकवरी फ्रेज़ डालें", - "textarea_placeholder": "Add a space between each word and make sure no one is watching", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "textarea_placeholder": "प्रत्येक शब्द के बीच एक स्पेस जोड़ें और ध्यान दें कि कोई भी नहीं देख रहा हो", + "description": "अपने वॉलेट का सीक्रेट रिकवरी फ्रेज़ एंटर करें। आप कोई भी Ethereum, Solana या Bitcoin सीक्रेट रिकवरी फ्रेज़ इंपोर्ट कर सकते हैं।", + "subtitle": "अपने सीक्रेट रिकवरी फ्रेज़ को पेस्ट करें", "cta_text": "जारी रखें", "paste": "पेस्ट करें", "clear": "सभी हटाएं", "srp_number_of_words_option_title": "शब्दों की संख्या", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "मेरे पास 12 शब्दों का एक फ्रेज़ है", + "24_word_option": "मेरे पास 24 शब्दों का एक फ्रेज़ है", "error_title": "कुछ गलत हो गया", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "हम उस सीक्रेट रिकवरी फ्रेज़ को इंपोर्ट नहीं कर सके। कृपया सुनिश्चित करें कि आपने इसे सही ढंग से एंटर किया है।", + "error_empty_message": "आपको अपना सीक्रेट रिकवरी फ्रेज़ एंटर करना होगा।", + "error_number_of_words_error_message": "सीक्रेट रिकवरी फ्रेज़ में 12, या 24 शब्द होते हैं", "error_srp_is_case_sensitive": "ग़लत इनपुट! सीक्रेट रिकवरी फ्रेज़ केस सेंसिटिव है।", "error_srp_word_error_1": "शब्द ", "error_srp_word_error_2": " गलत है या इसकी स्पेलिंग गलत है।", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " गलत हैं या उनकी स्पेलिंग गलत है।", "error_invalid_srp": "अमान्य सीक्रेट रिकवरी फ्रेज", "error_duplicate_srp": "यह सीक्रेट रिकवरी फ्रेज़ पहले ही इंपोर्ट किया जा चुका है।", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "आप जिस अकाउंट को इंपोर्ट करने की कोशिश कर रहे हैं वह डुप्लीकेट है।", + "invalid_qr_code_title": "QR कोड ग़लत है", + "invalid_qr_code_message": "QR कोड में कोई सही सीक्रेट रिकवरी फ्रेज़ नहीं है", "success_1": "वॉलेट", "success_2": "इम्पोर्ट किया गया" }, @@ -4665,7 +4687,7 @@ "button": "वॉलेट को सुरक्षित रखें" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "ट्रांसेक्शन अपडेट नहीं हो पाया", "text": "क्या आप फिर से कोशिश करना चाहेंगे?", "cancel_button": "रद्द करें", "retry_button": "फिर से कोशिश करें" @@ -4684,13 +4706,13 @@ "next": "अगला", "amount_placeholder": "0.00", "link_copied": "क्लिपबोर्ड पर लिंक कॉपी किया गया", - "send_link_title": "Send link", + "send_link_title": "लिंक भेजें", "description_1": "आपका अनुरोध लिंक भेजे जाने के लिए तैयार है!", "description_2": "इस लिंक को अपने एक दोस्त को भेजें, और यह उन्हें भेजने के लिए कहेगा", "copy_to_clipboard": "क्लिपबोर्ड पर कॉपी करें", "qr_code": "QR कोड", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "लिंक भेजें", + "request_qr_code": "पेमेंट के लिए QR कोड का रिक्वेस्ट", "balance": "बैलेंस" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "आपके पास सक्रिय सत्र नहीं हैं", - "end_session_title": "End session", + "end_session_title": "सेशन समाप्त करें", "end": "समाप्त करें", "cancel": "रद्द करें", - "session_ended_title": "Session ended", + "session_ended_title": "सेशन समाप्त हुआ", "session_ended_desc": "चुन गए सत्र को समाप्त कर दिया गया", "session_already_exist": "यह सत्र पहले से ही कनेक्ट हुआ पड़ा है।", "close_current_session": "नया सत्र शुरू करने से पहले वर्तमान सत्र को बंद कर दें।" @@ -4765,15 +4787,14 @@ "on_network": "{{networkName}} पर", "debit_card": "डेबिट कार्ड", "select_payment_method": "भुगतान विधि चुनें", - "loading_quote": "Loading quote...", "pay_with": "के साथ भुगतान करें", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "{{providerName}} के ज़रिए खरीद रहे हैं।", + "change_provider": "प्रोवाइडर बदलें।", "payment_error": "कुछ गलत हो गया। कृपया फिर से कोशिश करें।", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "पेमेंट का कोई तरीका उपलब्ध नहीं है।", "error_fetching_quotes": "कुछ गलत हो गया। कृपया फिर से कोशिश करें।", - "no_quotes_available": "No providers available.", - "providers": "Providers", + "no_quotes_available": "कोई प्रोवाइडर उपलब्ध नहीं है।", + "providers": "प्रोवाइडर्स", "continue": "जारी रखें", "powered_by_provider": "{{provider}} द्वारा संचालित", "purchased_currency": "{{currency}} खरीदा गया", @@ -4871,6 +4892,15 @@ "log_out": "{{provider}} से लॉग आउट करें", "logged_out_success": "सफलतापूर्वक लॉग आउट किया गया", "logged_out_error": "लॉग आउट करते समय गड़बड़ी" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "सबसे कम विक्रय सीमा", "medium_sell_limit": "मध्यम विक्रय सीमा", "highest_sell_limit": "उच्चतम विक्रय सीमा", - "change": "Change", + "change": "बदलें", "continue_to_amount": "राशि जारी रखें", "no_payment_methods_title": "{{regionName}} में कोई पेमेंट का तरीका नहीं है", "no_cash_destinations_title": "{{regionName}} में कोई कैश डेस्टिनेशन नहीं है", @@ -5118,7 +5148,7 @@ "start_swapping": "स्वैप करना शुरु करें" }, "feature_off_title": "अस्थायी रुप से उपलब्ध नहीं", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "MetaMask स्वैप्स में रख-रखाव का काम चल रहा है। कृपया बाद में फिर से देखें।", "wrong_network_title": "स्वैप उपलब्ध नहीं है", "wrong_network_body": "Ethereum मुख्य नेटवर्क पर ही केवल आप टोकन स्वैप करने में सक्षम हैं।", "unallowed_asset_title": "इस टोकन को स्वैप नहीं कर सकते", @@ -5160,7 +5190,7 @@ "not_enough": "इस स्वैप को पूरा करने के लिए पर्याप्त {{symbol}} नहीं है", "max_slippage": "मैक्स स्लिपेज (slippage)", "max_slippage_amount": "अधिकतम गिरावट {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "अगर आपका ऑर्डर प्लेस होने और कन्फर्म होने के बीच रेट बदलता है तो इसे “स्लिपेज” कहते हैं। अगर स्लिपेज आपकी “मैक्स स्लिपेज” सेटिंग से ज़्यादा हो जाता है तो आपका स्वैप अपने आप कैंसिल हो जाएगा।", "slippage_warning": "सुनिश्चित करें कि आप जानते हैं कि आप क्या कर रहे हैं!", "allows_up_to_decimals": "{{symbol}} {{decimals}} दशमलव तक अनुमति देता है", "get_quotes": "उद्धरण पाएं", @@ -5199,7 +5229,7 @@ "edit": "संपादित करें", "quotes_include_fee": "उद्धरण में एक {{fee}}% MetaMask शुल्क शामिल है", "quotes_include_gas_and_metamask_fee": "कोटेशन में गैस और{{fee}}% MetaMask शुल्क शामिल है", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "स्वैप करने के लिए टैप करें", "swipe_to_swap": "स्वैप करने के लिए स्वाइप करें", "swipe_to": "स्वाइप करें", "swap": "स्वैप करें", @@ -5259,7 +5289,7 @@ "approve": "स्वैप के लिए {{sourceToken}} को स्वीकृति दें: {{upTo}} तक" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "स्वैप ({{sourceToken}} से {{destinationToken}}) अभी पूरा नहीं हुआ", "swap_confirmed": "{{sourceToken}} का स्वैप पूरा करें {{destinationToken}}) में", "approve_pending": "स्वैप के लिए {{sourceToken}} को स्वीकृति दें", "approve_confirmed": "स्वीकृति के लिए {{sourceToken}} स्वीकृत" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "नेटवर्क ड्रॉपडाउन आपके एसेट्स में स्थानांतरित कर दिया गया", "description_2": "एक सरल फ्लो में स्वैप और ब्रिज करें", - "description_3": "Streamlined send experience", + "description_3": "भेजने का आसान अनुभव", "description_4": "फ्रेश अकाउंट व्यू" }, "more_information": "अब आप नेटवर्क की बजाय अपने टोकन और गतिविधियों पर ध्यान केंद्रित कर सकते हैं।", @@ -5406,21 +5436,21 @@ "aggressive_label": "एग्रेसिव", "aggressive_text": "उच्च संभावना, अस्थिर बाजारों में भी। लोकप्रिय एनएफटी ड्रॉप्स जैसी चीजों के कारण नेटवर्क ट्रैफ़िक में वृद्धि को कवर करने के लिए आक्रामक बने।", "market_label": "मार्केट", - "market_text": "Use market for fast processing at current market price.", + "market_text": "मौजूदा मार्केट प्राइस पर तेज़ प्रोसेसिंग के लिए मार्केट का इस्तेमाल करें।", "low_label": "निम्न", "low_text": "सस्ती कीमत की प्रतीक्षा करने के लिए लो का उपयोग करें। चूंकि कीमतें कुछ हद तक अप्रत्याशित होती हैं, इसलिए समय के अनुमान बहुत ही कम सटीक होते हैं।", "link": "कस्टमाइज गैस के बारे में ज्यादा जानें।" }, "save": "सहेजें", "submit": "सबमिट करें", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "अभी के नेटवर्क कंडीशन के लिए मैक्स प्रायोरिटी फीस कम है", + "max_priority_fee_high": "मैक्स प्रायोरिटी फीस ज़रूरत से ज़्यादा है", + "max_priority_fee_speed_up_low": "मैक्स प्रायोरिटी फी कम से कम {{speed_up_floor_value}} GWEI (प्रारंभिक ट्रांसेक्शन से 10% ज़्यादा) होना चाहिए", + "max_priority_fee_cancel_low": "मैक्स प्रायोरिटी फी कम से कम {{cancel_value}} GWEI (प्रारंभिक ट्रांसेक्शन से 50% ज़्यादा) होना चाहिए", + "max_fee_low": "अभी के नेटवर्क कंडीशन के लिए मैक्स फीस कम है", + "max_fee_high": "मैक्स फीस ज़रूरत से ज़्यादा है", + "max_fee_speed_up_low": "मैक्स फीस कम से कम {{speed_up_floor_value}} GWEI (शुरुआती ट्रांसेक्शन से 10% ज़्यादा) होनी चाहिए", + "max_fee_cancel_low": "मैक्स फीस कम से कम {{cancel_value}} GWEI (शुरुआती ट्रांसेक्शन से 50% ज़्यादा) होनी चाहिए", "learn_more_gas_limit": "गैस की सीमा गैस की वह अधिकतम इकाई है जिसका उपयोग करने की आप इच्छा रखते हैं। गैस की इकाई “अधिकतम प्राथमिकता शुल्क”और “अधिकतम शुल्क” का एक गुणक होती हैं।", "learn_more_max_priority_fee": "अधिकतम प्राथमिकता शुल्क (उर्फ “खनिकों का टिप”) सीधे खनिकों के पास जाता है आपके लेन-देन को प्राथमिकता देने के लिए उन्हें प्रोत्साहित करता है। आप अक्सर अपनी अधिकतम सेटिंग का भुगतान करेंगे।", "learn_more_max_fee": "अधिकतम शुल्क आपके द्वारा किया जाने वाला अधिकतम भुगतान होता है (आधार शुल्क + प्राथमिकता शुल्क)।", @@ -5530,10 +5560,10 @@ "enable_remember_me_description": "जब रिमेंबर मी चालू रहेगा, आपका फोन एक्सेस करने वाला कोई भी व्यक्ति आपके MetaMask अकाउंट को एक्सेस कर सकता है।" }, "turn_off_remember_me": { - "title": "रिमेंबर मी को बंद करने के लिए अपना पासवर्ड दर्ज करें", - "placeholder": "पासवर्ड", - "description": "अगर आप इस ऑप्शन को बंद करते हैं, तो अब से आपको MetaMask अनलॉक करने के लिए अपने पासवर्ड की ज़रूरत होगी।", - "action": "रिमेंबर मी बंद करें" + "title": "Remember Me बंद करें", + "placeholder": "पासवर्ड कन्फर्म करें", + "description": "बंद करने के बाद, Remember Me का दोबारा इस्तेमाल नहीं किया जा सकता। यह फ़ीचर बंद कर दिया गया है, इसलिए आप इसके बजाय अपने पासवर्ड या बायोमेट्रिक्स से MetaMask अनलॉक कर सकते हैं।", + "action": "Remember Me बंद करें" }, "dapp_connect": { "warning": "इस फीचर का उपयोग करने के लिए, कृपया ऐप को नवीनतम संस्करण में अपडेट करें" @@ -5582,7 +5612,7 @@ "learn_more": "ज़्यादा जानें" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "थर्ड पार्टी के विवरण को सत्यापित करें", "protect_from_scams": "स्कैमर्स से खुद को बचाने के लिए, कुछ समय निकालकर थर्ड पार्टी के विवरण को सत्यापित करें।", "learn_to_verify": "थर्ड पार्टी के विवरण को सत्यापित करने का तरीका जानें", "spending_cap": "खर्च करने की सीमा", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "पुनर्स्थापित करने की आवश्यकता है", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "कुछ गड़बड़ हो गई है, लेकिन चिंता न करें! चलिए आपका वॉलेट रिस्टोर करने की कोशिश करते हैं।", "restore_needed_action": "वॉलेट पुनर्स्थापित करें" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "आपके Ledger डिवाइस पर चल रहे ऐप को बंद करने में विफल।", "ethereum_app_not_installed": "Ethereum ऐप इंस्टॉल नहीं है।", "ethereum_app_not_installed_error": "कृपया अपने Ledger डिवाइस पर Ethereum ऐप इंस्टॉल करें।", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "Ethereum ऐप खुला नहीं है", + "eth_app_not_open_message": "कृपया अपने Ledger डिवाइस पर Ethereum ऐप खोलें।", "ledger_is_locked": "Ledger लॉक है", "unlock_ledger_message": "कृपया अपने Ledger डिवाइस को अनलॉक करें", "cannot_get_account": "अकाउंट नहीं मिल रहा है", @@ -5797,8 +5827,8 @@ "error_description": "{{snap}} का इंस्टॉलेशन नहीं हो पाया।" }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "एक सालाना बोनस जिसे आप अपने वॉलेट से रोज़ाना क्लेम कर सकते हैं।", + "earn_a_percentage_bonus": "{{percentage}}% बोनस कमाएं", "claimable_bonus": "क्लेम करने योग्य बोनस", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "प्रोटोकॉल से अपना टोकन निकालने और उसे अपने वॉलेट में वापस लाने में लगने वाला समय", "receive": "इस टोकन का उपयोग आपकी एसेट्स और पुरस्कारों को ट्रैक करने के लिए किया जाता है। उन्हें ट्रांसफर या ट्रेड न करें, अन्यथा आप अपनी एसेट्स नहीं निकाल पाएंगे।", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "आपका हेल्थ फैक्टर लिक्विडेशन रिस्क को मापता है", "above_two_dot_zero": "2.0 से ऊपर", "safe_position": "सुरक्षित पोजीशन", "between_one_dot_five_and_2_dot_zero": "1.5-2.0 के बीच", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "मध्यम लिक्विडेशन रिस्क", "below_one_dot_five": "1.5 से नीचे", "higher_liquidation_risk": "उच्च लिक्विडेशन जोखिम" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "मैं अपना पूरा बैलेंस क्यों नहीं निकाल सकता?", "your_withdrawal_amount_may_be_limited_by": "आपकी निकासी राशि इससे सीमित हो सकती है", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "पूल लिक्विडिटी", "not_enough_funds_available_in_the_lending_pool_right_now": "अभी ऋण पूल में पर्याप्त फंड्स उपलब्ध नहीं है।", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "मौजूदा उधारी लेने की स्थितियाँ", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "निकालने से आपकी मौजूदा ऋण स्थितियाँ लिक्विडेशन के जोखिम में पड़ सकती हैं।" } }, @@ -5998,11 +6028,11 @@ "earn_button": "कमाएं" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "TRX स्टेक करें और कमाएं", + "stake_any_amount": "TRX की किसी भी राशि को स्टेक करें।", "earn_trx_rewards": "TRX पुरस्कार अर्जित करें।", "earn_trx_rewards_description": "स्टेक करते ही कमाई शुरू करें। पुरस्कार स्वचालित रूप से जुड़ते हैं।", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "कभी भी अनस्टेक करें। अनस्टेकिंग को प्रोसेस होने में 14 दिन का समय लगता है।" }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "अनुमानित गैस शुल्क", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "गैस फीस उन क्रिप्टो माइनर्स को दी जाती है जो Ethereum नेटवर्क पर ट्रांसेक्शन प्रोसेस करते हैं। MetaMask को गैस फीस से कोई प्रॉफिट नहीं होता है।", "gas_fluctuation": "गैस फीस का अनुमान लगाया जाता है और नेटवर्क ट्रैफिक और ट्रांसेक्शन की जटिलता के आधार पर इसमें उतार-चढ़ाव आएगा।", "gas_learn_more": "गैस फीस के बारे में और अधिक जानें" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "के साथ साइन इन किया जा रहा है", "spender": "खर्च करने वाला", "now": "अब", - "switching_to": "Switching to", + "switching_to": "यहां स्विच किया जा रहा है", "bridge_estimated_time": "अनुमानित समय", "pay_with": "के साथ भुगतान करें", - "receive_as": "Receive", + "receive_as": "प्राप्त करें", "total": "कुल", - "you_receive": "You'll receive", + "you_receive": "आपको प्राप्त होगा", "transaction_fee": "ट्रांसेक्शन फीस", - "transaction_fees": "Transaction fees", + "transaction_fees": "ट्रांसेक्शन फीस", "metamask_fee": "MetaMask फीस", "network_fee": "नेटवर्क फीस", "bridge_fee": "प्रदाता फीस ब्रिज करें" @@ -6234,7 +6264,7 @@ "transaction_fee": "हम आपके टोकन को Polygon पर USDC.e में स्वैप करेंगे, जो प्रिडिक्शन्स द्वारा इस्तेमाल होने वाला नेटवर्क है। स्वैप प्रदाता फीस ले सकते हैं, लेकिन MetaMask कोई फीस नहीं लेगा।" }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMask आपके लिए आपके पसंदीदा टोकन में स्वैप करेगा। जब आप MUSD में स्वैप करते हैं तो कोई MetaMask फीस लागू नहीं होती है।" }, "musd_conversion": { "transaction_fee": "mUSD कन्वर्शन फ़ीस में नेटवर्क कॉस्ट और प्रोवाइडर फ़ीस शामिल हो सकती है।" @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "यह साइट आपका सिग्नेचर मांग रही है", "transaction_tooltip": "यह साइट आपका ट्रांसेक्शन मांग रही है", "details": "विवरण", - "qr_get_sign": "Get signature", + "qr_get_sign": "सिग्नेचर प्राप्त करें", "qr_scan_text": "अपने हार्डवेयर वॉलेट के साथ स्कैन करें", "sign_with_ledger": "Ledger के साथ साइन करें", "smart_account": "स्मार्ट अकाउंट", "smart_contract": "स्मार्ट कॉन्ट्रैक्ट", - "standard_account": "Standard account", + "standard_account": "मानक अकाउंट", "siwe_message": { "url": "URL", "network": "नेटवर्क", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "स्मार्ट अकाउंट", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "मानक अकाउंट", "switch": "बदलें", "switchBack": "वापस स्विच करें", "includes_transaction": "{{transactionCount}} ट्रांसेक्शन शामिल हैं", @@ -6307,9 +6337,9 @@ "cancel": "कैंसिल करें", "description": "वह अमाउंट जिसे आप अपनी ओर से खर्च किए जाने में सहज महसूस करते हैं।", "invalid_number_error": "खर्च की सीमा एक संख्या होनी चाहिए", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "खर्च की सीमा खाली नहीं हो सकती", + "no_extra_decimals_error": "खर्च की सीमा में टोकन से अधिक दशमलव नहीं हो सकते", + "no_zero_error": "खर्च की सीमा 0 नहीं हो सकती", "no_zero_error_decrease_allowance": "'decreaseAllowance' विधि पर 0 खर्च की सीमा का कोई प्रभाव नहीं पड़ता है", "no_zero_error_increase_allowance": "'increaseAllowance' विधि पर 0 खर्च की सीमा का कोई प्रभाव नहीं पड़ता है", "save": "सेव करें", @@ -6336,7 +6366,7 @@ "transferRequest": "ट्रांसफर अनुरोध", "nested_transaction_heading": "ट्रांसेक्शन {{index}}", "transaction": "ट्रांसेक्शन", - "available_balance": "Available balance: ", + "available_balance": "उपलब्ध बैलेंस: ", "edit_amount_done": "जारी रखें", "deposit_edit_amount_done": "फंड जोड़ें", "deposit_edit_amount_predict_withdraw": "निकालें", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "नियम और शर्त", "select_token": "टोकन चुनें", "no_tokens_found": "कोई टोकन नहीं मिला", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "हमें इस नाम का कोई टोकन नहीं मिला। कोई एक अलग सर्च से कोशिश करें।", "select_network": "नेटवर्क चुनें", "all_networks": "सभी नेटवर्क", "num_networks": "{{numNetworks}} नेटवर्क", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "सभी को अचयनित करें", "see_all": "सभी को देखें", "all": "सभी", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}} और", "apply": "लागू करें", "slippage": "स्लिपेज (slippage)", "slippage_info": "यदि आपके ऑर्डर लगाए जाने और पुष्टि किए जाने के बीच कीमत में बदलाव होता है तो इसे \"स्लिपेज (slippage)\" कहा जाता है। यदि स्लिपेज (slippage) आपके द्वारा यहां निर्धारित सहनशीलता से अधिक हो जाता है तो आपका स्वैप स्वचालित रूप से कैंसिल हो जाएगा।", @@ -6392,7 +6422,7 @@ "quote_info_title": "रेट", "network_fee_info_title": "नेटवर्क फीस", "network_fee_info_content": "नेटवर्क फीस इस बात पर निर्भर करते हैं कि नेटवर्क कितना व्यस्त है और आपका ट्रांसेक्शन कितना जटिल है।", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "यह नेटवर्क फीस MetaMask देता है, इसलिए आप अपने अकाउंट में {{nativeToken}} के बिना भी ट्रांसेक्शन कर सकते हैं।", "points": "अनुमानित पॉइंट्स", "points_tooltip": "पॉइंट्स", "points_tooltip_content_1": "पॉइंट्स वह हैं जिनसे आप ट्रांसेक्शन पूरा करने पर MetaMask रिवार्ड्स कमाते हैं, जैसे जब आप स्वैप, ब्रिज या पर्प्स ट्रेड करते हैं।", @@ -6406,7 +6436,7 @@ "select_recipient": "प्राप्तकर्ता चुनें", "external_account": "बाहरी अकाउंट", "error_banner_description": "यह ट्रेड रूट अभी उपलब्ध नहीं है। राशि, नेटवर्क या टोकन बदलने का प्रयास करें और हम सबसे अच्छा विकल्प ढूँढ लेंगे।", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "यह ट्रेड रूट अभी उपलब्ध नहीं है। अमाउंट, नेटवर्क या टोकन बदलने की कोशिश करें और हम सबसे अच्छा विकल्प ढूंढ लेंगे।", "insufficient_funds": "अपर्याप्त फंड", "insufficient_gas": "अपर्याप्त गैस", "select_amount": "राशि चुनें", @@ -6417,9 +6447,9 @@ "title": "ब्रिज", "submitting_transaction": "सबमिट किया जा रहा है", "fetching_quote": "कोटेशन लाया जा रहा है", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "इसमें {{feePercentage}}% MetaMask फीस शामिल है।", "no_mm_fee": "कोई MM फीस नहीं", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "{{destTokenSymbol}} में स्वैप करने पर कोई MetaMask फीस नहीं।", "hardware_wallet_not_supported": "हार्डवेयर वॉलेट अभी तक सपोर्टेड नहीं हैं। जारी रखने के लिए हॉट वॉलेट का उपयोग करें।", "hardware_wallet_not_supported_solana": "Solana के लिए हार्डवेयर वॉलेट अभी तक सपोर्टेड नहीं हैं। जारी रखने के लिए हॉट वॉलेट का उपयोग करें।", "price_impact_info_title": "कीमत का प्रभाव", @@ -6432,17 +6462,24 @@ "approval_needed": "स्वैप के लिए टोकन एप्रूव करता है।", "approval_tooltip_title": "सटीक एक्सेस प्रदान करें", "approval_tooltip_content": "आप निर्दिष्ट राशि, {{amount}} {{symbol}} तक एक्सेस की अनुमति दे रहे हैं। कॉन्ट्रैक्ट किसी अतिरिक्त फंड को एक्सेस नहीं करेगा।", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "न्यूनतम प्राप्त किया गया", + "minimum_received_tooltip_title": "न्यूनतम प्राप्त किया गया", "minimum_received_tooltip_content": "आपके ट्रांसेक्शन के प्रोसेस होने के दौरान प्राइस बदलने पर जो न्यूनतम राशि आपको प्राप्त होगी, वह आपके स्लिपेज (slippage) टॉलरेंस पर आधारित है। यह हमारे लिक्विडिटी प्रदाताओं से एक अनुमान है। अंतिम राशि भिन्न हो सकती है।", + "market_closed": { + "title": "मार्केट बंद है", + "description": "इस टोकन को सपोर्ट करने वाला मार्केट अभी बंद है। टोकन को कभी भी ऑन-चेन ट्रांसफर किया जा सकता है।", + "learn_more": "ज़्यादा जानें", + "learn_more_url": "https://status.ondo.finance/market", + "done": "पूरा हुआ" + }, "submit": "सबमिट करें", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "अगर प्राइस स्लिपेज परसेंट से ज़्यादा बदल जाता है, तो आपका ट्रांसेक्शन पूरा नहीं होगा।", "cancel": "कैंसिल करें", "confirm": "कन्फर्म करें", "exceeding_upper_slippage_warning": "ज़्यादा स्लिपेज (slippage), इससे स्वैप बिगड़ सकता है", "exceeding_lower_slippage_warning": "कम स्लिपेज (slippage), इससे स्वैप बिगड़ सकता है", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "{{value}}% से ज़्यादा वैल्यू डालें", + "exceeding_upper_slippage_error": "आप {{value}}% से ज़्यादा वैल्यू नहीं डाल सकते", "custom": "कस्टम" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "वॉलेट रिकवरी", "login_with_social": "सोशल अकाउंट्स से लॉग इन करें", "setup": "सेट अप करें", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "सीक्रेट रिकवरी फ्रेज़ {{num}}", "back_up": "बैक अप लें", "reveal": "दिखाएं", "social_recovery_title": "{{authConnection}} रिकवरी", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "पासवर्ड दर्ज करें", "description": "कार्ड विवरण देखने के लिए अपना वॉलेट पासवर्ड दर्ज करें।", + "description_unfreeze": "अपने कार्ड पर खर्च फिर से शुरू करने के लिए अपना वॉलेट पासवर्ड डालें।", "placeholder": "पासवर्ड", "confirm": "कन्फर्म करें", "cancel": "कैंसिल करें", @@ -7001,6 +7039,7 @@ "enable_card_error": "कार्ड चालू नहीं हो पाया। कृपया बाद में फिर से कोशिश करें।", "view_card_details_error": "कार्ड विवरण लोड नहीं हो सके। कृपया फिर से प्रयास करें।", "biometric_verification_required": "कार्ड विवरण देखने के लिए प्रमाणीकरण आवश्यक है।", + "unfreeze_auth_required": "अपने कार्ड पर खर्च फिर से शुरू करने के लिए ऑथेंटिकेशन ज़रूरी है।", "warnings": { "close_spending_limit": { "title": "आप अपनी खर्च सीमा के करीब पहुँच चुके हैं", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "आपका कार्ड फ़्रीज़ कर दिया गया है", - "description": "कृपया अपने कार्ड को अनफ़्रीज़ करने के लिए सपोर्ट से संपर्क करें" + "description": "आपका कार्ड कुछ समय के लिए फ़्रीज़ किया गया है। आप इसे कभी भी अनफ़्रीज़ कर सकते हैं।" }, "blocked": { "title": "आपका कार्ड ब्लॉक कर दिया गया है", @@ -7068,7 +7107,14 @@ "travel_description": "70% तक की छूट के साथ होटल बुक करें", "card_tos_title": "नियम और शर्त", "order_metal_card": "मेटल कार्ड", - "order_metal_card_description": "अपना भौतिक मेटल कार्ड अभी ऑर्डर करें" + "order_metal_card_description": "अपना भौतिक मेटल कार्ड अभी ऑर्डर करें", + "freeze_card": "कार्ड फ़्रीज़ करें", + "unfreeze_card": "कार्ड अनफ़्रीज़ करें", + "freeze_card_description": "अपने कार्ड पर सभी खर्च रोकें", + "unfreeze_card_description": "अपने कार्ड पर सभी खर्च फिर से शुरू करें", + "freeze_error": "कार्ड का स्टेटस अपडेट नहीं हो पाया। कृपया फिर से कोशिश करें।", + "freeze_success": "कार्ड अच्छे से फ्रीज़ हो गया", + "unfreeze_success": "कार्ड अच्छे से अनफ्रीज़ हो गया" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "{{seconds}} सेकंड में फिर से भेज पाएंगे" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "{{walletName}} में जोड़ें", + "adding_to_wallet": "{{walletName}} में जोड़ें...", + "continue_setup": "{{walletName}} सेटअप जारी रखें", + "wallet_not_available": "{{walletName}} उपलब्ध नहीं है", + "already_in_wallet": "पहले से ही {{walletName}} में है", + "success_title": "कार्ड जोड़ा गया!", + "success_message": "आपका MetaMask कार्ड {{walletName}} में जोड़ दिया गया है।", + "error_title": "कार्ड नहीं जोड़ा जा सका", + "error_wallet_not_available": "{{walletName}} इस डिवाइस पर उपलब्ध नहीं है। कृपया पक्का करें कि आपने {{walletName}} सेट अप किया है।", + "error_wallet_not_initialized": "{{walletName}} तैयार नहीं हुआ है। कृपया अपना वॉलेट सेट अप करें और फिर से कोशिश करें।", "error_card_already_in_wallet": "यह कार्ड पहले से ही {{walletName}} में जोड़ा जा चुका है।", "error_card_pending": "आपका कार्ड {{walletName}} में सेट अप किया जा रहा है। कृपया कुछ मिनट बाद फिर से देखें।", "error_card_suspended": "{{walletName}} में आपका कार्ड सस्पेंड कर दिया गया है। मदद के लिए कृपया सपोर्ट से संपर्क करें।", "error_card_not_eligible": "यह कार्ड मोबाइल वॉलेट प्रोविज़निंग के लिए एलिजिबल नहीं है।", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "कार्ड डेटा एन्क्रिप्ट नहीं हो पाया। कृपया फिर से कोशिश करें।", "error_invalid_card_data": "कार्ड डेटा ग़लत है। कृपया अपने कार्ड की जानकारी सत्यापित करें और फिर से कोशिश करें।", "error_card_not_found": "कार्ड नहीं मिला। कृपया फिर से कोशिश करें।", "error_card_provider_not_found": "आपके इलाके के लिए कार्ड प्रोवाइडर उपलब्ध नहीं है।", "error_card_id_mismatch": "कार्ड सत्यापन नहीं हो पाया। कृपया फिर से कोशिश करें।", "error_card_not_active": "आपका कार्ड एक्टिव नहीं है। कृपया पहले अपना कार्ड एक्टिवेट करें।", "error_network": "नेटवर्क में गड़बड़ी हुई। कृपया अपना कनेक्शन जांचें और फिर से प्रयास करें।", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "रिक्वेस्ट टाइम आउट हो गई। कृपया फिर से कोशिश करें।", + "error_server": "सर्वर में एरर आया। कृपया बाद में फिर से कोशिश करें।", + "error_unknown": "कोई अनपेक्षित गड़बड़ी उत्पन्न हुई। कृपया बाद में फिर से प्रयास करें या सपोर्ट से कॉन्टेक्ट करें।", + "error_platform_not_supported": "यह प्लेटफ़ॉर्म मोबाइल वॉलेट प्रोविज़निंग को सपोर्ट नहीं करता है।", "try_again": "फिर से प्रयास करें", "cancel": "कैंसिल करें" } @@ -7299,7 +7345,7 @@ "main_title": "पुरस्कार", "referral_title": "रेफरल", "tab_overview_title": "ओवरव्यू", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "स्नैपशॉट्स", "tab_activity_title": "गतिविधि", "referral_stats_earned_from_referrals": "रेफरल से अर्जित", "referral_stats_referrals": "रेफरल", @@ -7353,7 +7399,7 @@ "verifying_rewards": "इससे पहले कि आप रिवॉर्ड क्लेम करें, हम पुष्टि कर रहे हैं कि सब कुछ सही है।" }, "season_status": { - "points_earned": "Points earned" + "points_earned": "पॉइंट्स कमाए" }, "onboarding": { "not_supported_region_title": "क्षेत्र सपोर्टेड नहीं है", @@ -7431,7 +7477,7 @@ "show_less": "कम दिखाएं", "linking_progress": "एकाउंट्स जोड़ा जा रहा है... ({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}} नामांकन किया गया", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "सभी एकाउंट जोड़ें" }, "referred_by_code": { "title": "रेफरल कोड", @@ -7514,7 +7560,7 @@ "claim_label": "क्लेम करें", "claimed_label": "क्लेम किया गया", "reward_claimed": "रिवॉर्ड क्लेम किया गया", - "time_left": "{{time}} left", + "time_left": "{{time}} बचा है", "expired": "एक्सपायर हो गया" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "रिडीम नहीं हो पाया", "redeem_failure_description": "कृपया बाद में फिर से प्रयास करें।", "reward_details": "पुरस्कार विवरण", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "वह अकाउंट चुनें जहाँ आप यह रिवॉर्ड भेजना चाहते हैं।" }, "animation": { "could_not_load": "लोड नहीं हो सका" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", - "pill_calculating": "Calculating", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "starts_date": "{{date}} को शुरू होता है", + "ends_date": "{{date}} को समाप्त होता है", + "results_coming_soon": "रिज़ल्ट जल्द ही आ रहे हैं", + "tokens_on_the_way": "टोकन आने वाले हैं", + "pill_up_next": "आगे आने वाला है", + "pill_live_now": "अब लाइव है", + "pill_calculating": "गणना की जा रही है", + "pill_results_ready": "रिज़ल्ट तैयार हैं", + "pill_complete": "पूरा" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "स्नैपशॉट्स", + "error_title": "स्नैपशॉट्स लोड नहीं हो पा रहे हैं", + "error_description": "हम स्नैपशॉट्स लोड नहीं कर पाए। कृपया फिर से कोशिश करें।", "retry_button": "फिर से प्रयास करें" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "एक्टिव", + "upcoming_title": "आगे आने वाला है", + "previous_title": "पिछला", + "empty_state": "कोई स्नैपशॉट उपलब्ध नहीं है", + "error_title": "स्नैपशॉट्स लोड नहीं हो पा रहे हैं", + "error_description": "हम स्नैपशॉट्स लोड नहीं कर पाए। कृपया फिर से कोशिश करें।", "retry_button": "फिर से प्रयास करें", - "refreshing": "Refreshing..." + "refreshing": "रिफ्रेश हो रहा है..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "{{approveSymbol}} एप्रूव करें", "bridge_approval_loading": "एप्रूव करें", "bridge_send": "{{sourceSymbol}} को {{sourceChain}} से ब्रिज करें", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "ब्रिज भेजें", "bridge_receive": "{{targetChain}} पर {{targetSymbol}} प्राप्त करें", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "ब्रिज प्राप्त करें", "default": "ट्रांसेक्शन", "musd_convert_send": "{{sourceSymbol}} को {{sourceChain}} से भेजा गया", "musd_claim": "mUSD क्लेम करें", @@ -7607,20 +7653,20 @@ "description": "{{dappName}} के साथ कनेक्शन स्थापित किया जा रहा है..." }, "show_error": { - "title": "Connection error", + "title": "कनेक्शन में गड़बड़ी", "description": "कनेक्शन स्थापित करना नहीं हो पाया। कृपया फिर से प्रयास करें।" }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "एप्रूवल रिजेक्ट हो गया", + "description": "यूज़र ने रिक्वेस्ट को रिजेक्ट कर दिया।" }, "show_return_to_app": { "title": "सफल", "description": "जारी रखने के लिए ऐप पर वापस जाएँ।" }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "कनेक्शन नहीं मिला", + "description": "जारी रखने के लिए कृपया ऐप से नया कनेक्शन बनाएं।" } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "एक्सप्लोर करें", + "trending_tokens": "ट्रेंडिंग टोकन", "price_change": "प्राइस में बदलाव", "all_networks": "सभी नेटवर्क", - "24h": "24h", + "24h": "24 घंटे", "time": "समय", "24_hours": "24 घंटे", "6_hours": "6 घंटे", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 घंटा", + "5_minutes": "5 मिनट", "networks": "नेटवर्क", "sort_by": "इसके अनुसार क्रमबद्ध करें", "volume": "वॉल्यूम", @@ -7650,32 +7696,48 @@ "high_to_low": "ज़्यादा से कम", "low_to_high": "कम से ज़्यादा", "apply": "लागू करें", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "टोकन, साइट, URL ढूंढें", "cancel": "कैंसिल करें", "perps": "पर्प्स", "predictions": "प्रेडिक्शंस", - "no_results": "No results found", + "no_results": "कोई रिज़ल्ट नहीं मिला", "sites": "साइट्स", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "पॉपुलर साइटें", + "search_sites": "साइट ढूंढें", + "enable_basic_functionality": "बेसिक फंक्शनलिटी को चालू करें", + "basic_functionality_disabled_title": "एक्सप्लोर उपलब्ध नहीं है", + "basic_functionality_disabled_description": "बेसिक फंक्शनलिटी बंद होने पर हम ज़रूरी मेटाडेटा नहीं ला सकते।", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "ट्रेडिंग टोकन उपलब्ध नहीं हैं", + "description": "हम अभी यह पेज नहीं ला सकते", "try_again": "फिर से प्रयास करें" }, "empty_search_result_state": { "title": "कोई टोकन नहीं मिला", - "description": "We were not able to find this token" + "description": "हमें यह टोकन नहीं मिला" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "अपडेट तैयार है", + "description_ios": "हमने कुछ ज़रूरी सुधार किए हैं। MetaMask के लेटेस्ट वर्शन के लिए रीलोड करें।", + "description_android": "हमने कुछ ज़रूरी सुधार किए हैं। अपडेट लागू करने के लिए MetaMask को बंद करके फिर से खोलें।", "primary_action_reload": "फिर से लोड करें", "primary_action_acknowledge": "समझ गए" + }, + "homepage": { + "sections": { + "tokens": "टोकन", + "perpetuals": "परपेचुअल्स", + "predictions": "प्रेडिक्शंस", + "defi": "DeFi", + "nfts": "NFTs", + "import_nfts": "NFTs इंपोर्ट करें", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/id.json b/locales/languages/id.json index a794a84bb80..3fce47c943f 100644 --- a/locales/languages/id.json +++ b/locales/languages/id.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "dihapus secara permanen", "reset_wallet_desc_2": "dari MetaMask di perangkat ini. Tindakan ini tidak dapat dibatalkan.", "reset_wallet_desc_login": "Untuk memulihkan dompet, Anda dapat menggunakan Frasa Pemulihan Rahasia, atau kata sandi akun Google atau Apple. MetaMask tidak memiliki informasi ini.", - "reset_wallet_desc_srp": "Untuk memulihkan dompet, pastikan Anda memiliki Frasa Pemulihan Rahasia. MetaMask tidak memiliki informasi ini." + "reset_wallet_desc_srp": "Untuk memulihkan dompet, pastikan Anda memiliki Frasa Pemulihan Rahasia. MetaMask tidak memiliki informasi ini.", + "biometric_authentication_cancelled": "Autentikasi biometrik dibatalkan", + "biometric_authentication_cancelled_title": "Pengaturan Biometrik Gagal", + "biometric_authentication_cancelled_description": "Atur ulang autentikasi biometrik dari pengaturan.", + "biometric_authentication_cancelled_button": "Konfirmasikan" }, "connect_hardware": { "title_select_hardware": "Hubungkan dompet perangkat keras", @@ -1040,7 +1044,7 @@ "title": "Jumlah yang akan dideposit", "get_usdc_hyperliquid": "Dapatkan USDC • Hyperliquid", "insufficient_funds": "Dana tidak cukup", - "no_funds_available": "Tidak ada dana yang tersedia. Silakan deposit terlebih dahulu.", + "no_funds_available": "Dana tidak cukup. Depositkan dana atau pilih metode pembayaran lain", "enter_amount": "Masukkan jumlah", "fetching_quote": "Mengambil kuotasi", "submitting": "Mengirimkan transaksi", @@ -1970,8 +1974,8 @@ "trade_again": "Berdagang lagi", "activity": { "deposit_title": "Deposit", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "Mendeposit {{amount}} {{symbol}}", + "withdrew_amount": "Menarik {{amount}} {{symbol}}", "status_completed": "Selesai", "status_failed": "Gagal", "status_pending": "Menunggu" @@ -2051,6 +2055,16 @@ "referral_code_text": "Gunakan kode referensi saya untuk mendapatkan reward tambahan." } }, + "market_insights": { + "title": "Wawasan pasar", + "updated_ago": "Diperbarui {{time}}", + "disclaimer": "Wawasan AI. Bukan nasihat keuangan.", + "whats_driving_price": "Apa yang mendorong kenaikan harga?", + "what_people_saying": "Apa yang dikatakan orang-orang", + "trade_button": "Berdagang", + "sources_count": "+{{count}} sumber", + "sources_title": "Teratas" + }, "predict": { "title": "MetaMask Predictions", "prediction_markets": "Pasar prediksi", @@ -2384,8 +2398,8 @@ "no_available_tokens": "Tidak melihat token Anda?", "add_tokens": "Impor token", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "{{tokenCount}} {{tokensLabel}} baru ditemukan di akun ini", "token_toast": { "tokens_imported_title": "Token yang diimpor", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "Desimal token wajib diisi.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "Kami tidak dapat menemukan token dengan nama tersebut.", + "tokens_empty_description": "Search for any token and import it", "select_token": "Pilih token", "address_must_be_smart_contract": "Alamat pribadi terdeteksi. Masukkan alamat kontrak token.", "billion_abbreviation": "M", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "Putuskan koneksi semua akun", "deceptive_site_ahead": "Situs yang menipu terdeteksi", "deceptive_site_desc": "Situs yang akan Anda kunjungi tidak aman. Penyerang dapat mengelabui Anda untuk melakukan sesuatu yang berbahaya.", + "malicious_site_detected": "Situs berbahaya terdeteksi", + "malicious_site_warning": "Jika Anda terhubung ke situs ini, Anda bisa kehilangan seluruh aset Anda.", + "connect_anyway": "Tetap Terhubung", "learn_more": "Selengkapnya", "advisory_by": "Penasihat disediakan oleh Ethereum Phishing Detector dan PhishFort", "potential_threat": "Potensi ancaman meliputi", @@ -2846,7 +2864,11 @@ "permissions": "Izin", "card_title": "Kartu MetaMask", "settings": "Pengaturan", - "log_out": "Keluar" + "networks": "Jaringan", + "log_out": "Keluar", + "notifications": "Notifikasi", + "buy": "Beli", + "scan": "Pindai" }, "app_settings": { "enabling_notifications": "Mengaktifkan notifikasi...", @@ -2870,6 +2892,8 @@ "state_logs": "Log state", "add_network_title": "Tambahkan jaringan", "auto_lock": "Kunci otomatis", + "enable_device_authentication": "Aktifkan Autentikasi Perangkat", + "enable_device_authentication_desc": "Gunakan biometrik atau kode sandi perangkat Anda untuk membuka MetaMask.", "auto_lock_desc": "Pilih jumlah waktu sebelum aplikasi terkunci secara otomatis.", "state_logs_desc": "Ini akan membantu MetaMask memperbaiki masalah apa pun yang mungkin Anda temui. Kirimkan kepada dukungan MetaMask melalui ikon hamburger > Kirim Umpan Balik, atau balas tiket yang ada, jika ada.", "autolock_immediately": "Langsung", @@ -2975,6 +2999,11 @@ "add_rpc_url": "Tambahkan URL RPC", "add_block_explorer_url": "Tambahkan URL block explorer", "networks_desc": "Tambahkan dan edit jaringan RPC khusus", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "Cari jaringan", + "networks_no_results": "Jaringan tidak ditemukan", "network_name_label": "Nama jaringan", "network_name_placeholder": "Nama jaringan (opsional)", "network_rpc_url_label": "URL RPC", @@ -2991,7 +3020,16 @@ "network_other_networks": "Jaringan lainnya", "network_rpc_networks": "Jaringan RPC", "network_add_network": "Tambahkan jaringan", + "add_chain_title": "Tambahkan jaringan", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "Coba lagi", + "add_chain_added": "Added", + "add_chain_or": "atau", + "add_chain_custom_link": "Tambahkan jaringan khusus", "network_add_custom_network": "Tambahkan jaringan khusus", + "network_add_test_network": "Add a test network", "network_add": "Tambahkan", "network_save": "Simpan", "remove_network_title": "Apakah Anda ingin menghapus jaringan ini?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "Oke", "title": "Akun tidak dapat terhubung", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "Pindai kode QR di situs ini untuk terhubung kembali ke MetaMask" }, "app_information": { "title": "Informasi", @@ -3379,6 +3417,7 @@ "sell_description": "Jual kripto untuk dapat uang tunai" }, "asset_overview": { + "market_closed": "Pasar tutup", "send_button": "Kirim", "buy_button": "Beli", "cash_buy_button": "Pembelian Tunai", @@ -3399,19 +3438,6 @@ "bridge": "Bridge", "earn": "Dapatkan", "convert_to_musd": "Konversikan ke mUSD", - "merkl_rewards": { - "annual_bonus": "Bonus {{apy}}%", - "claimable_bonus": "Bonus yang dapat diklaim", - "claimable_bonus_tooltip_description": "Bonus mUSD diklaim di Linea.", - "terms_apply": "Syarat berlaku.", - "ok": "Oke", - "claim": "Klaim", - "processing_claim": "Memproses klaim...", - "claim_on_linea_title": "Klaim bonus di Linea", - "claim_on_linea_description": "Bonus Anda akan diterbitkan di Linea, terpisah dari saldo mUSD Ethereum.", - "continue": "Lanjutkan", - "unexpected_error": "Terjadi kesalahan tidak terduga. Coba lagi." - }, "tron": { "daily_resource_new_energy": "Energi harian baru", "sufficient_to_cover": "Cukup untuk menutupi", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "Alamat token disalin ke papan klip" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "Kode QR tidak valid", "invalid_qr_code_message": "Kode QR yang Anda coba pindai tidak valid.", "allow_camera_dialog_title": "Izinkan akses kamera", "allow_camera_dialog_message": "Kami memerlukan izin Anda untuk memindai kode QR", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "Tampaknya Anda mencoba menyinkronkan dengan ekstensi. Untuk melakukannya, Anda harus menghapus dompet saat ini. \n\nSetelah Anda menghapus atau menginstal ulang versi baru aplikasi, pilih opsi untuk \"Menyinkronkan dengan Ekstensi MetaMask\". Penting! Sebelum menghapus dompet Anda, pastikan Anda telah mencadangkan Frasa Pemulihan Rahasia.", "not_allowed_error_title": "Aktifkan akses kamera", "not_allowed_error_desc": "Untuk memindai kode QR, berikan MetaMask akses ke kamera dari menu pengaturan perangkat Anda.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "Kode QR tak dikenal", "unrecognized_address_qr_code_desc": "Maaf, kode QR ini tidak terkait dengan alamat akun atau alamat kontrak.", "url_redirection_alert_title": "Anda akan mengunjungi tautan eksternal", "url_redirection_alert_desc": "Tautan dapat digunakan untuk mencoba menipu atau mengelabui orang, jadi pastikan untuk mengunjungi situs web yang aman saja.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "Anda tidak memiliki koleksi ini", "known_asset_contract": "Alamat kontrak aset yang diketahui", "max": "Maks", - "recipient_address": "Recipient address", + "recipient_address": "Alamat penerima", "required": "Diperlukan", "to": "Ke", "total": "Total", @@ -3641,7 +3667,7 @@ "nevermind": "Abaikan", "edit_network_fee": "Edit biaya gas", "edit_priority": "Edit prioritas", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "Biaya pembatalan gas", "gas_speedup_fee": "Biaya percepatan Gas", "use_max": "Gunakan maks", "set_gas": "Atur", @@ -3650,7 +3676,7 @@ "transaction_fee": "Biaya gas", "transaction_fee_less": "Tidak ada biaya", "total_amount": "Jumlah total", - "view_data": "View data", + "view_data": "Lihat data", "adjust_transaction_fee": "Sesuaikan biaya transaksi", "could_not_resolve_ens": "Tidak dapat menyelesaikan ENS", "asset": "Aset", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "Untuk menelusuri web yang terdesentralisasi, tambahkan tab baru", "got_it": "Mengerti", "max_tabs_title": "Jumlah tab maksimum tercapai", - "max_tabs_desc": "Saat ini kami hanya mendukung 5 tab yang terbuka sekaligus. Tutup tab yang ada sebelum menambahkan tab baru.", + "max_tabs_desc": "Saat ini kami hanya mendukung 20 tab yang terbuka sekaligus. Tutup tab yang ada sebelum menambahkan tab baru.", "failed_to_resolve_ens_name": "Kami tidak dapat menyelesaikan nama ENS tersebut", "remove_bookmark_title": "Hapus favorit", "remove_bookmark_msg": "Yakin ingin menghapus situs ini dari favorit Anda?", @@ -3828,7 +3854,7 @@ "cancel_button": "Batal" }, "approval": { - "title": "Confirm transaction" + "title": "Konfirmasikan transaksi" }, "approve": { "title": "Setujui", @@ -3839,39 +3865,39 @@ "unavailable": "Tidak tersedia", "tx_review_confirm": "Konfirmasikan", "tx_review_transfer": "Transfer", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "Penempatan kontrak", + "tx_review_transfer_from": "Transfer dari", + "tx_review_unknown": "Metode tidak dikenal", "tx_review_approve": "Setujui", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "Naikkan tunjangan", + "tx_review_set_approval_for_all": "Atur persetujuan untuk semua", + "tx_review_staking_claim": "Klaim stake", "tx_review_staking_deposit": "Deposit stake", "tx_review_staking_unstake": "Batalkan stake", "tx_review_lending_deposit": "Deposit pinjaman", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "Penarikan pinjaman", "tx_review_perps_deposit": "Perp yang didanai", "tx_review_predict_deposit": "Dana prediksi", "tx_review_predict_claim": "Klaim kemenangan", "tx_review_predict_withdraw": "Prediksi penarikan", "tx_review_musd_conversion": "Konversi mUSD", "claim": "Klaim", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "Mengirim ETH", + "self_sent_ether": "Mengirim ETH ke diri Anda sendiri", + "received_ether": "Menerima ETH", "sent_dai": "Mengirim DAI", "self_sent_dai": "Mengirim DAI ke Diri Sendiri", "received_dai": "Menerima DAI", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "Mengirim token", + "received_tokens": "Menerima token", "ether": "ETH", "sent_unit": "Mengirim {{unit}}", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "Mengirim {{unit}} ke diri Anda sendiri", "received_unit": "Menerima {{unit}}", "sent_collectible": "Koleksi yang dikirim", "received_collectible": "Koleksi yang diterima", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "Kirim ETH", + "send_unit": "Kirim {{unit}}", "send_collectible": "Kirim koleksi", "receive_collectible": "Terima koleksi", "sent": "Mengirim", @@ -3881,17 +3907,17 @@ "send": "Kirim", "redeposit": "Deposit ulang", "interaction": "Interaksi", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "Penempatan kontrak", + "to_contract": "Kontrak baru", + "mint": "Cetak", "tx_details_free": "Gratis", "tx_details_not_available": "Tidak tersedia", "smart_contract_interaction": "Interaksi kontrak cerdas", "swaps_transaction": "Transaksi swap", "bridge_transaction": "Bridge", "approve": "Setujui", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "Naikkan tunjangan", + "set_approval_for_all": "Atur persetujuan untuk semua", "hash": "Hash", "from": "Dari", "to": "Ke", @@ -3899,15 +3925,15 @@ "amount": "Jumlah", "fee": { "transaction_fee_in_ether": "Biaya transaksi", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "Biaya transaksi (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "Gas yang digunakan (unit)", + "gas_limit": "Batas gas (satuan)", + "gas_price": "Harga gas (GWEI)", + "base_fee": "Biaya dasar (GWEI)", + "priority_fee": "Biaya prioritas (GWEI)", "multichain_priority_fee": "Biaya prioritas", - "max_fee": "Max fee per gas", + "max_fee": "Biaya maks per gas", "total": "Total", "view_on": "Lihat di", "view_on_etherscan": "Lihat di Etherscan", @@ -3923,13 +3949,13 @@ "nonce": "Nonce", "from_device_label": "dari perangkat ini", "import_wallet_row": "Akun ditambahkan ke perangkat ini", - "import_wallet_label": "Account added", + "import_wallet_label": "Akun ditambahkan", "import_wallet_tip": "Seluruh transaksi mendatang yang dilakukan dari perangkat ini akan menyertakan label \"dari perangkat ini\" di samping stempel waktu. Untuk transaksi yang diberi tanggal sebelum menambahkan akun, riwayat ini tidak akan menunjukkan transaksi keluar mana saja yang berasal dari perangkat ini.", "sign_title_scan": "Pindai ", "sign_title_device": "dengan dompet perangkat keras Anda", "sign_description_1": "Setelah menandatangani dengan dompet perangkat keras,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "Ketuk dapatkan tanda tangan", + "sign_get_signature": "Dapatkan tanda tangan", "transaction_id": "ID Transaksi", "network": "Jaringan", "request_from": "Permintaan dari", @@ -4032,7 +4058,7 @@ "title": "Jaringan", "other_networks": "Jaringan lainnya", "close": "Tutup", - "status_ok": "All systems operational", + "status_ok": "Operasional seluruh sistem", "status_not_ok": "Jaringan mengalami masalah", "want_to_add_network": "Ingin menambahkan jaringan ini?", "add_custom_network": "Tambahkan jaringan khusus", @@ -4051,7 +4077,7 @@ "review": "Tinjau", "view_details": "Lihat detail", "network_details": "Detail jaringan", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "Memilih konfirmasikan akan mengaktifkan pemeriksaan detail jaringan. Anda dapat menonaktifkan pemeriksaan detail jaringan di ", "network_settings_security_privacy": "Pengaturan > Keamanan dan privasi", "network_currency_symbol": "Simbol mata uang", "network_block_explorer_url": "URL block explorer", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "Penyedia jaringan jahat dapat berbohong tentang status blockchain dan merekam aktivitas jaringan Anda. Hanya tambahkan jaringan kustom yang Anda percayai.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Informasi jaringan", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "Informasi jaringan tambahan", "network_warning_desc": "Koneksi jaringan ini mengandalkan pihak ketiga. Koneksi ini mungkin kurang bisa diandalkan atau memungkinkan pihak ketiga melacak aktivitas.", "additonial_network_information_desc": "Beberapa jaringan ini mengandalkan pihak ketiga. Koneksi ini kurang dapat diandalkan atau memungkinkan pihak ketiga melacak aktivitas.", "connect_more_networks": "Hubungkan jaringan lainnya", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "Jaringan ini tidak digunakan lagi", "network_deprecated_description": "Jaringan yang Anda coba hubungkan tidak lagi didukung di MetaMask.", "edit_networks_title": "Edit jaringan", - "no_network_fee": "No network fee" + "no_network_fee": "Tidak ada biaya jaringan" }, "permissions": { "title_this_site_wants_to": "Situs ini ingin:", @@ -4111,11 +4137,11 @@ "network_connected": "jaringan terhubung ", "see_your_accounts": "Melihat akun Anda dan menyarankan transaksi", "connected_to": "Terhubung dengan ", - "manage_permissions": "Manage permissions", + "manage_permissions": "Kelola izin", "edit": "Edit", "cancel": "Batal", "got_it": "Mengerti", - "connection_details_title": "Connection details", + "connection_details_title": "Detail koneksi", "connection_details_description": "Anda terhubung ke situs ini menggunakan browser MetaMask pada {{connectionDateTime}}", "title_add_network_permission": "Tambahkan izin jaringan", "add_this_network": "Tambahkan jaringan ini", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "Buka dengan PIN perangkat?" }, "authentication": { - "auth_prompt_title": "Autentikasi diperlukan", - "auth_prompt_desc": "Harap autentikasikan untuk menggunakan MetaMask", - "fingerprint_prompt_title": "Autentikasi diperlukan", - "fingerprint_prompt_desc": "Gunakan sidik jari untuk membuka MetaMask", - "fingerprint_prompt_cancel": "Batal" + "auth_prompt_desc": "Harap autentikasikan untuk menggunakan MetaMask" }, "accountApproval": { "title": "PERMINTAAN CONNECT", "walletconnect_title": "PERMINTAAN WALLETCONNECT", "action": "Hubungkan ke situs ini?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "Untuk melanjutkan koneksi, pilih nomor yang Anda lihat di situs", + "action_reconnect_deeplink": "Ingin terhubung kembali ke situs ini?", "connect": "Hubungkan", "resume": "Lanjutkan", "cancel": "Batal", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "Lupa koneksi situs ini", "disconnect": "Putuskan koneksi", "permission": "Lihat", "address": "alamat publik", @@ -4218,7 +4240,7 @@ "error_title": "Terjadi kesalahan", "error_message": "Kami tidak dapat mengimpor kunci pribadi tersebut. Pastikan Anda memasukkannya dengan benar.", "error_empty_message": "Masukkan kunci pribadi Anda.", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "atau pindai kode QR" }, "import_private_key_success": { "title": "Akun berhasil diimpor!", @@ -4229,18 +4251,18 @@ "import_wallet_title": "Impor dompet", "enter_srp_subtitle": "Masukkan Frasa Pemulihan Rahasia", "textarea_placeholder": "Tambahkan spasi di antara setiap kata dan pastikan tidak ada yang melihat", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "Masukkan Frasa Pemulihan Rahasia dompet. Anda dapat mengimpor Frasa Pemulihan Rahasia Ethereum, Solana, atau Bitcoin.", + "subtitle": "Tempel Frasa Pemulihan Rahasia", "cta_text": "Lanjutkan", "paste": "Tempel", "clear": "Hapus semua", "srp_number_of_words_option_title": "Jumlah kata", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "Saya memiliki frasa yang terdiri dari 12 kata", + "24_word_option": "Saya memiliki frasa yang terdiri dari 24 kata", "error_title": "Terjadi kesalahan", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "Kami tidak dapat mengimpor Frasa Pemulihan Rahasia tersebut. Pastikan Anda memasukkannya dengan benar.", + "error_empty_message": "Anda perlu memasukkan Frasa Pemulihan Rahasia.", + "error_number_of_words_error_message": "Frasa Pemulihan Rahasia berisi 12 atau 24 kata", "error_srp_is_case_sensitive": "Masukan tidak valid! Frasa Pemulihan Rahasia peka kapital.", "error_srp_word_error_1": "Kata ", "error_srp_word_error_2": " salah atau salah eja.", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " salah atau salah eja.", "error_invalid_srp": "Frasa Pemulihan Rahasia Tidak Valid", "error_duplicate_srp": "Frasa Pemulihan Rahasia ini telah diimpor.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "Akun yang Anda coba impor adalah akun duplikat.", + "invalid_qr_code_title": "Kode QR tidak valid", + "invalid_qr_code_message": "Kode QR tidak berisi Frasa Pemulihan Rahasia yang valid", "success_1": "Dompet", "success_2": "diimpor" }, @@ -4665,7 +4687,7 @@ "button": "Lindungi dompet" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "Pembaruan transaksi gagal", "text": "Apakah Anda ingin mencobanya lagi?", "cancel_button": "Batal", "retry_button": "Coba lagi" @@ -4684,13 +4706,13 @@ "next": "Selanjutnya", "amount_placeholder": "0,00", "link_copied": "Tautan disalin ke papan klip", - "send_link_title": "Send link", + "send_link_title": "Kirim tautan", "description_1": "Tautan permintaan Anda siap dikirim!", "description_2": "Kirimkan tautan ini kepada teman, dan ini akan meminta mereka untuk mengirim", "copy_to_clipboard": "Salin ke papan klip", "qr_code": "Kode QR", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "Kirim tautan", + "request_qr_code": "Kode QR permintaan pembayaran", "balance": "Saldo" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "Anda tidak memiliki sesi aktif", - "end_session_title": "End session", + "end_session_title": "Akhiri sesi", "end": "Akhiri", "cancel": "Batal", - "session_ended_title": "Session ended", + "session_ended_title": "Sesi berakhir", "session_ended_desc": "Sesi yang dipilih telah dihentikan", "session_already_exist": "Sesi ini sudah terhubung.", "close_current_session": "Tutup sesi saat ini sebelum memulai yang baru." @@ -4765,15 +4787,14 @@ "on_network": "pada {{networkName}}", "debit_card": "Kartu debit", "select_payment_method": "Pilih metode pembayaran", - "loading_quote": "Loading quote...", "pay_with": "Bayar melalui", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "Membeli melalui {{providerName}}.", + "change_provider": "Ubah penyedia.", "payment_error": "Terjadi kesalahan. Coba lagi.", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "Tidak ada metode pembayaran yang tersedia.", "error_fetching_quotes": "Terjadi kesalahan. Coba lagi.", "no_quotes_available": "Penyedia tidak tersedia.", - "providers": "Providers", + "providers": "Penyedia", "continue": "Lanjutkan", "powered_by_provider": "Didukung oleh {{provider}}", "purchased_currency": "Membeli {{currency}}", @@ -4871,6 +4892,15 @@ "log_out": "Keluar dari {{provider}}", "logged_out_success": "Berhasil keluar", "logged_out_error": "Kesalahan saat keluar" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "batas penjualan terendah", "medium_sell_limit": "batas penjualan medium", "highest_sell_limit": "batas penjualan tertinggi", - "change": "Change", + "change": "Ubah", "continue_to_amount": "Lanjutkan ke jumlah", "no_payment_methods_title": "Tidak ada metode pembayaran di {{regionName}}", "no_cash_destinations_title": "Tidak ada destinasi tunai di {{regionName}}", @@ -5118,7 +5148,7 @@ "start_swapping": "Mulai menukar" }, "feature_off_title": "Sementara tidak tersedia", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "MetaMask Swaps sedang menjalani pemeliharaan. Periksa kembali nanti.", "wrong_network_title": "Pertukaran tidak tersedia", "wrong_network_body": "Anda hanya dapat menukar token di Jaringan Utama Ethereum.", "unallowed_asset_title": "Tidak dapat menukar token ini", @@ -5160,7 +5190,7 @@ "not_enough": "{{symbol}} tidak cukup untuk menyelesaikan pertukaran ini", "max_slippage": "Selip maks", "max_slippage_amount": "Selip maks {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "Jika harga berubah antara waktu penempatan dan konfirmasi order Anda, ini disebut “selip”. Swap akan otomatis dibatalkan jika selip melebihi pengaturan “selip maks”.", "slippage_warning": "Pastikan Anda merasa yakin dengan yang Anda lakukan!", "allows_up_to_decimals": "{{symbol}} memungkinkan hingga {{decimals}} desimal", "get_quotes": "Dapatkan kuotasi", @@ -5199,7 +5229,7 @@ "edit": "Edit", "quotes_include_fee": "Kuotasi mencakup {{fee}}% biaya MetaMask", "quotes_include_gas_and_metamask_fee": "Kuotasi mencakup biaya gas dan biaya MetaMask sebesar {{fee}}%", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "Ketuk untuk menukar", "swipe_to_swap": "Usap untuk menukar", "swipe_to": "Usap untuk", "swap": "Tukar", @@ -5259,7 +5289,7 @@ "approve": "Setujui {{sourceToken}} untuk ditukar: Hingga {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "Tunda swap ({{sourceToken}} ke {{destinationToken}})", "swap_confirmed": "Pertukaran selesai ({{sourceToken}} ke {{destinationToken}})", "approve_pending": "Menyetujui {{sourceToken}} untuk ditukar", "approve_confirmed": "{{sourceToken}} disetujui untuk ditukar" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "Menu tarik-turun jaringan dipindahkan ke aset Anda", "description_2": "Swap dan Bridge dalam satu alur sederhana", - "description_3": "Streamlined send experience", + "description_3": "Pengalaman pengiriman yang lebih efisien", "description_4": "Tampilan akun baru" }, "more_information": "Kini Anda dapat fokus pada token dan aktivitas, bukan jaringan di belakangnya.", @@ -5406,21 +5436,21 @@ "aggressive_label": "Agresif", "aggressive_text": "Kemungkinan besar, bahkan di pasar yang fluktuaktif. Gunakan Agresif untuk menutup biaya lonjakan lalu lintas jaringan karena hal-hal seperti penurunan NFT yang populer.", "market_label": "Pasar", - "market_text": "Use market for fast processing at current market price.", + "market_text": "Gunakan pasar untuk pemrosesan cepat dengan harga pasar saat ini.", "low_label": "Rendah", "low_text": "Gunakan opsi rendah untuk menunggu harga yang lebih murah. Estimasi waktu kurang akurat karena harga agak sulit diprediksi.", "link": "Pelajari selengkapnya seputar penyesuaian gas." }, "save": "Simpan", "submit": "Kirim", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "Biaya prioritas maks rendah untuk kondisi jaringan saat ini", + "max_priority_fee_high": "Biaya prioritas maks lebih tinggi dari yang seharusnya", + "max_priority_fee_speed_up_low": "Biaya prioritas maks minimal harus {{speed_up_floor_value}} GWEI (10% lebih tinggi dari transaksi awal)", + "max_priority_fee_cancel_low": "Biaya prioritas maks minimal harus {{cancel_value}} GWEI (50% lebih tinggi dari transaksi awal)", + "max_fee_low": "Biaya maks rendah untuk kondisi jaringan saat ini", + "max_fee_high": "Biaya maks lebih tinggi dari yang seharusnya", + "max_fee_speed_up_low": "Biaya maks minimal harus {{speed_up_floor_value}} GWEI (10% lebih tinggi dari transaksi awal)", + "max_fee_cancel_low": "Biaya maks minimal harus {{cancel_value}} GWEI (50% lebih tinggi dari transaksi awal)", "learn_more_gas_limit": "Batas gas merupakan unit maksimum gas yang ingin Anda gunakan. Unit gas merupakan pengganda untuk “Biaya prioritas maks” dan “Biaya maks”.", "learn_more_max_priority_fee": "Biaya prioritas maks (alias “tip penambang”) langsung masuk ke penambang dan memberi insentif kepada mereka untuk memprioritaskan transaksi Anda. Pengaturan maks akan menjadi yang paling sering Anda bayar", "learn_more_max_fee": "Biaya maks merupakan biaya tertinggi yang akan Anda bayarkan (biaya dasar + biaya prioritas).", @@ -5530,10 +5560,10 @@ "enable_remember_me_description": "Saat Ingatkan saya aktif, siapa pun yang memiliki akses ke ponsel Anda dapat mengakses akun MetaMask Anda." }, "turn_off_remember_me": { - "title": "Masukkan kata sandi Anda untuk menonaktifkan Ingatkan saya", - "placeholder": "Kata sandi", - "description": "Jika Anda menonaktifkan opsi ini, Anda memerlukan kata sandi untuk membuka MetaMask mulai sekarang.", - "action": "Nonaktifkan Ingatkan saya" + "title": "Nonaktifkan Ingat Saya", + "placeholder": "Konfirmasikan kata sandi", + "description": "Setelah dinonaktifkan, fitur Ingat Saya tidak dapat digunakan lagi. Fitur ini telah dihentikan, jadi Anda dapat membuka MetaMask dengan kata sandi atau biometrik sebagai gantinya.", + "action": "Nonaktifkan Ingat Saya" }, "dapp_connect": { "warning": "Untuk menggunakan fitur ini, perbarui aplikasi ke versi terbaru" @@ -5582,7 +5612,7 @@ "learn_more": "Pelajari selengkapnya" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "Verifikasikan detail pihak ketiga", "protect_from_scams": "Untuk melindungi diri Anda dari penipu, luangkan waktu sejenak untuk memverifikasi detail pihak ketiga.", "learn_to_verify": "Pelajari cara memverifikasi detail pihak ketiga", "spending_cap": "batas penggunaan", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "Pemulihan diperlukan", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "Terjadi kesalahan, tetapi jangan khawatir! Mari coba pulihkan dompet Anda.", "restore_needed_action": "Pulihkan dompet" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Gagal menutup aplikasi yang berjalan di perangkat Ledger.", "ethereum_app_not_installed": "Aplikasi Ethereum belum diinstal.", "ethereum_app_not_installed_error": "Instal aplikasi Ethereum di perangkat Ledger.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "Aplikasi Ethereum tidak terbuka", + "eth_app_not_open_message": "Buka aplikasi Ethereum di perangkat Ledger.", "ledger_is_locked": "Ledger terkunci", "unlock_ledger_message": "Buka perangkat Ledger", "cannot_get_account": "Tidak bisa mendapatkan akun", @@ -5797,8 +5827,8 @@ "error_description": "Instalasi {{snap}} gagal." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "Bonus tahunan yang dapat diklaim setiap hari dari dompet Anda.", + "earn_a_percentage_bonus": "Dapatkan bonus sebesar {{percentage}}%", "claimable_bonus": "Bonus yang dapat diklaim", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "Waktu yang diperlukan untuk menarik token dari protokol dan mengembalikannya ke dompet Anda", "receive": "Token ini digunakan untuk melacak aset dan reward. Jangan mentransfer atau memperdagangkannya, atau Anda tidak akan dapat menarik aset.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "Faktor kesehatan Anda mengukur risiko likuidasi", "above_two_dot_zero": "Di atas 2,0", "safe_position": "Posisi aman", "between_one_dot_five_and_2_dot_zero": "Antara 1,5-2,0", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "Risiko likuidasi sedang", "below_one_dot_five": "Di bawah 1,5", "higher_liquidation_risk": "Risiko likuidasi lebih tinggi" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "Mengapa saya tidak dapat menarik seluruh saldo?", "your_withdrawal_amount_may_be_limited_by": "Jumlah penarikan Anda mungkin dibatasi oleh", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "Likuiditas pool", "not_enough_funds_available_in_the_lending_pool_right_now": "Dana yang tersedia pada pool pinjaman saat ini tidak cukup.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "Posisi pinjaman yang ada", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "Penarikan dapat membuat posisi pinjaman saat ini berisiko dilikuidasi." } }, @@ -5998,11 +6028,11 @@ "earn_button": "Dapatkan" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "Stake TRX dan dapatkan", + "stake_any_amount": "Stake sejumlah TRX.", "earn_trx_rewards": "Dapatkan reward TRX.", "earn_trx_rewards_description": "Mulailah menghasilkan segera setelah Anda melakukan stake. Reward akan terakumulasi secara otomatis.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "Batalkan stake setipa saat. Pembatalan stake memerlukan waktu 14 hari." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "Estimasi biaya gas", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "Biaya gas dibayarkan kepada penambang kripto yang memproses transaksi di jaringan Ethereum. MetaMask tidak mengambil keuntungan dari biaya gas.", "gas_fluctuation": "Biaya gas diperkirakan dan akan berfluktuasi berdasarkan lalu lintas jaringan dan kompleksitas transaksi.", "gas_learn_more": "Pelajari selengkapnya seputar biaya gas" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "Masuk dengan", "spender": "Spender", "now": "Sekarang", - "switching_to": "Switching to", + "switching_to": "Beralih ke", "bridge_estimated_time": "Estimasi waktu", "pay_with": "Bayar melalui", - "receive_as": "Receive", + "receive_as": "Terima", "total": "Total", - "you_receive": "You'll receive", + "you_receive": "Anda akan menerima", "transaction_fee": "Biaya transaksi", - "transaction_fees": "Transaction fees", + "transaction_fees": "Biaya transaksi", "metamask_fee": "Biaya MetaMask", "network_fee": "Biaya jaringan", "bridge_fee": "Biaya penyedia bridge" @@ -6234,7 +6264,7 @@ "transaction_fee": "Kami akan menukar token Anda dengan USDC.e di Polygon, jaringan yang digunakan oleh Predictions. Penyedia swap mungkin akan mengenakan biaya, tetapi MetaMask tidak." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMask akan menukar token Anda ke token yang Anda inginkan. MetaMask tidak mengenakan biaya saat Anda menukar ke MUSD." }, "musd_conversion": { "transaction_fee": "Biaya konversi mUSD mencakup biaya jaringan dan mungkin termasuk biaya penyedia layanan." @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "Situs ini meminta tanda tangan Anda", "transaction_tooltip": "Situs ini meminta transaksi Anda", "details": "Detail", - "qr_get_sign": "Get signature", + "qr_get_sign": "Dapatkan tanda tangan", "qr_scan_text": "Pindai dengan dompet perangkat keras", "sign_with_ledger": "Tandatangani dengan Ledger", "smart_account": "Akun cerdas", "smart_contract": "Kontrak cerdas", - "standard_account": "Standard account", + "standard_account": "Akun standar", "siwe_message": { "url": "URL", "network": "Jaringan", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "Akun cerdas", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "Akun standar", "switch": "Alihkan", "switchBack": "Alihkan kembali", "includes_transaction": "Mencakup {{transactionCount}} transaksi", @@ -6307,9 +6337,9 @@ "cancel": "Batalkan", "description": "Masukkan jumlah yang paling sesuai untuk digunakan atas nama Anda.", "invalid_number_error": "Batas penggunaan harus berupa angka", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "Batas penggunaan tidak boleh kosong", + "no_extra_decimals_error": "Batas penggunaan tidak boleh memiliki desimal lebih banyak dari token", + "no_zero_error": "Batas penggunaan tidak boleh 0", "no_zero_error_decrease_allowance": "Batas penggunaan 0 tidak berpengaruh pada metode 'decreaseAllowance'", "no_zero_error_increase_allowance": "Batas penggunaan 0 tidak berpengaruh pada metode 'increaseAllowance'", "save": "Simpan", @@ -6336,7 +6366,7 @@ "transferRequest": "Permintaan transfer", "nested_transaction_heading": "Transaksi {{index}}", "transaction": "Transaksi", - "available_balance": "Available balance: ", + "available_balance": "Saldo tersedia: ", "edit_amount_done": "Lanjutkan", "deposit_edit_amount_done": "Tambahkan dana", "deposit_edit_amount_predict_withdraw": "Tarik", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "Syarat & Ketentuan", "select_token": "Pilih token", "no_tokens_found": "Tidak ada token yang ditemukan", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "Kami tidak dapat menemukan token dengan nama ini. Coba pencarian lain.", "select_network": "Pilih jaringan", "all_networks": "Semua jaringan", "num_networks": "{{numNetworks}} jaringan", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "Batal pilih semua", "see_all": "Lihat semua", "all": "Semua", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}} lainnya", "apply": "Terapkan", "slippage": "Selip", "slippage_info": "Jika harga berubah antara waktu penempatan dan konfirmasi order Anda, ini disebut “selip”. Pertukaran akan otomatis dibatalkan jika selip melebihi toleransi yang Anda tetapkan di sini.", @@ -6392,7 +6422,7 @@ "quote_info_title": "Tingkat", "network_fee_info_title": "Biaya jaringan", "network_fee_info_content": "Biaya jaringan bergantung pada seberapa sibuk jaringan dan seberapa rumit transaksi Anda.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "Biaya jaringan ini dibayar oleh MetaMask, sehingga Anda dapat bertransaksi tanpa {{nativeToken}} di akun Anda.", "points": "Estimasi poin", "points_tooltip": "Poin", "points_tooltip_content_1": "Poin merupakan cara Anda memperoleh MetaMask Rewards untuk menyelesaikan transaksi, seperti saat Anda menukar, menggunakan bridge, atau memperdagangkan perp.", @@ -6406,7 +6436,7 @@ "select_recipient": "Pilih penerima", "external_account": "Akun eksternal", "error_banner_description": "Rute perdagangan ini tidak tersedia untuk saat ini. Coba ubah jumlah, jaringan, atau token, dan kami akan mencari opsi terbaik.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "Rute perdagangan ini tidak tersedia untuk saat ini. Coba ubah jumlah, jaringan, atau token, dan kami akan menemukan opsi terbaik.\n\nIngatlah bahwa jika Anda mencoba memperdagangkan Saham Token Ondo, Anda mungkin akan dibatasi secara geografis, misalnya melalui AS, Uni Eropa, Britania Raya, dan Brasil.", "insufficient_funds": "Dana tidak cukup", "insufficient_gas": "Gas tidak cukup", "select_amount": "Pilih jumlah", @@ -6417,9 +6447,9 @@ "title": "Bridge", "submitting_transaction": "Mengirim", "fetching_quote": "Mengambil kuotasi", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "Termasuk biaya MetaMask sebesar {{feePercentage}}%.", "no_mm_fee": "Tidak ada biaya MM", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "Tidak ada biaya MetaMask saat menukar ke {{destTokenSymbol}}.", "hardware_wallet_not_supported": "Dompet perangkat keras belum didukung. Gunakan hot wallet untuk melanjutkan.", "hardware_wallet_not_supported_solana": "Dompet perangkat keras belum didukung untuk Solana. Gunakan hot wallet untuk melanjutkan.", "price_impact_info_title": "Dampak harga", @@ -6432,17 +6462,24 @@ "approval_needed": "Menyetujui token untuk swap.", "approval_tooltip_title": "Berikan akses yang tepat", "approval_tooltip_content": "Anda mengizinkan akses ke jumlah yang ditentukan, {{amount}} {{symbol}}. Kontrak tidak akan mengakses dana tambahan apa pun.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "Jumlah minimum yang diterima", + "minimum_received_tooltip_title": "Jumlah minimum yang diterima", "minimum_received_tooltip_content": "Jumlah minimum yang akan Anda terima jika harga berubah selama transaksi diproses, berdasarkan toleransi selip. Ini merupakan estimasi dari penyedia likuiditas kami. Jumlah akhir dapat berbeda.", + "market_closed": { + "title": "Pasar tutup", + "description": "Pasar yang mendukung token ini saat ini ditutup. Token dapat ditransfer on-chain setiap saat.", + "learn_more": "Pelajari selengkapnya", + "learn_more_url": "https://status.ondo.finance/market", + "done": "Selesai" + }, "submit": "Kirim", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "Transaksi Anda tidak akan berhasil jika harga berubah lebih dari persentase selip.", "cancel": "Batal", "confirm": "Konfirmasikan", "exceeding_upper_slippage_warning": "Selip tinggi, ini dapat mengakibatkan swap yang tidak menguntungkan", "exceeding_lower_slippage_warning": "Selip rendah, ini dapat mengakibatkan swap yang tidak menguntungkan", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "Masukkan nilai yang lebih besar dari {{value}}%", + "exceeding_upper_slippage_error": "Anda tidak dapat memasukkan nilai yang lebih besar dari {{value}}%", "custom": "Kustom" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "Pemulihan dompet", "login_with_social": "Masuk dengan akun sosial", "setup": "Atur", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "Frasa Pemulihan Rahasia {{num}}", "back_up": "Cadangkan", "reveal": "Tampilkan", "social_recovery_title": "PEMULIHAN {{authConnection}}", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "Masukkan kata sandi", "description": "Masukkan kata sandi dompet untuk melihat detail kartu.", + "description_unfreeze": "Masukkan kata sandi dompet Anda untuk melanjutkan penggunaan kartu.", "placeholder": "Kata sandi", "confirm": "Konfirmasikan", "cancel": "Batalkan", @@ -7001,6 +7039,7 @@ "enable_card_error": "Gagal mengaktifkan kartu. Coba lagi nanti.", "view_card_details_error": "Tidak dapat memuat detail kartu. Coba lagi.", "biometric_verification_required": "Autentikasi untuk melihat detail kartu diperlukan.", + "unfreeze_auth_required": "Autentikasi diperlukan untuk melanjutkan penggunaan kartu.", "warnings": { "close_spending_limit": { "title": "Batas penggunaan hampir tercapai", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "Kartu Anda dibekukan", - "description": "Hubungi dukungan untuk mencairkan kartu Anda" + "description": "Kartu Anda diblokir untuk sementara. Anda dapat membuka blokirnya setiap saat." }, "blocked": { "title": "Kartu Anda diblokir", @@ -7068,7 +7107,14 @@ "travel_description": "Pesan hotel dengan diskon hingga 70%", "card_tos_title": "Syarat dan Ketentuan", "order_metal_card": "Kartu Logam", - "order_metal_card_description": "Pesan Kartu Logam fisik sekarang" + "order_metal_card_description": "Pesan Kartu Logam fisik sekarang", + "freeze_card": "Blokir kartu", + "unfreeze_card": "Buka blokir kartu", + "freeze_card_description": "Jeda semua penggunaan kartu", + "unfreeze_card_description": "Lanjutkan semua penggunaan kartu", + "freeze_error": "Gagal memperbarui status kartu. Coba lagi.", + "freeze_success": "Kartu berhasil diblokir", + "unfreeze_success": "Blokir kartu berhasil dibuka" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "Kirim ulang tersedia dalam {{seconds}} detik" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "Tambahkan ke {{walletName}}", + "adding_to_wallet": "Menambahkan ke {{walletName}}...", + "continue_setup": "Lanjutkan Pengaturan {{walletName}}", + "wallet_not_available": "{{walletName}} tidak tersedia", + "already_in_wallet": "Sudah ada di {{walletName}}", + "success_title": "Kartu ditambahkan!", + "success_message": "Kartu MetaMask Anda telah ditambahkan ke {{walletName}}.", + "error_title": "Tidak dapat menambahkan kartu", + "error_wallet_not_available": "{{walletName}} tidak tersedia di perangkat ini. Pastikan Anda telah mengatur {{walletName}}.", + "error_wallet_not_initialized": "{{walletName}} belum diinisialisasi. Atur dompet Anda dan coba lagi.", "error_card_already_in_wallet": "Kartu ini sudah ditambahkan ke {{walletName}}.", "error_card_pending": "Kartu Anda sedang diatur di {{walletName}}. Periksa kembali dalam beberapa menit.", "error_card_suspended": "Kartu Anda di {{walletName}} telah diblokir. Hubungi dukungan untuk mendapatkan bantuan.", "error_card_not_eligible": "Kartu ini tidak memenuhi syarat untuk penyediaan dompet digital.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "Gagal mengenkripsi data kartu. Coba lagi.", "error_invalid_card_data": "Data kartu tidak valid. Verifikasikan detail kartu Anda dan coba lagi.", "error_card_not_found": "Kartu tidak ditemukan. Coba lagi.", "error_card_provider_not_found": "Penyedia kartu tidak tersedia untuk wilayah Anda.", "error_card_id_mismatch": "Verifikasi kartu gagal. Coba lagi.", "error_card_not_active": "Kartu Anda belum aktif. Aktifkan kartu terlebih dahulu.", "error_network": "Terjadi kesalahan jaringan. Periksa koneksi Anda dan coba lagi.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "Waktu permintaan telah habis. Coba lagi.", + "error_server": "Terjadi kesalahan server. Coba lagi nanti.", + "error_unknown": "Terjadi kesalahan tak terduga. Coba lagi atau hubungi dukungan.", + "error_platform_not_supported": "Platform ini tidak mendukung penyediaan dompet seluler.", "try_again": "Coba lagi", "cancel": "Batalkan" } @@ -7299,7 +7345,7 @@ "main_title": "Reward", "referral_title": "Rujukan", "tab_overview_title": "Ikhtisar", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "Snapshot", "tab_activity_title": "Aktivitas", "referral_stats_earned_from_referrals": "Diperoleh dari rujukan", "referral_stats_referrals": "Rujukan", @@ -7353,7 +7399,7 @@ "verifying_rewards": "Kami memastikan semuanya benar sebelum Anda mengklaim reward." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "Poin yang diperoleh" }, "onboarding": { "not_supported_region_title": "Wilayah tidak didukung", @@ -7431,7 +7477,7 @@ "show_less": "Ciutkan", "linking_progress": "Menambahkan akun... ({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}} terdaftar", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "Tambahkan semua akun" }, "referred_by_code": { "title": "Kode Referensi", @@ -7514,7 +7560,7 @@ "claim_label": "Klaim", "claimed_label": "Diklaim", "reward_claimed": "Reward diklaim", - "time_left": "{{time}} left", + "time_left": "{{time}} tersisa", "expired": "Kedaluwarsa" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "Penukaran gagal", "redeem_failure_description": "Coba lagi nanti.", "reward_details": "Detail Reward", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "Pilih akun tempat reward ini dikirimkan." }, "animation": { "could_not_load": "Tidak dapat memuat" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", + "starts_date": "Mulai {{date}}", + "ends_date": "Berakhir {{date}}", + "results_coming_soon": "Hasil akan segera diumumkan", + "tokens_on_the_way": "Token sedang dalam perjalanan", + "pill_up_next": "Selanjutnya", + "pill_live_now": "Sedang live", "pill_calculating": "Menghitung", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "pill_results_ready": "Hasil Sudah Siap", + "pill_complete": "Selesaikan" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "Snapshot", + "error_title": "Tidak dapat memuat snapshot", + "error_description": "Kami tidak dapat memuat snapshot. Coba lagi.", "retry_button": "Coba lagi" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "Aktif", + "upcoming_title": "Mendatang", + "previous_title": "Sebelumnya", + "empty_state": "Snapshot tidak tersedia", + "error_title": "Tidak dapat memuat snapshot", + "error_description": "Kami tidak dapat memuat snapshot. Coba lagi.", "retry_button": "Coba lagi", - "refreshing": "Refreshing..." + "refreshing": "Menyegarkan..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "Setujui {{approveSymbol}}", "bridge_approval_loading": "Setujui", "bridge_send": "Bridge {{sourceSymbol}} dari {{sourceChain}}", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "Bridge kirim", "bridge_receive": "Terima {{targetSymbol}} di {{targetChain}}", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "Bridge terima", "default": "Transaksi", "musd_convert_send": "Mengirim {{sourceSymbol}} dari {{sourceChain}}", "musd_claim": "Klaim mUSD", @@ -7607,20 +7653,20 @@ "description": "Membuat koneksi dengan {{dappName}}..." }, "show_error": { - "title": "Connection error", + "title": "Kesalahan koneksi", "description": "Gagal membuat koneksi. Coba lagi." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "Persetujuan ditolak", + "description": "Pengguna menolak permintaan tersebut." }, "show_return_to_app": { "title": "Berhasil", "description": "Kembali ke aplikasi untuk melanjutkan." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "Koneksi Tidak Ditemukan", + "description": "Buat koneksi baru dari aplikasi untuk melanjutkan." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "Jelajahi", + "trending_tokens": "Token yang tren", "price_change": "Perubahan harga", "all_networks": "Semua jaringan", - "24h": "24h", + "24h": "24 jam", "time": "Waktu", "24_hours": "24 jam", "6_hours": "6 jam", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 jam", + "5_minutes": "5 menit", "networks": "Jaringan", "sort_by": "Urutkan sesuai", "volume": "Volume", @@ -7650,32 +7696,48 @@ "high_to_low": "Tinggi ke rendah", "low_to_high": "Rendah ke tinggi", "apply": "Terapkan", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "Cari token, situs, URL", "cancel": "Batal", "perps": "Perps", "predictions": "Prediksi", - "no_results": "No results found", + "no_results": "Hasil tidak ditemukan", "sites": "Situs", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "Situs populer", + "search_sites": "Cari situs", + "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.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "Token yang tren tidak tersedia", + "description": "Kami tidak dapat mengakses halaman ini saat ini", "try_again": "Coba lagi" }, "empty_search_result_state": { "title": "Tidak ada token yang ditemukan", - "description": "We were not able to find this token" + "description": "Kami tidak dapat menemukan token ini" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "Pembaruan siap", + "description_ios": "Kami telah melakukan beberapa perbaikan penting. Muat ulang untuk versi MetaMask terbaru.", + "description_android": "Kami telah melakukan beberapa perbaikan penting. Tutup dan buka kembali MetaMask untuk menerapkan pembaruan.", "primary_action_reload": "Muat ulang", "primary_action_acknowledge": "Mengerti" + }, + "homepage": { + "sections": { + "tokens": "Token", + "perpetuals": "Abadi", + "predictions": "Prediksi", + "defi": "DeFi", + "nfts": "NFT", + "import_nfts": "Impor NFT", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/ja.json b/locales/languages/ja.json index 4536414ba56..bb91707b2ee 100644 --- a/locales/languages/ja.json +++ b/locales/languages/ja.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "完全に消去されます", "reset_wallet_desc_2": "。この操作は元に戻せません。", "reset_wallet_desc_login": "ウォレットの復元には、シークレットリカバリーフレーズか、GoogleまたはAppleアカウントのパスワードを利用できます。MetaMaskはこの情報を保持していません。", - "reset_wallet_desc_srp": "ウォレットを復元するには、シークレットリカバリーフレーズがあることを確認してください。MetaMaskはこの情報を保持していません。" + "reset_wallet_desc_srp": "ウォレットを復元するには、シークレットリカバリーフレーズがあることを確認してください。MetaMaskはこの情報を保持していません。", + "biometric_authentication_cancelled": "生体認証がキャンセルされました", + "biometric_authentication_cancelled_title": "生体認証の設定に失敗しました", + "biometric_authentication_cancelled_description": "設定で生体認証を設定しなおしてください。", + "biometric_authentication_cancelled_button": "確定" }, "connect_hardware": { "title_select_hardware": "ハードウェアウォレットの接続", @@ -1040,7 +1044,7 @@ "title": "入金額", "get_usdc_hyperliquid": "USDCを入手 • Hyperliquid", "insufficient_funds": "資金不足", - "no_funds_available": "利用可能な資金がありません。はじめにデポジットを行ってください。", + "no_funds_available": "資金が足りません。入金するか、別の支払方法を選択してください", "enter_amount": "金額を入力してください", "fetching_quote": "クォートを取得中", "submitting": "トランザクションを送信中", @@ -1970,8 +1974,8 @@ "trade_again": "再取引", "activity": { "deposit_title": "入金する", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "{{amount}} {{symbol}}を入金しました", + "withdrew_amount": "{{amount}} {{symbol}}を引き出しました", "status_completed": "完了", "status_failed": "失敗", "status_pending": "保留中" @@ -2051,6 +2055,16 @@ "referral_code_text": "私の紹介コードを使って追加の報酬を獲得しましょう。" } }, + "market_insights": { + "title": "市場分析情報", + "updated_ago": "{{time}}に更新", + "disclaimer": "AI分析情報であり、金融に関するアドバイスではありません。", + "whats_driving_price": "価格はなぜ変動するのですか?", + "what_people_saying": "人々のコメント", + "trade_button": "取引", + "sources_count": "他{{count}}件のソース", + "sources_title": "ソース" + }, "predict": { "title": "MetaMask 予測", "prediction_markets": "予測市場", @@ -2384,8 +2398,8 @@ "no_available_tokens": "トークンが見当たりませんか?", "add_tokens": "トークンをインポート", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "このアカウントに{{tokenCount}}件の新しい{{tokensLabel}}が見つかりました", "token_toast": { "tokens_imported_title": "トークンをインポートしました", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "トークンの小数点以下は必須入力項目です。", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "その名前のトークンは見つかりませんでした。", + "tokens_empty_description": "Search for any token and import it", "select_token": "トークンを選択", "address_must_be_smart_contract": "パーソナルアドレスが検出されました。トークンのコントラクトアドレスを入力してください。", "billion_abbreviation": "B", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "すべてのアカウントを接続解除", "deceptive_site_ahead": "偽装サイトにアクセスしようとしています", "deceptive_site_desc": "アクセスしようとしているサイトは安全ではありません。攻撃者が危険な行為に誘導しようとする可能性があります。", + "malicious_site_detected": "悪質なサイトが検出されました", + "malicious_site_warning": "このサイトに接続すると、資産をすべて失う可能性があります。", + "connect_anyway": "それでも接続する", "learn_more": "詳細", "advisory_by": "イーサリアムフィッシング検知システムとPhishFortからの忠告", "potential_threat": "潜在的な脅威には次のものが含まれます", @@ -2846,7 +2864,11 @@ "permissions": "アクセス許可", "card_title": "MetaMaskカード", "settings": "設定", - "log_out": "ログアウト" + "networks": "ネットワーク", + "log_out": "ログアウト", + "notifications": "通知", + "buy": "購入", + "scan": "スキャン" }, "app_settings": { "enabling_notifications": "通知を有効にしています...", @@ -2870,6 +2892,8 @@ "state_logs": "ステートログ", "add_network_title": "ネットワークを追加", "auto_lock": "自動ロック", + "enable_device_authentication": "デバイス認証を有効にする", + "enable_device_authentication_desc": "MetaMaskのロックを解除するには、デバイスの生体認証またはパスコードを使用してください。", "auto_lock_desc": "アプリケーションが自動的にロックされるまでの時間を選択してください。", "state_logs_desc": "これはMetaMaskが問題のデバッグを行ううえで参考になります。「ハンバーガーアイコン」>「フィードバックを送信」から、またはチケットがある場合はそのチケットに返信して、MetaMaskサポートまでお送りください。", "autolock_immediately": "すぐに", @@ -2975,6 +2999,11 @@ "add_rpc_url": "RPC URLを追加", "add_block_explorer_url": "ブロックエクスプローラーURLを追加", "networks_desc": "カスタムRPCネットワークの追加と編集", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "ネットワークを検索", + "networks_no_results": "ネットワークが見つかりません", "network_name_label": "ネットワーク名", "network_name_placeholder": "ネットワーク名 (オプション)", "network_rpc_url_label": "RPC URL", @@ -2991,7 +3020,16 @@ "network_other_networks": "他のネットワーク", "network_rpc_networks": "RPCネットワーク", "network_add_network": "ネットワークを追加", + "add_chain_title": "ネットワークを追加", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "再試行", + "add_chain_added": "Added", + "add_chain_or": "または", + "add_chain_custom_link": "カスタムネットワークを追加", "network_add_custom_network": "カスタムネットワークを追加", + "network_add_test_network": "Add a test network", "network_add": "追加", "network_save": "保存", "remove_network_title": "このネットワークを削除しますか?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "OK", "title": "アカウントを接続できませんでした", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "MetaMaskに再接続するには、サイトのQRコードをスキャンしてください" }, "app_information": { "title": "情報", @@ -3379,6 +3417,7 @@ "sell_description": "仮想通貨を売って現金化します" }, "asset_overview": { + "market_closed": "市場が取引時間外です", "send_button": "送信", "buy_button": "購入", "cash_buy_button": "キャッシュ購入", @@ -3399,19 +3438,6 @@ "bridge": "ブリッジ", "earn": "獲得", "convert_to_musd": "mUSDに変換", - "merkl_rewards": { - "annual_bonus": "{{apy}}%のボーナス", - "claimable_bonus": "獲得できるボーナス", - "claimable_bonus_tooltip_description": "mUSDボーナスの請求はLinea上で行います。", - "terms_apply": "諸条件が適用されます。", - "ok": "OK", - "claim": "請求", - "processing_claim": "請求を処理しています...", - "claim_on_linea_title": "Lineaでボーナスを獲得", - "claim_on_linea_description": "ボーナスはLinea上で発行され、イーサリアムのmUSD残高とは切り離されます。", - "continue": "続行", - "unexpected_error": "予期せぬエラーが発生しました。もう一度お試しください。" - }, "tron": { "daily_resource_new_energy": "1日の新規エネルギー量", "sufficient_to_cover": "カバーできる取引量:", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "トークンアドレスがクリップボードにコピーされました" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "無効なQRコード", "invalid_qr_code_message": "スキャンしようとしているQRコードが有効ではありません。", "allow_camera_dialog_title": "カメラへのアクセスを許可してください。", "allow_camera_dialog_message": "QRコードのスキャンにはアクセス許可が必要です", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "拡張機能と同期しようとしているようですが、これを行うには現在のウォレットを消去する必要があります。\n\n消去してアプリの新しいバージョンを再インストールしたら、「MetaMask拡張機能と同期」オプションを選択してください。ウォレットを消去する前に、必ず秘密のリカバリーフレーズがバックアップされていることを確認してください。", "not_allowed_error_title": "カメラへのアクセスをオンにしてください", "not_allowed_error_desc": "QRコードをスキャンするには、デバイスの設定メニューでMetaMaskにカメラへのアクセスを許可する必要があります。", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "認識されないQRコード", "unrecognized_address_qr_code_desc": "申し訳ございませんが、このQRコードはアカウントアドレスまたはコントラクトアドレスに関連付けられていません。", "url_redirection_alert_title": "外部リンクにアクセスしようとしています", "url_redirection_alert_desc": "リンクは詐欺やフィッシングに利用される場合があるため、信頼できるWebサイトにのみアクセスするようにしてください。", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "このコレクティブルを所有していません", "known_asset_contract": "既知のアセットコントラクトアドレス", "max": "最大", - "recipient_address": "Recipient address", + "recipient_address": "受取人のアドレス", "required": "必須", "to": "送信先", "total": "合計", @@ -3641,7 +3667,7 @@ "nevermind": "キャンセル", "edit_network_fee": "ガス代を編集", "edit_priority": "優先度を編集", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "キャンセルのガス代", "gas_speedup_fee": "スピードアップのガス代", "use_max": "最大額を使用", "set_gas": "設定", @@ -3650,7 +3676,7 @@ "transaction_fee": "ガス代", "transaction_fee_less": "手数料なし", "total_amount": "合計額", - "view_data": "View data", + "view_data": "データを表示", "adjust_transaction_fee": "トランザクション手数料を調整", "could_not_resolve_ens": "ENSの名前解決ができませんでした", "asset": "アセット", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "分散型インターネットを閲覧するには、新規タブを追加してください", "got_it": "了解", "max_tabs_title": "タブの最大数に達しました", - "max_tabs_desc": "現在同時に開けるタブの数は5つまでです。新しいタブを追加する前に、既存のタブを閉じてください。", + "max_tabs_desc": "現在同時に開けるタブの数は20つまでです。新しいタブを追加する前に、既存のタブを閉じてください。", "failed_to_resolve_ens_name": "ENS名を解決できませんでした", "remove_bookmark_title": "お気に入りを削除", "remove_bookmark_msg": "このサイトをお気に入りから削除してよろしいですか?", @@ -3828,7 +3854,7 @@ "cancel_button": "キャンセル" }, "approval": { - "title": "Confirm transaction" + "title": "トランザクションの確定" }, "approve": { "title": "承認", @@ -3839,39 +3865,39 @@ "unavailable": "利用不可", "tx_review_confirm": "確定", "tx_review_transfer": "送金", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "コントラクトのデプロイ", + "tx_review_transfer_from": "送金元:", + "tx_review_unknown": "不明な方法", "tx_review_approve": "承認", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "許容額を増やす", + "tx_review_set_approval_for_all": "すべてを承認に設定", + "tx_review_staking_claim": "ステーキングの請求", "tx_review_staking_deposit": "ステーキング用デポジット", "tx_review_staking_unstake": "ステーキングを解除", "tx_review_lending_deposit": "レンディング用デポジット", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "貸付金の引き出し", "tx_review_perps_deposit": "パーペチュアルに入金しました", "tx_review_predict_deposit": "入金済みの予測", "tx_review_predict_claim": "請求済みの報酬", "tx_review_predict_withdraw": "予測の出金", "tx_review_musd_conversion": "mUSDへの変換", "claim": "請求", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "ETHを送金しました", + "self_sent_ether": "ETHを自分に送金しました", + "received_ether": "ETHを受け取りました", "sent_dai": "送信されたDAI", "self_sent_dai": "自分に送信されたDAI", "received_dai": "受け取ったDAI", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "トークンを送金しました", + "received_tokens": "トークンを受け取りました", "ether": "ETH", "sent_unit": "送信された{{unit}}", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "自分に{{unit}}を送金しました", "received_unit": "受け取った{{unit}}", "sent_collectible": "送られたコレクティブル", "received_collectible": "受け取ったコレクティブル", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "ETHを送金", + "send_unit": "{{unit}}を送金", "send_collectible": "コレクティブルを送る", "receive_collectible": "コレクティブルを受け取る", "sent": "送信済み", @@ -3881,17 +3907,17 @@ "send": "送金", "redeposit": "再入金", "interaction": "やり取り", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "コントラクトのデプロイ", + "to_contract": "新しいコントラクト", + "mint": "ミント", "tx_details_free": "無料", "tx_details_not_available": "利用できません", "smart_contract_interaction": "スマートコントラクトのインタラクション", "swaps_transaction": "スワップトランザクション", "bridge_transaction": "ブリッジ", "approve": "承認", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "許容額を増やす", + "set_approval_for_all": "すべてを承認に設定", "hash": "ハッシュ", "from": "送信元", "to": "送信先", @@ -3899,15 +3925,15 @@ "amount": "金額", "fee": { "transaction_fee_in_ether": "トランザクション手数料", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "トランザクション手数料 (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "使用ガス (単位)", + "gas_limit": "ガスリミット (単位)", + "gas_price": "ガス価格 (gwei)", + "base_fee": "基本料金 (gwei)", + "priority_fee": "優先手数料 (gwei)", "multichain_priority_fee": "優先手数料", - "max_fee": "Max fee per gas", + "max_fee": "ガス1単位あたりの最大手数料", "total": "合計", "view_on": "表示方法:", "view_on_etherscan": "Etherscanで表示", @@ -3923,13 +3949,13 @@ "nonce": "ナンス", "from_device_label": "このデバイスから", "import_wallet_row": "このデバイスに追加されたアカウント", - "import_wallet_label": "Account added", + "import_wallet_label": "追加されたアカウント", "import_wallet_tip": "今後このデバイスで行われるトランザクションにはすべて、タイムスタンプの隣に「このデバイスから」というラベルが付きます。このアカウントを追加する前のトランザクションに関しては、履歴 (このデバイスからどの送信トランザクションが処理されたか) が表示されません。", "sign_title_scan": "スキャン", "sign_title_device": "ハードウェアウォレットで", "sign_description_1": "ハードウェアウォレットで署名した後、", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "「署名を取得」をタップします", + "sign_get_signature": "署名を取得", "transaction_id": "トランザクションID", "network": "ネットワーク", "request_from": "要求元", @@ -4032,7 +4058,7 @@ "title": "ネットワーク", "other_networks": "他のネットワーク", "close": "閉じる", - "status_ok": "All systems operational", + "status_ok": "全システムが稼働しています", "status_not_ok": "ネットワークに問題があります", "want_to_add_network": "このネットワークを追加しますか?", "add_custom_network": "カスタムネットワークを追加", @@ -4051,7 +4077,7 @@ "review": "確認", "view_details": "詳細を表示", "network_details": "ネットワークの詳細", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "「確定」を選択すると、ネットワーク情報の確認がオンになります。ネットワーク情報の確認は、次の場所でオフにできます: ", "network_settings_security_privacy": "「設定」>「セキュリティとプライバシー」で", "network_currency_symbol": "通貨記号", "network_block_explorer_url": "ブロックエクスプローラーURL", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "悪意のあるネットワーク プロバイダーは、ブロックチェーンのステータスを偽り、ユーザーのネットワークアクティビティを記録することがあります。信頼するカスタムネットワークのみを追加してください。", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "ネットワーク情報", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "その他のネットワーク情報", "network_warning_desc": "このネットワーク接続はサードパーティに依存しているため、信頼性が低かったり、サードパーティによるアクティビティの追跡が可能になる可能性があります。", "additonial_network_information_desc": "これらのネットワークの一部はサードパーティに依存しているため、接続の信頼性が低かったり、サードパーティによるアクティビティの追跡が可能になったりする可能性があります。", "connect_more_networks": "他のネットワークを接続する", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "このネットワークはサポートされなくなりました", "network_deprecated_description": "接続しようとしているネットワークは現在MetaMaskによりサポートされていません。", "edit_networks_title": "ネットワークの編集", - "no_network_fee": "No network fee" + "no_network_fee": "ネットワーク手数料なし" }, "permissions": { "title_this_site_wants_to": "このサイトが次のことを求めています:", @@ -4111,11 +4137,11 @@ "network_connected": "ネットワークに接続しました", "see_your_accounts": "アカウントを確認しトランザクションを提案する", "connected_to": "接続先: ", - "manage_permissions": "Manage permissions", + "manage_permissions": "アクセス許可の管理", "edit": "編集", "cancel": "キャンセル", "got_it": "了解", - "connection_details_title": "Connection details", + "connection_details_title": "接続の詳細", "connection_details_description": "{{connectionDateTime}}にMetaMaskブラウザを使用してこのサイトに接続しました", "title_add_network_permission": "ネットワークのアクセス許可を追加", "add_this_network": "このネットワークを追加", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "デバイスの暗証番号でロックを解除しますか?" }, "authentication": { - "auth_prompt_title": "認証が必要です", - "auth_prompt_desc": "MetaMaskを使用するには認証してください", - "fingerprint_prompt_title": "認証が必要です", - "fingerprint_prompt_desc": "指紋でMetaMaskのロックを解除してください", - "fingerprint_prompt_cancel": "キャンセル" + "auth_prompt_desc": "MetaMaskを使用するには認証してください" }, "accountApproval": { "title": "接続リクエスト", "walletconnect_title": "WalletConnectリクエスト", "action": "このサイトに接続しますか?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "接続を再開するには、サイトに表示されている数字を選択してください", + "action_reconnect_deeplink": "このサイトに再び接続しますか?", "connect": "接続", "resume": "再開", "cancel": "キャンセル", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "このサイトの接続を記憶しない", "disconnect": "接続解除", "permission": "次を表示:", "address": "パブリックアドレス", @@ -4218,7 +4240,7 @@ "error_title": "問題が発生しました", "error_message": "秘密鍵をインポートできませんでした。正しく入力されていることを確認してください。", "error_empty_message": "秘密鍵の入力が必要です。", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "またはQRコードをスキャンしてください" }, "import_private_key_success": { "title": "アカウントがインポートされました!", @@ -4229,18 +4251,18 @@ "import_wallet_title": "ウォレットをインポート", "enter_srp_subtitle": "シークレットリカバリーフレーズを入力", "textarea_placeholder": "各単語の間に半角スペースを入れ、誰にも見られないようにご注意ください", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "ウォレットのシークレットリカバリーフレーズを入力してください。イーサリアム、Solana、ビットコインのシークレットリカバリーフレーズをインポートできます。", + "subtitle": "シークレットリカバリーフレーズを貼り付けます", "cta_text": "続行", "paste": "貼り付け", "clear": "すべて消去", "srp_number_of_words_option_title": "単語数", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "12単語のフレーズがあります", + "24_word_option": "24単語のフレーズがあります", "error_title": "問題が発生しました", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "指定されたシークレットリカバリーフレーズをインポートできませんでした。正しく入力されていることを確認してください。", + "error_empty_message": "シークレットリカバリーフレーズの入力が必要です。", + "error_number_of_words_error_message": "シークレットリカバリーフレーズは12単語または24単語のいずれかで構成されています", "error_srp_is_case_sensitive": "無効な入力です!シークレットリカバリーフレーズは大文字・小文字を区別します。", "error_srp_word_error_1": "単語", "error_srp_word_error_2": "が正しくないか、スペルが間違っています。", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": "が正しくないか、スペルが間違っています。", "error_invalid_srp": "無効なシークレットリカバリーフレーズ", "error_duplicate_srp": "このシークレットリカバリーフレーズはすでにインポートされています。", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "インポートしようとしているアカウントは重複アカウントです。", + "invalid_qr_code_title": "無効なQRコード", + "invalid_qr_code_message": "QRコードに有効なシークレットリカバリーフレーズが含まれていません", "success_1": "ウォレット", "success_2": "インポートされました" }, @@ -4665,7 +4687,7 @@ "button": "ウォレットを保護" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "トランザクションの更新に失敗しました", "text": "もう一度試しますか?", "cancel_button": "キャンセル", "retry_button": "もう一度試す" @@ -4684,13 +4706,13 @@ "next": "次へ", "amount_placeholder": "0.00", "link_copied": "リンクがクリップボードにコピーされました", - "send_link_title": "Send link", + "send_link_title": "リンクの送信", "description_1": "リクエストリンクの送信準備ができました!", "description_2": "このリンクを友達に送信すると、送るようリクエストされます", "copy_to_clipboard": "クリップボードにコピー", "qr_code": "QRコード", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "リンクを送信", + "request_qr_code": "支払いリクエストQRコード", "balance": "残高" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "アクティブなセッションがありません", - "end_session_title": "End session", + "end_session_title": "セッションの終了", "end": "終了", "cancel": "キャンセル", - "session_ended_title": "Session ended", + "session_ended_title": "セッションが終了しました", "session_ended_desc": "選択されたセッションは終了しました", "session_already_exist": "このセッションはすでに接続されています。", "close_current_session": "新しいセッションを開始する前に、現在のセッションを閉じてください。" @@ -4765,15 +4787,14 @@ "on_network": "{{networkName}}を利用", "debit_card": "デビットカード", "select_payment_method": "支払方法を選択", - "loading_quote": "Loading quote...", "pay_with": "支払方法:", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "{{providerName}}で購入しようとしています。", + "change_provider": "プロバイダーを変更します。", "payment_error": "問題が発生しました。もう一度お試しください。", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "使用可能な支払方法がありません。", "error_fetching_quotes": "問題が発生しました。もう一度お試しください。", "no_quotes_available": "利用可能なプロバイダーがありません。", - "providers": "Providers", + "providers": "プロバイダー", "continue": "続行", "powered_by_provider": "Powered by {{provider}}", "purchased_currency": "{{currency}}を購入しました", @@ -4871,6 +4892,15 @@ "log_out": "{{provider}}からログアウトする", "logged_out_success": "ログアウトしました", "logged_out_error": "ログアウト中にエラーが発生しました" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "最も低い売却限度額", "medium_sell_limit": "中くらいの売却限度額", "highest_sell_limit": "最も高い売却限度額", - "change": "Change", + "change": "変更", "continue_to_amount": "金額に進む", "no_payment_methods_title": "{{regionName}}で使用できる支払方法がありません", "no_cash_destinations_title": "{{regionName}}に現金の送金先がありません", @@ -5118,7 +5148,7 @@ "start_swapping": "スワップを開始" }, "feature_off_title": "一時的に利用できません", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "MetaMask Swapsはメンテナンス中です。後ほどご確認ください。", "wrong_network_title": "Swapsが利用できません", "wrong_network_body": "トークンのスワップはイーサリアムメインネットでのみ可能です。", "unallowed_asset_title": "このトークンはスワップできません", @@ -5160,7 +5190,7 @@ "not_enough": "このスワップを完了させるのに十分な{{symbol}}がありません", "max_slippage": "最大スリッページ", "max_slippage_amount": "最大スリッページ {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "発注から確定までの間にレートが変わることを「スリッページ」といいます。スリッページが「最大スリッページ」設定を超えた場合、スワップは自動的にキャンセルされます。", "slippage_warning": "仕組みをしっかりと把握してから実行しましょう。", "allows_up_to_decimals": "{{symbol}}は小数点以下{{decimals}}桁まで使用できます", "get_quotes": "クォートを入手", @@ -5199,7 +5229,7 @@ "edit": "編集", "quotes_include_fee": "クォートには{{fee}}%のMetaMask手数料が含まれています", "quotes_include_gas_and_metamask_fee": "クォートにはガス代と、{{fee}}%のMetaMask手数料が含まれています", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "タップしてスワップ", "swipe_to_swap": "スワイプしてスワップ", "swipe_to": "スワイプして", "swap": "スワップ", @@ -5259,7 +5289,7 @@ "approve": "{{sourceToken}}のスワップを{{upTo}}まで承認" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "スワップ保留中 ({{sourceToken}}から{{destinationToken}})", "swap_confirmed": "スワップ完了 ({{sourceToken}}から{{destinationToken}})", "approve_pending": "{{sourceToken}}のスワップを承認中", "approve_confirmed": "{{sourceToken}}のスワップが承認されました" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "ネットワークのドロップダウンをアセットに移動", "description_2": "スワップとブリッジが1つのシンプルなフローで完結", - "description_3": "Streamlined send experience", + "description_3": "送金エクスペリエンスを効率化", "description_4": "アカウントの表示を刷新" }, "more_information": "これで、ネットワークを気にすることなく、トークンや取引に集中できるようになりました。", @@ -5406,21 +5436,21 @@ "aggressive_label": "積極的", "aggressive_text": "変動の激しい市場でも確率が高くなります。人気のNFTドロップなどによるネットワークトラフィックの急増に備えるには、「積極的」を使用してください。", "market_label": "マーケット", - "market_text": "Use market for fast processing at current market price.", + "market_text": "現在の市場価格での迅速な処理には、「マーケット」を使用してください。", "low_label": "低", "low_text": "「低」を使用すると、価格が安くなるまで待ちます。価格が若干予想不能なため、予想時間は大幅に不正確になります。", "link": "ガス代のカスタマイズに関する詳細をご覧ください。" }, "save": "保存", "submit": "送信", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "現在のネットワークの状況に対して最大優先手数料が低めです", + "max_priority_fee_high": "最大優先手数料が必要以上に高くなっています", + "max_priority_fee_speed_up_low": "最大優先手数料は{{speed_up_floor_value}} gwei (初回トランザクションの10%増し) 以上必要です", + "max_priority_fee_cancel_low": "最大優先手数料は{{cancel_value}} gwei (初回トランザクションの50%増し) 以上必要です", + "max_fee_low": "現在のネットワークの状況に対して最大手数料が低めです", + "max_fee_high": "最大手数料が必要以上に高くなっています", + "max_fee_speed_up_low": "最大手数料は{{speed_up_floor_value}} gwei (初回トランザクションの10%増し) 以上必要です", + "max_fee_cancel_low": "最大手数料は{{cancel_value}} gwei (初回トランザクションの50%増し) 以上必要です", "learn_more_gas_limit": "ガスリミットは、使用しても構わないガスの最大単位数です。ガスの単位数は、「最大優先手数料」および「最大手数料」の乗数になります。", "learn_more_max_priority_fee": "最大優先手数料 (別名「マイナーチップ」) は、マイナーに直接支払われ、トランザクションを優先するインセンティブとなります。通常最大設定値が支払われます。", "learn_more_max_fee": "最大手数料は、支払う最高額です (基本料金 + 優先手数料)。", @@ -5530,9 +5560,9 @@ "enable_remember_me_description": "認証情報の保存を有効にすると、携帯にアクセスできる人は誰でもMetaMaskアカウントにアクセスできるようになります。" }, "turn_off_remember_me": { - "title": "認証情報の保存を無効にするには、パスワードを入力してください", - "placeholder": "パスワード", - "description": "このオプションを無効にすると、今後MetaMaskのロックを解除するのにパスワードが必要になります。", + "title": "認証情報の保存を無効にする", + "placeholder": "パスワードの確認", + "description": "無効にすると、再び認証情報の保存を使用することはできなくなります。この機能は廃止され、代わりにパスワードまたは生体認証を使用してMetaMaskのロックを解除できます。", "action": "認証情報の保存を無効にする" }, "dapp_connect": { @@ -5582,7 +5612,7 @@ "learn_more": "詳細" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "サードパーティの詳細の確認", "protect_from_scams": "詐欺から身を守るため、サードパーティの詳細を確認してください。", "learn_to_verify": "サードパーティの詳細の確認方法", "spending_cap": "使用上限", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "復元が必要です", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "問題が発生しましたが、ご心配なく!ウォレットを復元してみましょう。", "restore_needed_action": "ウォレットを復元" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Ledgerデバイスで実行中のアプリを閉じられませんでした。", "ethereum_app_not_installed": "イーサリアムアプリがインストールされていません。", "ethereum_app_not_installed_error": "Ledgerデバイスにイーサリアムアプリをインストールしてください。", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "イーサリアムアプリが開いていません", + "eth_app_not_open_message": "Ledgerデバイスでイーサリアムアプリを開いてください。", "ledger_is_locked": "Ledgerがロックされています", "unlock_ledger_message": "Ledgerデバイスのロックを解除してください。", "cannot_get_account": "アカウントを取得できません", @@ -5797,8 +5827,8 @@ "error_description": "{{snap}}のインストールに失敗しました。" }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "ウォレットから毎日請求できる年次ボーナス。", + "earn_a_percentage_bonus": "{{percentage}}%のボーナスを獲得", "claimable_bonus": "獲得できるボーナス", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "プロトコルからトークンを引き出してウォレットに戻すのにかかる時間です", "receive": "このトークンは資産と報酬を追跡するのに使用されます。資産が引き出せなくなるため、このトークンを送金したり取引したりしないでください。", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "健全性指標とは清算リスクを計測するものです", "above_two_dot_zero": "2.0超", "safe_position": "安全なポジション", "between_one_dot_five_and_2_dot_zero": "1.5~2.0の間", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "中程度の清算リスク", "below_one_dot_five": "1.5未満", "higher_liquidation_risk": "清算リスクが高め" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "残高を全額引き出せないのはなぜですか?", "your_withdrawal_amount_may_be_limited_by": "引き出し額は", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "プールの流動性", "not_enough_funds_available_in_the_lending_pool_right_now": "によって制限される場合があります。現在、レンディングプールに十分な資金がありません。", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "既存の借入ポジション", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "引き出しを行うと、既存の貸借ポジションが清算のリスクに晒される可能性があります。" } }, @@ -5998,11 +6028,11 @@ "earn_button": "獲得" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "TRXをステーキングして収益を獲得", + "stake_any_amount": "TRXはいくらでもステーキング可能。", "earn_trx_rewards": "TRXの報酬を獲得しましょう。", "earn_trx_rewards_description": "ステーキングすればすぐに収益が得られます。報酬は自動的に発生します。", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "いつでもステーキングを解除できます。解除の処理には14日間かかります。" }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "ガス代の見積もり", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "ガス代は、イーサリアムネットワークでトランザクションを処理するクリプトマイナーに支払われます。MetaMaskはガス代から利益を得ません。", "gas_fluctuation": "ガス代は、ネットワークトラフィックとトランザクションの複雑さに基づき見積もられ、変動します。", "gas_learn_more": "ガス代に関する詳細" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "サインイン方法", "spender": "使用者", "now": "現在", - "switching_to": "Switching to", + "switching_to": "切り替え先: ", "bridge_estimated_time": "推定所要時間", "pay_with": "支払方法:", - "receive_as": "Receive", + "receive_as": "受取", "total": "合計", - "you_receive": "You'll receive", + "you_receive": "受取額", "transaction_fee": "トランザクション手数料", - "transaction_fees": "Transaction fees", + "transaction_fees": "トランザクション手数料", "metamask_fee": "MetaMaskの手数料", "network_fee": "ネットワーク手数料", "bridge_fee": "ブリッジプロバイダー手数料" @@ -6234,7 +6264,7 @@ "transaction_fee": "Polygon (予測で使用されるネットワーク) 上でトークンをUSDCにスワップします。スワッププロバイダーは手数料を請求する場合がありますが、MetaMaskは無料です。" }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMaskはユーザーに代わり、希望のトークンをスワップします。MUSDへのスワップにはMetaMaskの手数料がかかりません。" }, "musd_conversion": { "transaction_fee": "mUSD換算手数料にはネットワーク費用が含まれ、プロバイダー手数料も含まれる場合があります。" @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "このサイトが署名を求めています", "transaction_tooltip": "このサイトがトランザクションを求めています", "details": "詳細", - "qr_get_sign": "Get signature", + "qr_get_sign": "署名を取得", "qr_scan_text": "ハードウェアウォレットでスキャン", "sign_with_ledger": "Ledgerで署名", "smart_account": "スマートアカウント", "smart_contract": "スマートコントラクト", - "standard_account": "Standard account", + "standard_account": "スタンダードアカウント", "siwe_message": { "url": "URL", "network": "ネットワーク", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "スマートアカウント", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "スタンダードアカウント", "switch": "切り替える", "switchBack": "元に戻す", "includes_transaction": "{{transactionCount}}件のトランザクションを含む", @@ -6307,9 +6337,9 @@ "cancel": "キャンセル", "description": "ご自身に代わって使われても良いと思う金額を入力してください。", "invalid_number_error": "支出上限は数字である必要があります", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "使用上限は空欄にできません", + "no_extra_decimals_error": "使用上限の小数点以下の桁数は、トークンの桁数を超えることができません", + "no_zero_error": "使用上限は0にできません", "no_zero_error_decrease_allowance": "支出上限を0にした場合、'decreaseAllowance'メソッドの効果はありません", "no_zero_error_increase_allowance": "支出上限を0にした場合、'increaseAllowance'メソッドの効果はありません", "save": "保存", @@ -6336,7 +6366,7 @@ "transferRequest": "送金リクエスト", "nested_transaction_heading": "トランザクション {{index}}", "transaction": "トランザクション", - "available_balance": "Available balance: ", + "available_balance": "利用可能残高: ", "edit_amount_done": "続行", "deposit_edit_amount_done": "資金を追加", "deposit_edit_amount_predict_withdraw": "出金", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "利用規約", "select_token": "トークンを選択", "no_tokens_found": "トークンが見つかりませんでした", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "この名前のトークンが見つかりませんでした。別の検索をお試しください。", "select_network": "ネットワークを選択", "all_networks": "すべてのネットワーク", "num_networks": "{{numNetworks}}個のネットワーク", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "すべて選択解除", "see_all": "すべて表示", "all": "すべて", - "more_networks": "+{{count}} more", + "more_networks": "他{{count}}件", "apply": "適用", "slippage": "スリッページ", "slippage_info": "発注から確定までの間に価格が変わることを「スリッページ」といいます。スリッページがここで設定された許容範囲を超えた場合、スワップは自動的にキャンセルされます。", @@ -6392,7 +6422,7 @@ "quote_info_title": "レート", "network_fee_info_title": "ネットワーク手数料", "network_fee_info_content": "ネットワーク手数料は、ネットワークの混雑状況とトランザクションの複雑さによって異なります。", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "このネットワーク手数料はMetaMaskにより支払われるため、ユーザーのアカウントに{{nativeToken}}がなくてもトランザクションが可能です。", "points": "推定ポイント数", "points_tooltip": "ポイント", "points_tooltip_content_1": "ポイントは、スワップ、ブリッジ、パーペチュアル取引など、トランザクションを完了したことによるMetaMaskリワードの獲得状況を示します。", @@ -6406,7 +6436,7 @@ "select_recipient": "受取人を選択してください", "external_account": "外部アカウント", "error_banner_description": "この取引ルートは現在使用できません。金額、ネットワーク、またはトークンを変更してみてください。最善のオプションを探します。", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "この取引ルートは現在使用できません。金額、ネットワーク、またはトークンを変更してみてください。最適なオプションを検索します。", "insufficient_funds": "資金不足", "insufficient_gas": "ガス不足", "select_amount": "アカウントを選択", @@ -6417,9 +6447,9 @@ "title": "ブリッジ", "submitting_transaction": "送信中", "fetching_quote": "クォートを取得中", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "{{feePercentage}}%のMetaMask手数料を含みます。", "no_mm_fee": "MM手数料なし", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "{{destTokenSymbol}}へのスワップにMetaMask手数料はかかりません。", "hardware_wallet_not_supported": "まだハードウェアウォレットに対応していません。続行するにはホットウォレットをご使用ください。", "hardware_wallet_not_supported_solana": "Solanaはまだハードウェアウォレットに対応していません。続行するにはホットウォレットをご使用ください。", "price_impact_info_title": "プライスインパクト", @@ -6432,17 +6462,24 @@ "approval_needed": "トークンのスワップを承認します。", "approval_tooltip_title": "アクセス許可は正確に", "approval_tooltip_content": "指定された金額({{amount}} {{symbol}})へのアクセスを許可します。コントラクトは追加の資金にアクセスしません。", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "最低受取額", + "minimum_received_tooltip_title": "最低受取額", "minimum_received_tooltip_content": "トランザクションの処理中に価格が変動した場合に、お客様のスリッページ許容値に基づいて受け取る最低額です。これは流動性プロバイダーからの見積もりであり、最終的な金額は異なる場合があります。", + "market_closed": { + "title": "市場が取引時間外です", + "description": "このトークンをサポートしている市場は、現在取引時間外です。トークンはいつでもオンチェーンで送金できます。", + "learn_more": "詳細", + "learn_more_url": "https://status.ondo.finance/market", + "done": "完了" + }, "submit": "送信", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "価格がスリッページの割合を超えて変動した場合、トランザクションは処理されません。", "cancel": "キャンセル", "confirm": "確定", "exceeding_upper_slippage_warning": "高スリッページ、これにより不利なスワップになる可能性があります", "exceeding_lower_slippage_warning": "低スリッページ、これにより不利なスワップになる可能性があります", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "{{value}}%より大きい値を入力してください", + "exceeding_upper_slippage_error": "{{value}}%より大きい値を入力することはできません", "custom": "カスタム" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "ウォレットの復元", "login_with_social": "ソーシャルアカウントでログイン", "setup": "設定", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "シークレットリカバリーフレーズ {{num}}", "back_up": "バックアップ", "reveal": "確認", "social_recovery_title": "{{authConnection}}での復元", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "パスワードを入力してください", "description": "カード情報を表示するには、ウォレットのパスワードを入力してください。", + "description_unfreeze": "カードでの支払いを再開するには、ウォレットのパスワードを入力してください。", "placeholder": "パスワード", "confirm": "確定", "cancel": "キャンセル", @@ -7001,6 +7039,7 @@ "enable_card_error": "カードを有効化できませんでした。後ほどもう一度お試しください。", "view_card_details_error": "カード情報を読み込めません。もう一度お試しください。", "biometric_verification_required": "カード情報を表示するには認証が必要です。", + "unfreeze_auth_required": "カードでの支払いを再開するには、認証が必要です。", "warnings": { "close_spending_limit": { "title": "利用限度額に近づいています", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "カードが凍結されています", - "description": "カードの凍結を解除するにはサポートにお問い合わせください" + "description": "お客様のカードは一時的に凍結されています。凍結はいつでも解除できます。" }, "blocked": { "title": "カードがロックされています", @@ -7068,7 +7107,14 @@ "travel_description": "最大70%割引でホテルを予約", "card_tos_title": "利用規約", "order_metal_card": "メタルカード", - "order_metal_card_description": "実物のメタルカードを今すぐ注文" + "order_metal_card_description": "実物のメタルカードを今すぐ注文", + "freeze_card": "カードを凍結する", + "unfreeze_card": "カードの凍結を解除する", + "freeze_card_description": "カードでのすべての支払いを一時停止する", + "unfreeze_card_description": "カードでのすべての支払いを再開する", + "freeze_error": "カードステータスの更新に失敗しました。もう一度お試しください。", + "freeze_success": "カードが凍結されました", + "unfreeze_success": "カードの凍結が解除されました" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "{{seconds}}秒後に再送できます" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "{{walletName}}に追加", + "adding_to_wallet": "{{walletName}}に追加しています...", + "continue_setup": "{{walletName}}の設定を続ける", + "wallet_not_available": "{{walletName}}が利用できません", + "already_in_wallet": "すでに{{walletName}}に追加されています", + "success_title": "カードが追加されました!", + "success_message": "MetaMaskカードが{{walletName}}に追加されました。", + "error_title": "カードを追加できません", + "error_wallet_not_available": "{{walletName}}はこのデバイスで使用できません。{{walletName}}が設定されていることを確認してください。", + "error_wallet_not_initialized": "{{walletName}}が初期化されていません。ウォレットを設定してもう一度お試しください。", "error_card_already_in_wallet": "このカードはすでに{{walletName}}に追加されています。", "error_card_pending": "お客様のカードは{{walletName}}でセットアップ中です。数分後にもう一度ご確認ください。", "error_card_suspended": "{{walletName}}のカードは利用停止になっています。サポートにお問い合わせください。", "error_card_not_eligible": "このカードは、モバイルウォレットプロビジョニングの対象外です。", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "カードデータの暗号化に失敗しました。もう一度お試しください。", "error_invalid_card_data": "カードデータが無効です。カード情報を確認してもう一度お試しください。", "error_card_not_found": "カードが見つかりません。もう一度お試しください。", "error_card_provider_not_found": "お住いの地域でカードプロバイダーが利用できません。", "error_card_id_mismatch": "カードの検証に失敗しました。もう一度お試しください。", "error_card_not_active": "お客様のカードは有効ではありません。まずはカードを有効にしてください。", "error_network": "ネットワークエラーが発生しました。接続を確認して再試行してください。", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "リクエストがタイムアウトしました。もう一度お試しください。", + "error_server": "サーバーエラーが発生しました。後ほどもう一度お試しください。", + "error_unknown": "予期せぬエラーが発生しました。もう一度試すか、サポートにお問い合わせください。", + "error_platform_not_supported": "このプラットフォームは、モバイルウォレットプロビジョニングをサポートしていません。", "try_again": "再試行してください", "cancel": "キャンセル" } @@ -7299,7 +7345,7 @@ "main_title": "報酬", "referral_title": "紹介", "tab_overview_title": "概要", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "スナップショット", "tab_activity_title": "アクティビティ", "referral_stats_earned_from_referrals": "紹介して報酬を獲得", "referral_stats_referrals": "紹介", @@ -7353,7 +7399,7 @@ "verifying_rewards": "リワードを獲得する前に、情報がすべて正しいことを確認しています。" }, "season_status": { - "points_earned": "Points earned" + "points_earned": "ポイント獲得" }, "onboarding": { "not_supported_region_title": "未対応の地域です", @@ -7431,7 +7477,7 @@ "show_less": "表示を戻す", "linking_progress": "アカウントを追加中… ({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}}件を登録済み", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "すべてのアカウントを追加" }, "referred_by_code": { "title": "紹介コード", @@ -7514,7 +7560,7 @@ "claim_label": "請求", "claimed_label": "請求済み", "reward_claimed": "リワードを請求しました", - "time_left": "{{time}} left", + "time_left": "残り{{time}}", "expired": "期限切れ" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "引き換えに失敗しました", "redeem_failure_description": "後ほどもう一度お試しください。", "reward_details": "報酬の詳細", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "このリワードを送る先のアカウントを選択してください。" }, "animation": { "could_not_load": "読み込ませんでした" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", + "starts_date": "{{date}}開始", + "ends_date": "{{date}}終了", + "results_coming_soon": "間もなく結果が出ます", + "tokens_on_the_way": "トークンを送金中です", + "pill_up_next": "次", + "pill_live_now": "現在進行中", "pill_calculating": "計算中", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "pill_results_ready": "結果が出ました", + "pill_complete": "完了" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "スナップショット", + "error_title": "スナップショットを読み込めません", + "error_description": "スナップショットを読み込めませんでした。もう一度お試しください。", "retry_button": "再試行" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "アクティブ", + "upcoming_title": "今後", + "previous_title": "以前", + "empty_state": "利用可能なスナップショットがありません", + "error_title": "スナップショットを読み込めません", + "error_description": "スナップショットを読み込めませんでした。もう一度お試しください。", "retry_button": "再試行", - "refreshing": "Refreshing..." + "refreshing": "更新中..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "{{approveSymbol}}を承認", "bridge_approval_loading": "承認", "bridge_send": "{{sourceChain}}から{{sourceSymbol}}をブリッジ", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "ブリッジ送信", "bridge_receive": "{{targetChain}}で{{targetSymbol}}を受け取る", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "ブリッジ受取", "default": "トランザクション", "musd_convert_send": "{{sourceChain}}から{{sourceSymbol}}を送金", "musd_claim": "mUSDの請求", @@ -7607,20 +7653,20 @@ "description": "{{dappName}}との接続を確立しています...\n" }, "show_error": { - "title": "Connection error", + "title": "接続エラー", "description": "接続の確立に失敗しました。もう一度お試しください。" }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "承認が拒否されました", + "description": "ユーザーがリクエストを拒否しました。" }, "show_return_to_app": { "title": "成功", "description": "アプリに戻って続行します。" }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "接続が見つかりません", + "description": "続行するには、アプリから新しい接続を確立させてください。" } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "閲覧", + "trending_tokens": "流行のトークン", "price_change": "価格変動", "all_networks": "すべてのネットワーク", - "24h": "24h", + "24h": "24時間", "time": "時間", "24_hours": "24時間", "6_hours": "6時間", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1時間", + "5_minutes": "5分", "networks": "ネットワーク", "sort_by": "並べ替え基準", "volume": "取引量", @@ -7650,32 +7696,48 @@ "high_to_low": "高い順", "low_to_high": "低い順", "apply": "適用", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "トークン、サイト、URLを検索", "cancel": "キャンセル", "perps": "パーペチュアル", "predictions": "予測", - "no_results": "No results found", + "no_results": "結果が見つかりませんでした", "sites": "サイト", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "人気のサイト", + "search_sites": "サイトを検索", + "enable_basic_functionality": "基本機能を有効にする", + "basic_functionality_disabled_title": "閲覧を利用できません", + "basic_functionality_disabled_description": "基本機能が無効になっていると、必要なメタデータを取得できません。", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "流行のトークンが利用できません", + "description": "現在このページを取得できません", "try_again": "再試行してください" }, "empty_search_result_state": { "title": "トークンが見つかりませんでした", - "description": "We were not able to find this token" + "description": "このトークンを見つけることができませんでした" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "アップデートの準備ができました", + "description_ios": "重要な修正が行われました。再読み込みして、MetaMaskの最新バージョンをご利用ください。", + "description_android": "重要な修正が行われました。MetaMaskを閉じてからもう一度開いて、アップデートを適用してください。", "primary_action_reload": "再読み込み", "primary_action_acknowledge": "了解" + }, + "homepage": { + "sections": { + "tokens": "トークン", + "perpetuals": "パーペチュアル", + "predictions": "予測", + "defi": "DeFi", + "nfts": "NFT", + "import_nfts": "NFTをインポート", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/ko.json b/locales/languages/ko.json index c8ea77b5224..b15b5245bee 100644 --- a/locales/languages/ko.json +++ b/locales/languages/ko.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "지갑 데이터가", "reset_wallet_desc_2": "영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다.", "reset_wallet_desc_login": "지갑을 복구하려면 비밀복구구문, Google 계정 비밀번호, Apple 계정 비밀번호를 사용할 수 있습니다. MetaMask는 이 정보를 보관하지 않습니다.", - "reset_wallet_desc_srp": "지갑을 복구하려면 비밀복구구문이 있어야 합니다. MetaMask는 이 정보를 보관하지 않습니다." + "reset_wallet_desc_srp": "지갑을 복구하려면 비밀복구구문이 있어야 합니다. MetaMask는 이 정보를 보관하지 않습니다.", + "biometric_authentication_cancelled": "바이오메트릭 인증 취소", + "biometric_authentication_cancelled_title": "바이오메트릭 설정 실패", + "biometric_authentication_cancelled_description": "설정에서 바이오메트릭 인증을 다시 설정해 주세요.", + "biometric_authentication_cancelled_button": "컨펌" }, "connect_hardware": { "title_select_hardware": "하드웨어 지갑 연결", @@ -1040,7 +1044,7 @@ "title": "예치 금액", "get_usdc_hyperliquid": "USDC 받기 • Hyperliquid", "insufficient_funds": "자금 부족", - "no_funds_available": "사용 가능한 자금이 없습니다. 먼저 입금하세요.", + "no_funds_available": "사용 가능한 자금이 부족합니다. 자금을 입금하거나 다른 결제 수단을 선택해 주세요", "enter_amount": "금액 입력", "fetching_quote": "견적 가져오기", "submitting": "트랜잭션 제출 중", @@ -1970,8 +1974,8 @@ "trade_again": "다시 거래", "activity": { "deposit_title": "예치", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "{{amount}} {{symbol}} 예치됨", + "withdrew_amount": "{{amount}} {{symbol}} 출금됨", "status_completed": "완료", "status_failed": "실패", "status_pending": "보류 중" @@ -2051,6 +2055,16 @@ "referral_code_text": "추천 코드를 사용하여 추가 리워드를 받으세요." } }, + "market_insights": { + "title": "시장 인사이트", + "updated_ago": "{{time}} 업데이트됨", + "disclaimer": "AI 인사이트. 투자 조언이 아닙니다.", + "whats_driving_price": "가격 변동 원인은 무엇인가요?", + "what_people_saying": "커뮤니티 의견", + "trade_button": "거래하기", + "sources_count": "출처 {{count}}개 이상", + "sources_title": "출처에서 얻으세요" + }, "predict": { "title": "MetaMask 예측", "prediction_markets": "예측 시장", @@ -2384,8 +2398,8 @@ "no_available_tokens": "토큰이 보이지 않나요?", "add_tokens": "토큰 가져오기", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "계정에서 {{tokenCount}}개의 신규 {{tokensLabel}} 발견됨", "token_toast": { "tokens_imported_title": "가져온 토큰", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "토큰 십진수는 비워둘 수 없습니다.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "해당 이름의 토큰을 찾을 수 없습니다.", + "tokens_empty_description": "Search for any token and import it", "select_token": "토큰 선택", "address_must_be_smart_contract": "개인 주소가 탐지되었습니다. 토큰 거래 주소를 입력하세요.", "billion_abbreviation": "B", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "모든 계정 연결 해제", "deceptive_site_ahead": "의심스러운 사이트 주의", "deceptive_site_desc": "방문하려는 사이트가 안전하지 않습니다. 공격자가 위험한 작업을 하도록 유도할 수 있습니다.", + "malicious_site_detected": "악성 사이트 감지됨", + "malicious_site_warning": "이 사이트에 연결하면 모든 자산을 잃을 수 있습니다.", + "connect_anyway": "연결 계속", "learn_more": "더 보기", "advisory_by": "Ethereum Phishing Detector와 PhishFort에서 자문 제공", "potential_threat": "다음과 같은 잠재적 위협이 있습니다.", @@ -2846,7 +2864,11 @@ "permissions": "권한", "card_title": "MetaMask 카드", "settings": "설정", - "log_out": "로그아웃" + "networks": "네트워크", + "log_out": "로그아웃", + "notifications": "지갑 사용", + "buy": "매수", + "scan": "스캔" }, "app_settings": { "enabling_notifications": "알림 활성화 중...", @@ -2870,6 +2892,8 @@ "state_logs": "상태 로그", "add_network_title": "네트워크 추가", "auto_lock": "자동 잠금", + "enable_device_authentication": "장치 인증 활성화", + "enable_device_authentication_desc": "장치의 바이오메트릭 또는 암호를 사용해 MetaMask를 잠금 해제하세요.", "auto_lock_desc": "앱이 자동으로 잠기기 전까지 걸리는 시간을 선택하세요.", "state_logs_desc": "이렇게 하면 MetaMask에서 발생할 수 있는 문제를 디버깅하는 데 도움이 됩니다. 햄버거 아이콘 > 피드백 보내기로 문의하시거나 또는 기존 문의 티켓이 있으면, 해당 티켓으로 회신하여 MetaMask 지원 서비스로 보내주시기 바랍니다.", "autolock_immediately": "즉시", @@ -2975,6 +2999,11 @@ "add_rpc_url": "RPC URL 추가", "add_block_explorer_url": "블록 탐색기 URL 추가", "networks_desc": "맞춤 RPC 네트워크 추가 및 수정", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "네트워크 검색", + "networks_no_results": "네트워크를 찾을 수 없습니다", "network_name_label": "네트워크 이름", "network_name_placeholder": "네트워크 이름(옵션)", "network_rpc_url_label": "RPC URL", @@ -2991,7 +3020,16 @@ "network_other_networks": "다른 네트워크", "network_rpc_networks": "RPC 네트워크", "network_add_network": "네트워크 추가", + "add_chain_title": "네트워크 추가", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "다시 시도", + "add_chain_added": "Added", + "add_chain_or": "또는", + "add_chain_custom_link": "사용자 지정 네트워크 추가", "network_add_custom_network": "사용자 지정 네트워크 추가", + "network_add_test_network": "Add a test network", "network_add": "추가", "network_save": "저장", "remove_network_title": "해당 네트워크를 제거하시겠습니까?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "확인", "title": "계정에 연결할 수 없습니다", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "MetaMask에 다시 연결하려면 사이트의 QR 코드를 스캔해 주세요" }, "app_information": { "title": "정보", @@ -3379,6 +3417,7 @@ "sell_description": "암호화폐를 현금으로 매도" }, "asset_overview": { + "market_closed": "시장 마감", "send_button": "보내기", "buy_button": "구매", "cash_buy_button": "현금 매수", @@ -3399,19 +3438,6 @@ "bridge": "브릿지", "earn": "수익 창출", "convert_to_musd": "mUSD로 전환", - "merkl_rewards": { - "annual_bonus": "{{apy}}% 보너스", - "claimable_bonus": "청구 가능한 보너스", - "claimable_bonus_tooltip_description": "mUSD 보너스는 Linea에서 청구할 수 있습니다.", - "terms_apply": "약관이 적용됩니다.", - "ok": "확인", - "claim": "청구", - "processing_claim": "청구 진행 중...", - "claim_on_linea_title": "Linea에서 보너스 수령", - "claim_on_linea_description": "보너스는 Ethereum mUSD 잔액과 별도로 Linea에서 지급됩니다.", - "continue": "계속", - "unexpected_error": "예기치 못한 오류가 발생했습니다. 다시 시도하세요." - }, "tron": { "daily_resource_new_energy": "신규 일일 에너지", "sufficient_to_cover": "잔액 충분", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "토큰 주소가 클립보드에 복사됨" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "잘못된 QR 코드", "invalid_qr_code_message": "스캔하려 하는 QR 코드가 유효하지 않습니다.", "allow_camera_dialog_title": "카메라 접근 허용", "allow_camera_dialog_message": "QR 코드를 스캔하기 위해서는 권한이 필요합니다", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "확장 프로그램과의 동기화를 시도하시는 것 같습니다. 이를 위해서는 현재 지갑을 지워야 합니다. \n\n 신규 버전 앱을 지우거나 재설치하고 나면 \"MetaMask 확장 프로그램과 동기화\" 옵션을 선택하세요. 중요!: 지갑을 지우기 전에 비밀 복구 구문을 반드시 백업하세요.", "not_allowed_error_title": "카메라 접근 권한 허용", "not_allowed_error_desc": "QR 코드를 스캔하려면 기기 설정 메뉴에서 MetaMask에 카메라 접근 권한을 허용해야 합니다.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "읽을 수 없는 QR 코드", "unrecognized_address_qr_code_desc": "죄송합니다. 이 QR 코드는 계정 주소나 연락처 주소와 연계되어 있지 않습니다.", "url_redirection_alert_title": "이제 외부 링크로 이동합니다", "url_redirection_alert_desc": "링크는 피싱이나 사기에 도용될 수 있습니다. 신뢰할 수 있는 웹사이트만 방문하시기 바랍니다.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "이 콜렉티블을 소유하고 있지 않습니다", "known_asset_contract": "확인된 자산 계약 주소", "max": "최대", - "recipient_address": "Recipient address", + "recipient_address": "받는 주소", "required": "필수", "to": "수신:", "total": "총합", @@ -3641,7 +3667,7 @@ "nevermind": "취소", "edit_network_fee": "가스요금 수정", "edit_priority": "우선 순위 편집", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "가스 취소 수수료", "gas_speedup_fee": "가스 속도 향상 비용", "use_max": "최대 사용", "set_gas": "설정", @@ -3650,7 +3676,7 @@ "transaction_fee": "가스요금", "transaction_fee_less": "요금 없음", "total_amount": "총 금액", - "view_data": "View data", + "view_data": "데이터 보기", "adjust_transaction_fee": "거래 수수료 조정", "could_not_resolve_ens": "ENS를 확인할 수 없습니다", "asset": "자산", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "탈중앙화된 웹에서 브라우징을 할 수 있도록 새 탭을 추가하세요", "got_it": "컨펌", "max_tabs_title": "탭 최대 수에 도달했습니다", - "max_tabs_desc": "현재 한 번에 최대 5개의 탭만 열 수 있습니다. 새 탭을 추가하려면 기존 탭을 닫아주세요.", + "max_tabs_desc": "현재 한 번에 최대 20개의 탭만 열 수 있습니다. 새 탭을 추가하려면 기존 탭을 닫아주세요.", "failed_to_resolve_ens_name": "해당 ENS 이름을 확인할 수 없습니다", "remove_bookmark_title": "즐겨찾기 제거", "remove_bookmark_msg": "즐겨찾기 목록에서 이 사이트를 정말 제거하기 원하십니까?", @@ -3828,7 +3854,7 @@ "cancel_button": "취소" }, "approval": { - "title": "Confirm transaction" + "title": "트랜잭션 컨펌" }, "approve": { "title": "승인", @@ -3839,39 +3865,39 @@ "unavailable": "사용 불가", "tx_review_confirm": "확인", "tx_review_transfer": "송금", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "계약 배포", + "tx_review_transfer_from": "송금 출처:", + "tx_review_unknown": "알 수 없는 방법", "tx_review_approve": "승인", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "한도 증가", + "tx_review_set_approval_for_all": "모두 승인 설정", + "tx_review_staking_claim": "스테이킹 청구", "tx_review_staking_deposit": "스테이킹 예치", "tx_review_staking_unstake": "언스테이크", "tx_review_lending_deposit": "대출 예치", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "대출 출금", "tx_review_perps_deposit": "무기한 선물 입금 완료", "tx_review_predict_deposit": "예측 자금 충전 완료", "tx_review_predict_claim": "수익금 수령 완료", "tx_review_predict_withdraw": "예측 자금 출금", "tx_review_musd_conversion": "mUSD 전환", "claim": "청구", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "ETH 보냄", + "self_sent_ether": "나에게 보낸 ETH", + "received_ether": "받은 ETH", "sent_dai": "보낸 DAI", "self_sent_dai": "나에게 보낸 DAI", "received_dai": "받은 DAI", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "보낸 토큰", + "received_tokens": "받은 토큰", "ether": "ETH", "sent_unit": "보낸 {{unit}}", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "나에게 보낸 {{unit}}", "received_unit": "받은 {{unit}}", "sent_collectible": "보낸 컬렉터블", "received_collectible": "받은 컬렉터블", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "ETH 전송", + "send_unit": "{{unit}} 전송", "send_collectible": "컬렉터블 보내기", "receive_collectible": "컬렉터블 받기", "sent": "보낸", @@ -3881,17 +3907,17 @@ "send": "전송", "redeposit": "재예치", "interaction": "상호작용", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "계약 배포", + "to_contract": "신규 계약", + "mint": "민트", "tx_details_free": "수수료", "tx_details_not_available": "이용할 수 없음", "smart_contract_interaction": "스마트 계약 인터렉션", "swaps_transaction": "스와프 트랜잭션", "bridge_transaction": "브릿지", "approve": "승인", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "한도 증가", + "set_approval_for_all": "모두 승인 설정", "hash": "해시", "from": "보내는 사람", "to": "수신:", @@ -3899,15 +3925,15 @@ "amount": "금액", "fee": { "transaction_fee_in_ether": "트랜잭션 수수료", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "트랜잭션 수수료(USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "사용한 가스(단위)", + "gas_limit": "가스 한도(단위)", + "gas_price": "가스 가격(GWEI)", + "base_fee": "기본 요금(GWEI)", + "priority_fee": "우선 요금(GWEI)", "multichain_priority_fee": "우선 요금", - "max_fee": "Max fee per gas", + "max_fee": "가스당 최대 수수료", "total": "총", "view_on": "다음에서 보기:", "view_on_etherscan": "이더스캔에서 보기", @@ -3923,13 +3949,13 @@ "nonce": "논스", "from_device_label": "이 기기에서 진행", "import_wallet_row": "이 기기에 추가된 계정", - "import_wallet_label": "Account added", + "import_wallet_label": "추가된 계정", "import_wallet_tip": "이 기기에서 진행된 모든 향후 거래에는 \"이 기기에서 진행\"이라는 라벨이 타임스탬프 옆에 추가됩니다. 계정이 추가되기 전 날짜에 발생한 거래에 대해서는 기록에 어떤 거래가 이 기기에서 진행되었는지 표시되지 않습니다.", "sign_title_scan": "스캔 ", "sign_title_device": "하드웨어 지갑으로", "sign_description_1": "하드웨어 지갑으로 서명한 후,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "서명 받기를 탭하세요", + "sign_get_signature": "서명 받기", "transaction_id": "트랜잭션 ID", "network": "네트워크", "request_from": "요청자:", @@ -4032,7 +4058,7 @@ "title": "네트워크", "other_networks": "다른 네트워크", "close": "종료", - "status_ok": "All systems operational", + "status_ok": "모든 시스템 작동 중", "status_not_ok": "네트워크에 문제가 있습니다", "want_to_add_network": "이 네트워크를 추가하시겠습니까?", "add_custom_network": "사용자 지정 네트워크 추가", @@ -4051,7 +4077,7 @@ "review": "검토", "view_details": "세부 사항 확인", "network_details": "네트워크 세부 사항", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "확인을 선택하면 네트워크 세부 정보 확인이 활성화됩니다. 다음에서 네트워크 세부 정보 확인을 끌 수 있습니다: ", "network_settings_security_privacy": "IPFS 레졸루션을", "network_currency_symbol": "통화 기호", "network_block_explorer_url": "블록 탐색기 URL", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "악성 네트워크 공급업체는 블록체인 상태를 거짓으로 보고하고 네트워크 활동을 기록할 수 있습니다. 신뢰하는 커스텀 네트워크만 추가하세요.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "네트워크 정보", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "추가 네트워크 정보", "network_warning_desc": "이 네트워크 연결은 타사 서비스를 이용합니다. 연결의 신뢰성이 낮거나 타사가 활동을 추적할 수 있습니다.", "additonial_network_information_desc": "이러한 네트워크 중 일부는 제삼자에 의존합니다. 이러한 연결은 안정성이 떨어지거나 제삼자가 활동을 추적할 수 있습니다.", "connect_more_networks": "더 많은 네트워크 연결", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "이 네트워크는 더 이상 지원되지 않습니다", "network_deprecated_description": "연결하려는 네트워크는 MetaMask에서 더 이상 지원되지 않습니다.", "edit_networks_title": "네트워크 편집", - "no_network_fee": "No network fee" + "no_network_fee": "네트워크 수수료 없음" }, "permissions": { "title_this_site_wants_to": "이 사이트에서 요청하는 사항:", @@ -4111,11 +4137,11 @@ "network_connected": "네트워크 연결됨", "see_your_accounts": "계정 보기 및 트랜잭션 제안", "connected_to": "다음에 연결됨: ", - "manage_permissions": "Manage permissions", + "manage_permissions": "권한 관리", "edit": "편집", "cancel": "취소", "got_it": "컨펌", - "connection_details_title": "Connection details", + "connection_details_title": "연결 세부 정보", "connection_details_description": "{{connectionDateTime}}에 MetaMask 브라우저를 이용해 이 사이트에 접속했습니다", "title_add_network_permission": "네트워크 권한 추가", "add_this_network": "이 네트워크 추가", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "기기 PIN으로 잠금 해제하시겠습니까?" }, "authentication": { - "auth_prompt_title": "인증 필요", - "auth_prompt_desc": "MetaMask를 사용하려면 인증을 받으세요", - "fingerprint_prompt_title": "인증 필요", - "fingerprint_prompt_desc": "지문으로 MetaMask 잠금을 해제하세요", - "fingerprint_prompt_cancel": "취소" + "auth_prompt_desc": "MetaMask를 사용하려면 인증을 받으세요" }, "accountApproval": { "title": "연결 요청", "walletconnect_title": "지갑 연결 요청", "action": "이 사이트로 연결하시겠습니까?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "연결을 재개하려면 사이트에 표시된 번호를 선택하세요", + "action_reconnect_deeplink": "이 사이트에 다시 연결하시겠습니까?", "connect": "연결", "resume": "다시 시작", "cancel": "취소", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "이 사이트 연결을 기억하지 않기", "disconnect": "연결 해제", "permission": "귀하의", "address": "공개 주소", @@ -4218,7 +4240,7 @@ "error_title": "문제가 발생했습니다", "error_message": "개인 키를 불러올 수 없었습니다. 키를 정확히 입력했는지 확인하세요.", "error_empty_message": "개인 키를 입력해야 합니다.", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "또는 QR 코드를 스캔하세요" }, "import_private_key_success": { "title": "계정을 성공적으로 불러왔습니다!", @@ -4229,18 +4251,18 @@ "import_wallet_title": "지갑 가져오기", "enter_srp_subtitle": "비밀복구구문을 입력하세요", "textarea_placeholder": "각 단어 사이에 공백을 넣고 다른 사람이 보지 못하게 하세요", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "지갑의 비밀복구구문을 입력하세요. 이더리움, 솔라나, 비트코인의 비밀복구구문을 가져올 수 있습니다.", + "subtitle": "비밀복구구문을 붙여넣으세요", "cta_text": "계속", "paste": "붙여넣기", "clear": "모두 지우기", "srp_number_of_words_option_title": "단어 수", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "12단어 구문이 있습니다", + "24_word_option": "24단어 구문이 있습니다", "error_title": "문제가 발생했습니다", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "해당 비밀복구구문을 가져올 수 없습니다. 정확히 입력했는지 확인하세요.", + "error_empty_message": "비밀복구구문을 입력해야 합니다.", + "error_number_of_words_error_message": "비밀복구구문은 12개 또는 24개의 단어로 구성됩니다", "error_srp_is_case_sensitive": "잘못된 입력입니다! 비밀복구구문은 대소문자가 구별되어야 합니다.", "error_srp_word_error_1": "단어 ", "error_srp_word_error_2": " 이(가) 잘못되었거나 철자가 틀렸습니다.", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " 이(가) 잘못되었거나 철자가 틀렸습니다.", "error_invalid_srp": "잘못된 비밀복구구문", "error_duplicate_srp": "이 비밀복구구문은 이미 가져왔습니다.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "가져오려는 계정이 이미 존재합니다.", + "invalid_qr_code_title": "잘못된 QR 코드", + "invalid_qr_code_message": "QR 코드에 유효한 비밀복구구문이 포함되어 있지 않습니다", "success_1": "지갑", "success_2": "가져옴" }, @@ -4665,7 +4687,7 @@ "button": "지갑 보호하기" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "트랜잭션 업데이트 실패", "text": "다시 시도하시겠습니까?", "cancel_button": "취소", "retry_button": "재시도" @@ -4684,13 +4706,13 @@ "next": "다음", "amount_placeholder": "0.00", "link_copied": "링크가 클립보드에 복사되었습니다", - "send_link_title": "Send link", + "send_link_title": "링크 전송", "description_1": "요청 링크를 전송할 준비가 되었습니다!", "description_2": "이 링크를 친구에게 보내세요. 그러면 링크에서 친구에게 보내라고 요청할 것입니다", "copy_to_clipboard": "클립보드로 복사", "qr_code": "QR 코드", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "링크 전송", + "request_qr_code": "결제 요청 QR 코드", "balance": "잔고" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "현재 활성화된 세션이 없습니다", - "end_session_title": "End session", + "end_session_title": "세션 종료", "end": "종료", "cancel": "취소", - "session_ended_title": "Session ended", + "session_ended_title": "세션이 종료되었습니다", "session_ended_desc": "선택한 세션은 종료되었습니다", "session_already_exist": "이 세션은 이미 연결되어 있습니다.", "close_current_session": "새 세션을 시작하기 전에 현재 세션을 닫아야 합니다." @@ -4765,15 +4787,14 @@ "on_network": "{{networkName}} 네트워크", "debit_card": "직불 카드", "select_payment_method": "결제 방법 선택", - "loading_quote": "Loading quote...", "pay_with": "결제 수단:", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "{{providerName}}을(를) 통해 구매", + "change_provider": "공급자를 변경하세요.", "payment_error": "오류가 발생했습니다. 다시 시도해 주세요.", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "사용 가능한 결제 방법이 없습니다.", "error_fetching_quotes": "오류가 발생했습니다. 다시 시도해 주세요.", - "no_quotes_available": "No providers available.", - "providers": "Providers", + "no_quotes_available": "공급자가 없습니다.", + "providers": "공급자", "continue": "계속", "powered_by_provider": "제공자: {{provider}}", "purchased_currency": "구매한 {{currency}}", @@ -4871,6 +4892,15 @@ "log_out": "{{provider}}에서 로그아웃", "logged_out_success": "로그아웃했습니다", "logged_out_error": "로그아웃 오류" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "최저 매도 한도", "medium_sell_limit": "중간 매도 한도", "highest_sell_limit": "최고 매도 한도", - "change": "Change", + "change": "변경", "continue_to_amount": "다음 금액으로 계속:", "no_payment_methods_title": "{{regionName}} 지역 결제 방법 없음", "no_cash_destinations_title": "{{regionName}} 지역 현금 송금처 없음", @@ -5118,7 +5148,7 @@ "start_swapping": "스와프 시작" }, "feature_off_title": "일시적으로 이용이 불가합니다", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "MetaMask 스왑은 현재 유지 보수 중입니다. 나중에 다시 확인해 주세요.", "wrong_network_title": "스와프 이용 불가", "wrong_network_body": "이더리움 메인 네트워크에서만 토큰 스와프가 가능합니다.", "unallowed_asset_title": "이 토큰은 스와프할 수 없습니다.", @@ -5160,7 +5190,7 @@ "not_enough": "스와프를 완료하기에 {{symbol}}(이)가 충분하지 않음", "max_slippage": "최대 슬리피지", "max_slippage_amount": "최대 슬리피지 {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "주문을 제출 시점과 컨펌 시점 사이의 환율 변동을 ‘슬리피지’라고 합니다. 슬리피지가 설정한 ‘최대 슬리피지’를 초과하면 해당 스왑은 자동으로 취소됩니다.", "slippage_warning": "본인의 거래 행동의 의미를 잘 이해하시기 바랍니다!", "allows_up_to_decimals": "{{symbol}}(은)는 최대 {{decimals}} 자리수를 허용합니다", "get_quotes": "견적 확인하기", @@ -5199,7 +5229,7 @@ "edit": "수정", "quotes_include_fee": "견적에는 {{fee}}%의 MetaMask 수수료가 포함됩니다", "quotes_include_gas_and_metamask_fee": "견적에는 가스비와 {{fee}}%의 MetaMask 수수료가 포함됩니다", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "탭하여 스왑하세요", "swipe_to_swap": "밀어서 스와프하세요", "swipe_to": "밀어서", "swap": "스왑", @@ -5259,7 +5289,7 @@ "approve": "{{sourceToken}} 토큰 스와프 승인: 최대 {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "대기 중인 스왑({{sourceToken}} 토큰을 {{destinationToken}} 토큰으로)", "swap_confirmed": "스와프 완료 ({{sourceToken}} 토큰을 {{destinationToken}} 토큰으로)", "approve_pending": "{{sourceToken}} 토큰 스와프 승인 중", "approve_confirmed": "{{sourceToken}} 토큰 스와프 승인 완료" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "네트워크 선택 메뉴가 자산 섹션으로 이동했습니다", "description_2": "스왑과 브릿지를 한 번에 간편하게 처리할 수 있습니다", - "description_3": "Streamlined send experience", + "description_3": "전송 절차 간소화", "description_4": "새로워진 계정 화면" }, "more_information": "이제 관련된 네트워크에 신경 쓸 필요 없이 토큰과 활동 자체에 집중할 수 있습니다.", @@ -5406,21 +5436,21 @@ "aggressive_label": "공격적", "aggressive_text": "변동성이 높은 시장에서도 체결 가능성이 높습니다. 인기 NFT 드랍 둥으로 인한 네트워크 트래픽 급증을 커버하려면 Aggressive를 사용하세요.", "market_label": "시장", - "market_text": "Use market for fast processing at current market price.", + "market_text": "현재 시장 가격으로 빠르게 처리하려면 시장을 사용하세요.", "low_label": "낮음", "low_text": "더 저렴한 가격을 기다리려면 '낮음'을 사용하세요. 가격은 어느 정도 예측이 어렵기 때문에 소요 시간 추정치는 정확도가 낮을 수 있습니다.", "link": "가스 맞춤에 대해 더 자세히 알아보세요." }, "save": "저장", "submit": "제출", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "현재 네트워크 상황에 비해 최대 우선 수수료가 낮습니다", + "max_priority_fee_high": "최대 우선 수수료가 필요 이상으로 높습니다", + "max_priority_fee_speed_up_low": "최대 우선 수수료는 {{speed_up_floor_value}} GWEI 이상이어야 합니다(최초 트랜잭션보다 10% 높음)", + "max_priority_fee_cancel_low": "최대 우선 수수료는 {{cancel_value}} GWEI 이상이어야 합니다(최초 트랜잭션보다 50% 높음)", + "max_fee_low": "현재 네트워크 상황에 비해 최대 수수료가 낮습니다", + "max_fee_high": "최대 수수료가 필요 이상으로 높습니다", + "max_fee_speed_up_low": "최대 수수료는 {{speed_up_floor_value}} GWEI 이상이어야 합니다(최초 트랜잭션보다 10% 높음)", + "max_fee_cancel_low": "최대 수수료는 {{cancel_value}} GWEI 이상이어야 합니다(최초 트랜잭션보다 50% 높음)", "learn_more_gas_limit": "가스 한도란 사용을 원하는 가스의 최대 단위를 말합니다. 가스 단위는 \"최대 우선 수수료\" 및 \"최대 수수료\"의 승수입니다.", "learn_more_max_priority_fee": "최대 우선 수수료(혹은 “채굴자 팁”)는 거래를 우선화하기 위해 채굴자에게 직접 지불하는 인센티브입니다. 주로 최대 설정 비용을 지불하게 됩니다.", "learn_more_max_fee": "최대 수수료는 사용자가 지불할 최대 금액입니다(기본 수수료 + 우선 수수료).", @@ -5530,10 +5560,10 @@ "enable_remember_me_description": "로그인 정보를 저장하면 회원님의 휴대폰을 이용하는 모든 사람이 회원님의 MetaMask 계정에 액세스할 수 있습니다." }, "turn_off_remember_me": { - "title": "로그인 정보 저장 기능을 끄려면 비밀번호를 입력하세요", - "placeholder": "비밀번호", - "description": "이 기능을 끄면 지금부터 비밀번호를 사용하여 MetaMask를 잠금 해제해야 합니다.", - "action": "로그인 정보 저장 끄기" + "title": "기억하기 기능 끄기", + "placeholder": "비밀번호 컨펌", + "description": "기억하기 기능을 끄면 이를 다시 사용할 수 없습니다. 이 기능은 지원이 중단되었으며, 대신 비밀번호 또는 바이오메트릭으로 MetaMask를 잠금 해제할 수 있습니다.", + "action": "기억하기 기능 끄기" }, "dapp_connect": { "warning": "이 기능을 사용하려면 앱을 최신 버전으로 업데이트하세요" @@ -5582,7 +5612,7 @@ "learn_more": "더 보기" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "타사 세부 정보 검증", "protect_from_scams": "사기를 방지하려면 잠시 시간을 내어 타사에 대한 세부 정보를 확인하세요.", "learn_to_verify": "타사 세부 정보 검증 방법", "spending_cap": "지출 한도", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "복원 필요", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "문제가 발생했지만 걱정하지 마세요! 지갑을 복원해 보겠습니다.", "restore_needed_action": "지갑 복원" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Ledger 기기에 실행 중인 앱을 종료하는 데 실패했습니다.", "ethereum_app_not_installed": "이더리움 앱이 설치되어 있지 않습니다.", "ethereum_app_not_installed_error": "Ledger 기기에 이더리움 앱을 설치하세요.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "Ethereum 앱이 열려 있지 않습니다", + "eth_app_not_open_message": "Ledger 장치에서 Ethereum 앱을 열어 주세요.", "ledger_is_locked": "Ledger 잠김", "unlock_ledger_message": "Ledger 기기를 잠금해제하세요", "cannot_get_account": "계정 연결 실패", @@ -5797,8 +5827,8 @@ "error_description": "{{snap}} 설치에 실패했습니다." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "지갑에서 매일 청구할 수 있는 연간 보너스.", + "earn_a_percentage_bonus": "{{percentage}}% 보너스 받기", "claimable_bonus": "청구 가능한 보너스", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "프로토콜에서 토큰을 인출해 지갑으로 보내는 데 걸리는 시간입니다", "receive": "이 토큰은 자산과 보상을 추적하는 데 사용됩니다. 이를 전송하거나 거래하면 자산을 인출할 수 없게 됩니다.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "청산 위험 지수는 청산 위험도를 나타냅니다", "above_two_dot_zero": "2.0 이상", "safe_position": "안전한 포지션", "between_one_dot_five_and_2_dot_zero": "1.5~2.0", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "중간 청산 위험", "below_one_dot_five": "1.5 미만", "higher_liquidation_risk": "높은 청산 위험" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "전체 잔액을 출금할 수 없는 이유는 무엇인가요?", "your_withdrawal_amount_may_be_limited_by": "출금 한도는 다음과 같은 사유로 인해 제한될 수 있습니다", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "풀 유동성", "not_enough_funds_available_in_the_lending_pool_right_now": "현재 대출 풀에 충분한 자금이 없습니다.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "기존 대출 포지션", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "출금 시 기존 대출이 청산 위험에 처할 수 있습니다." } }, @@ -5998,11 +6028,11 @@ "earn_button": "수익 창출" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "TRX를 스테이킹하고 수익을 창출하세요", + "stake_any_amount": "TRX를 원하는 만큼 스테이킹하세요.", "earn_trx_rewards": "TRX 보상을 획득하세요.", "earn_trx_rewards_description": "스테이킹 즉시 수익 창출이 시작됩니다. 보상은 자동으로 누적됩니다.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "언제든지 언스테이킹하세요. 언스테이킹 처리에는 최대 14일이 걸립니다." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "예상 가스비", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "가스비는 이더리움 네트워크에서 트랜잭션을 처리하는 암호화폐 채굴자에게 지급됩니다. MetaMask는 가스비로 수익을 창출하지 않습니다.", "gas_fluctuation": "가스비는 예상치이며 네트워크 트래픽과 트랜잭션 복잡도에 따라 변동됩니다.", "gas_learn_more": "가스비에 대해 더 자세히 알아보기" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "다음으로 로그인 중", "spender": "지출자", "now": "지금", - "switching_to": "Switching to", + "switching_to": "다음으로 전환 중:", "bridge_estimated_time": "예상 시간", "pay_with": "결제 수단:", - "receive_as": "Receive", + "receive_as": "받기", "total": "총액", - "you_receive": "You'll receive", + "you_receive": "받을 금액:", "transaction_fee": "트랜잭션 수수료", - "transaction_fees": "Transaction fees", + "transaction_fees": "트랜잭션 수수료", "metamask_fee": "MetaMask 수수료", "network_fee": "네트워크 수수료", "bridge_fee": "브릿지 제공자 수수료" @@ -6234,7 +6264,7 @@ "transaction_fee": "예측에서 사용하는 네트워크인 Polygon에서 토큰을 USDC.e로 스왑합니다. 스왑 제공업체가 수수료를 부과할 수 있지만 MetaMask에서 부과하는 수수료는 없습니다." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMask가 원하는 토큰으로 대신 스왑해 드립니다. MUSD로 스왑할 경우 MetaMask수수료는 부과되지 않습니다." }, "musd_conversion": { "transaction_fee": "mUSD 전환 수수료에는 네트워크 비용이 포함되며, 공급자 수수료가 추가될 수 있습니다." @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "서명이 필요한 사이트입니다", "transaction_tooltip": "이 사이트에서 트랜잭션을 요청하고 있습니다", "details": "세부 정보", - "qr_get_sign": "Get signature", + "qr_get_sign": "서명 받기", "qr_scan_text": "하드웨어 지갑으로 스캔하기", "sign_with_ledger": "Ledger로 로그인", "smart_account": "스마트 계정", "smart_contract": "스마트 계약", - "standard_account": "Standard account", + "standard_account": "일반 계정", "siwe_message": { "url": "URL", "network": "네트워크", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "스마트 계정", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "일반 계정", "switch": "전환", "switchBack": "돌아가기", "includes_transaction": "트랜잭션 {{transactionCount}}건 포함", @@ -6307,9 +6337,9 @@ "cancel": "취소", "description": "회원님을 대신해 금액이 지출될 경우 허용할 수 있는 액수를 입력하세요.", "invalid_number_error": "지출 한도는 숫자여야 합니다", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "지출 한도는 비워둘 수 없습니다", + "no_extra_decimals_error": "지출 한도의 소수 자릿수는 토큰의 소수 자릿수보다 많을 수 없습니다", + "no_zero_error": "지출 한도는 0이 될 수 없습니다", "no_zero_error_decrease_allowance": "지출 한도를 0으로 설정해도 'decreaseAllowance' 메서드에 영향이 생기지 않습니다", "no_zero_error_increase_allowance": "지출 한도를 0으로 설정해도 'increaseAllowance' 메서드에 영향이 생기지 않습니다", "save": "저장", @@ -6336,7 +6366,7 @@ "transferRequest": "전송 요청", "nested_transaction_heading": "트랜잭션 {{index}}", "transaction": "트랜잭션", - "available_balance": "Available balance: ", + "available_balance": "사용 가능한 잔액: ", "edit_amount_done": "계속", "deposit_edit_amount_done": "자금 추가", "deposit_edit_amount_predict_withdraw": "출금", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "이용약관", "select_token": "토큰 선택", "no_tokens_found": "토큰을 찾을 수 없습니다", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "해당 이름의 토큰을 찾을 수 없습니다. 다른 검색어를 입력해 보세요.", "select_network": "네트워크 선택", "all_networks": "모든 네트워크", "num_networks": "{{numNetworks}}개 네트워크", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "전체 선택 해제", "see_all": "모두 보기", "all": "모두", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}}개 추가", "apply": "적용", "slippage": "슬리피지", "slippage_info": "주문이 제출되고 확정되는 시점 사이에 가격이 변동되는 것을 '슬리피지'라고 합니다. 슬리피지가 여기 설정한 허용치를 초과하면 스왑이 자동으로 취소됩니다.", @@ -6392,7 +6422,7 @@ "quote_info_title": "비율", "network_fee_info_title": "네트워크 수수료", "network_fee_info_content": "네트워크 수수료는 네트워크의 혼잡도와 트랜잭션의 복잡성에 따라 달라집니다.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "이 네트워크 수수료는 MetaMask가 부담하므로 계정에 {{nativeToken}} 토큰이 없어도 거래할 수 있습니다.", "points": "예상 포인트", "points_tooltip": "포인트", "points_tooltip_content_1": "포인트는 스왑, 브릿지, 무기한 선물 거래 등의 트랜잭션을 완료할 때마다 적립되며, 이를 통해 MetaMask 보상을 획득할 수 있습니다.", @@ -6406,7 +6436,7 @@ "select_recipient": "수신자 선택", "external_account": "외부 계정", "error_banner_description": "현재 이 거래 경로를 이용할 수 없습니다. 금액이나 네트워크, 토큰을 변경해 보세요. 최적의 옵션을 찾아드리겠습니다.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "현재 이 거래 경로를 이용할 수 없습니다. 금액이나 네트워크, 토큰을 변경해 보세요. 최적의 옵션을 찾아드리겠습니다.\n\nOndo 토큰화 주식을 거래하려는 경우 미국, EU, 영국, 브라질 등 지역 제한이 적용될 수 있습니다.", "insufficient_funds": "자금 부족", "insufficient_gas": "가스 부족", "select_amount": "금액 선택", @@ -6417,9 +6447,9 @@ "title": "브릿지", "submitting_transaction": "제출 중", "fetching_quote": "견적 가져오기", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "MetaMask 수수료 {{feePercentage}}%가 포함됩니다.", "no_mm_fee": "MM 수수료 없음", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "{{destTokenSymbol}} 토큰으로 스왑 시 MetaMask 수수료 없음.", "hardware_wallet_not_supported": "하드웨어 지갑은 아직 지원되지 않습니다. 핫월렛을 사용하여 계속하세요.", "hardware_wallet_not_supported_solana": "솔라나는 아직 하드웨어 지갑이 지원하지 않습니다. 계속하려면 핫월렛을 사용하세요.", "price_impact_info_title": "가격 영향", @@ -6432,17 +6462,24 @@ "approval_needed": "토큰의 스왑 사용 권한을 승인합니다.", "approval_tooltip_title": "정확한 접근 권한 부여", "approval_tooltip_content": "지정된 금액, {{amount}} {{symbol}}에만 접근을 허용합니다. 계약은 추가 자금에 접근하지 않습니다.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "최소 수령 금액", + "minimum_received_tooltip_title": "최소 수령 금액", "minimum_received_tooltip_content": "이는 트랜잭션 처리 중 가격이 변동될 경우, 사용자의 슬리피지 허용 범위에 따라 최소한으로 수령할 수 있는 금액입니다. 이는 유동성 공급자들의 추정치이며, 최종 금액은 달라질 수 있습니다.", + "market_closed": { + "title": "시장이 마감되었습니다", + "description": "이 토큰을 뒷받침하는 시장이 현재 마감되었습니다. 토큰은 언제든지 온체인에서 전송할 수 있습니다.", + "learn_more": "더 보기", + "learn_more_url": "https://status.ondo.finance/market", + "done": "완료" + }, "submit": "제출", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "가격이 슬리피지 비율보다 더 크게 변동하면 트랜잭션이 처리되지 않습니다.", "cancel": "취소", "confirm": "컨펌", "exceeding_upper_slippage_warning": "슬리피지가 높아 스왑이 불리한 조건으로 이루어질 수 있습니다", "exceeding_lower_slippage_warning": "슬리피지가 높아 스왑이 불리한 조건으로 이루어질 수 있습니다", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "{{value}}%보다 큰 값을 입력하세요", + "exceeding_upper_slippage_error": "{{value}}%를 초과하는 값은 입력할 수 없습니다", "custom": "맞춤형" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "지갑 복구", "login_with_social": "소셜 계정으로 로그인", "setup": "설정", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "비밀복구구문 {{num}}", "back_up": "백업", "reveal": "공개", "social_recovery_title": "{{authConnection}} 복구", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "비밀번호 입력", "description": "카드 상세 정보를 보려면 지갑 비밀번호를 입력하세요.", + "description_unfreeze": "카드 사용을 재개하려면 지갑 비밀번호를 입력하세요.", "placeholder": "비밀번호", "confirm": "컨펌", "cancel": "취소", @@ -7001,6 +7039,7 @@ "enable_card_error": "카드를 활성화할 수 없습니다. 나중에 다시 시도하세요.", "view_card_details_error": "카드 상세 정보를 불러올 수 없습니다. 다시 시도해 주세요.", "biometric_verification_required": "카드 상세 정보를 보려면 인증이 필요합니다.", + "unfreeze_auth_required": "카드 사용을 재개하려면 인증이 필요합니다.", "warnings": { "close_spending_limit": { "title": "이용한도가 얼마 남지 않았습니다", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "정지된 카드입니다", - "description": "카드 정지를 해제하려면 고객 지원팀에 문의해 주세요" + "description": "카드가 일시적으로 정지되었습니다. 이는 언제든지 해제할 수 있습니다." }, "blocked": { "title": "차단된 카드입니다", @@ -7068,7 +7107,14 @@ "travel_description": "최대 70% 할인된 가격으로 호텔을 예약하세요", "card_tos_title": "이용약관", "order_metal_card": "메탈 카드", - "order_metal_card_description": "실물 메탈 카드를 지금 주문하세요" + "order_metal_card_description": "실물 메탈 카드를 지금 주문하세요", + "freeze_card": "카드 정지", + "unfreeze_card": "카드 정지 해제", + "freeze_card_description": "카드의 모든 사용 일시 중지", + "unfreeze_card_description": "카드의 모든 사용 재개", + "freeze_error": "카드 상태를 업데이트하지 못했습니다. 다시 시도해 주세요.", + "freeze_success": "카드가 성공적으로 정지되었습니다", + "unfreeze_success": "카드가 성공적으로 정지 해제되었습니다" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "{{seconds}}초 후에 다시 보낼 수 있습니다" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "{{walletName}}에 추가", + "adding_to_wallet": "{{walletName}}에 추가 중...", + "continue_setup": "{{walletName}} 설정 계속", + "wallet_not_available": "{{walletName}}을(를) 사용할 수 없습니다", + "already_in_wallet": "이미 {{walletName}}에 있습니다", + "success_title": "카드가 추가되었습니다!", + "success_message": "MetaMask 카드가 {{walletName}}에 추가되었습니다.", + "error_title": "카드 추가 불가능", + "error_wallet_not_available": "이 장치에서는 {{walletName}}을(를) 사용할 수 없습니다. {{walletName}}이(가) 설정되어 있는지 확인해 주세요.", + "error_wallet_not_initialized": "{{walletName}}이(가) 초기화되지 않았습니다. 지갑을 설정한 후 다시 시도해 주세요.", "error_card_already_in_wallet": "이 카드는 이미 {{walletName}}에 추가되어 있습니다.", "error_card_pending": "카드가 {{walletName}}에서 설정되고 있습니다. 몇 분 후 다시 확인하세요.", "error_card_suspended": "{{walletName}}의 카드가 정지되었습니다. 고객 지원팀에 연락하여 도움을 받으세요.", "error_card_not_eligible": "이 카드는 모바일 지갑에 등록할 수 없습니다.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "카드 데이터를 암호화하지 못했습니다. 다시 시도해 주세요.", "error_invalid_card_data": "카드 데이터가 유효하지 않습니다. 카드 정보를 확인한 후 다시 시도하세요.", "error_card_not_found": "카드를 찾을 수 없습니다. 다시 시도하세요.", "error_card_provider_not_found": "사용자 지역에서는 카드 제공자를 이용할 수 없습니다.", "error_card_id_mismatch": "카드 인증에 실패했습니다. 다시 시도하세요.", "error_card_not_active": "카드가 활성화되지 않았습니다. 먼저 카드를 활성화하세요.", "error_network": "네트워크 오류입니다. 연결을 확인하고 다시 시도하세요.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "요청 시간이 초과되었습니다. 다시 시도해 주세요.", + "error_server": "서버 오류가 발생했습니다. 나중에 다시 시도해 주세요.", + "error_unknown": "예기치 못한 오류가 발생했습니다. 다시 시도하거나 고객 지원에 문의해 주세요.", + "error_platform_not_supported": "이 플랫폼은 모바일 지갑 프로비저닝을 지원하지 않습니다.", "try_again": "다시 시도", "cancel": "취소" } @@ -7299,7 +7345,7 @@ "main_title": "보상", "referral_title": "추천", "tab_overview_title": "개요", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "스냅샷", "tab_activity_title": "활동", "referral_stats_earned_from_referrals": "추천을 통해 적립", "referral_stats_referrals": "추천", @@ -7353,7 +7399,7 @@ "verifying_rewards": "회원님이 보상을 수령하기 전에 모든 정보가 정확한지 확인하고 있습니다." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "포인트 획득함" }, "onboarding": { "not_supported_region_title": "지원되지 않는 지역입니다", @@ -7431,7 +7477,7 @@ "show_less": "요약 보기", "linking_progress": "계정 추가 중...({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}} 등록됨", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "모든 계정 추가" }, "referred_by_code": { "title": "추천 코드", @@ -7514,7 +7560,7 @@ "claim_label": "청구", "claimed_label": "받음", "reward_claimed": "보상받음", - "time_left": "{{time}} left", + "time_left": "{{time}} 남음", "expired": "만료됨" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "수령 실패", "redeem_failure_description": "나중에 다시 시도해 주세요.", "reward_details": "보상 상세 정보", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "보상을 받을 계정을 선택하세요." }, "animation": { "could_not_load": "불러올 수 없음" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", + "starts_date": "시작일: {{date}}", + "ends_date": "종료일: {{date}}", + "results_coming_soon": "결과 곧 공개", + "tokens_on_the_way": "토큰 지급 예정", + "pill_up_next": "다음 일정", + "pill_live_now": "지금 진행 중", "pill_calculating": "계산 중", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "pill_results_ready": "결과 준비 완료", + "pill_complete": "완료" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "스냅샷", + "error_title": "스냅샷을 불러올 수 없습니다", + "error_description": "스냅샷을 불러오지 못했습니다. 다시 시도해 주세요.", "retry_button": "다시 시도" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "진행 중", + "upcoming_title": "예정", + "previous_title": "이전", + "empty_state": "사용 가능한 스냅샷 없음", + "error_title": "스냅샷을 불러올 수 없습니다", + "error_description": "스냅샷을 불러오지 못했습니다. 다시 시도해 주세요.", "retry_button": "다시 시도", - "refreshing": "Refreshing..." + "refreshing": "새로 고침 중..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "{{approveSymbol}} 승인", "bridge_approval_loading": "승인", "bridge_send": "{{sourceChain}}에서 {{sourceSymbol}} 토큰 브릿지하기", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "브릿지 전송", "bridge_receive": "{{targetChain}}에서 {{targetSymbol}} 받기", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "브릿지 수신", "default": "트랜잭션", "musd_convert_send": "{{sourceChain}}에서 {{sourceSymbol}} 전송됨", "musd_claim": "mUSD 받기", @@ -7607,20 +7653,20 @@ "description": "{{dappName}}에 연결하는 중..." }, "show_error": { - "title": "Connection error", + "title": "연결 오류", "description": "연결하는 데 실패했습니다. 다시 시도하세요." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "승인 거부", + "description": "사용자가 요청을 거부했습니다." }, "show_return_to_app": { "title": "성공", "description": "앱으로 돌아가서 계속 진행하세요." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "트랜잭션 찾을 수 없음", + "description": "계속하려면 앱에서 새로 연결해 주세요." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "둘러보기", + "trending_tokens": "인기 토큰", "price_change": "가격 변동", "all_networks": "모든 네트워크", - "24h": "24h", + "24h": "24시간", "time": "시간", "24_hours": "24시간", "6_hours": "6시간", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1시간", + "5_minutes": "5분", "networks": "네트워크", "sort_by": "정렬 기준", "volume": "거래량", @@ -7650,32 +7696,48 @@ "high_to_low": "높은 순", "low_to_high": "낮은 순", "apply": "적용", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "토큰, 사이트 및 URL 검색", "cancel": "취소", "perps": "무기한 선물", "predictions": "예측", - "no_results": "No results found", + "no_results": "검색 결과가 없습니다", "sites": "사이트", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "인기 사이트", + "search_sites": "사이트 검색", + "enable_basic_functionality": "기본 기능 활성화", + "basic_functionality_disabled_title": "탐색 기능을 사용할 수 없습니다", + "basic_functionality_disabled_description": "기본 기능이 비활성화되어 있어 필요한 메타데이터를 가져올 수 없습니다.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "인기 토큰을 사용할 수 없습니다", + "description": "현재 이 페이지를 불러올 수 없습니다", "try_again": "다시 시도" }, "empty_search_result_state": { "title": "토큰을 찾을 수 없습니다", - "description": "We were not able to find this token" + "description": "해당 토큰을 찾을 수 없습니다" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "업데이트 준비 완료", + "description_ios": "중요한 수정 사항이 적용되었습니다. 새로 고침하여 최신 버전의 MetaMask를 사용하세요.", + "description_android": "중요한 수정 사항이 적용되었습니다. 업데이트를 적용하려면 MetaMask를 닫았다가 다시 열어 주세요.", "primary_action_reload": "새로고침", "primary_action_acknowledge": "컨펌" + }, + "homepage": { + "sections": { + "tokens": "토큰", + "perpetuals": "영구계약", + "predictions": "예측", + "defi": "디파이", + "nfts": "NFT", + "import_nfts": "NFT 가져오기", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/pt.json b/locales/languages/pt.json index 796bd4d3bec..7e5ef05132a 100644 --- a/locales/languages/pt.json +++ b/locales/languages/pt.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "permanentemente apagados", "reset_wallet_desc_2": "da MetaMask neste dispositivo. Isso não pode ser desfeito.", "reset_wallet_desc_login": "Para restaurar sua carteira, você pode usar sua Frase de Recuperação Secreta ou a senha da sua conta Google ou Apple. A MetaMask não tem essas informações.", - "reset_wallet_desc_srp": "Para restaurar sua carteira, certifique-se de ter em mãos sua Frase de Recuperação Secreta. A MetaMask não possui essa informação." + "reset_wallet_desc_srp": "Para restaurar sua carteira, certifique-se de ter em mãos sua Frase de Recuperação Secreta. A MetaMask não possui essa informação.", + "biometric_authentication_cancelled": "Autenticação biométrica cancelada", + "biometric_authentication_cancelled_title": "Configuração biométrica falhou", + "biometric_authentication_cancelled_description": "Reconfigure a autenticação biométrica nas configurações.", + "biometric_authentication_cancelled_button": "Confirmar" }, "connect_hardware": { "title_select_hardware": "Conectar uma carteira de hardware", @@ -1040,7 +1044,7 @@ "title": "Valor a ser depositado", "get_usdc_hyperliquid": "Obtenha USDC • Hyperliquid", "insufficient_funds": "Fundos insuficientes", - "no_funds_available": "Não há fundos disponíveis. Deposite primeiro.", + "no_funds_available": "Não há fundos suficientes. Deposite fundos ou selecione outro método de pagamento", "enter_amount": "Insira o valor", "fetching_quote": "Buscando cotação", "submitting": "Enviando transação", @@ -1970,8 +1974,8 @@ "trade_again": "Negociar novamente", "activity": { "deposit_title": "Depositar", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "Depositou {{amount}} {{symbol}}", + "withdrew_amount": "Sacou {{amount}} {{symbol}}", "status_completed": "Concluído", "status_failed": "Falha", "status_pending": "Pendente" @@ -2051,6 +2055,16 @@ "referral_code_text": "Use meu código de indicação para ganhar recompensas extras." } }, + "market_insights": { + "title": "Análises de mercado", + "updated_ago": "Atualizado em {{time}}", + "disclaimer": "Análises por IA. Isso não é aconselhamento financeiro.", + "whats_driving_price": "O que está impulsionando o preço?", + "what_people_saying": "O que as pessoas estão dizendo", + "trade_button": "Negociar", + "sources_count": "+{{count}} fontes", + "sources_title": "Maior liquidez" + }, "predict": { "title": "Previsões da MetaMask", "prediction_markets": "Mercados de previsão", @@ -2384,8 +2398,8 @@ "no_available_tokens": "Não está vendo seu token?", "add_tokens": "Importar tokens", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "{{tokenCount}} novo(s) {{tokensLabel}} encontrado(s) nesta conta", "token_toast": { "tokens_imported_title": "Tokens importados", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "Os números decimais do token não podem ficar em branco.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "Não encontramos nenhum token com esse nome.", + "tokens_empty_description": "Search for any token and import it", "select_token": "Selecionar token", "address_must_be_smart_contract": "Endereço pessoal detectado. Insira o endereço do contrato do token.", "billion_abbreviation": "B", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "Desconectar todas as contas", "deceptive_site_ahead": "Site enganoso à vista", "deceptive_site_desc": "O site que você está tentando visitar não é seguro. Invasores podem induzir você a fazer algo perigoso por engano.", + "malicious_site_detected": "Site malicioso detectado", + "malicious_site_warning": "Se você se conectar a este site, poderá perder todos os seus ativos.", + "connect_anyway": "Conectar mesmo assim", "learn_more": "Saiba mais", "advisory_by": "Aconselhamento fornecido por Ethereum Phishing Detector e PhishFort", "potential_threat": "Ameaças potenciais incluem", @@ -2846,7 +2864,11 @@ "permissions": "Permissões", "card_title": "Cartão MetaMask", "settings": "Configurações", - "log_out": "Sair" + "networks": "Redes", + "log_out": "Sair", + "notifications": "Notificações", + "buy": "Comprar", + "scan": "Leia" }, "app_settings": { "enabling_notifications": "Ativando notificações...", @@ -2870,6 +2892,8 @@ "state_logs": "Logs de estado", "add_network_title": "Adicionar uma rede", "auto_lock": "Bloqueio automático", + "enable_device_authentication": "Ativar autenticação de dispositivo", + "enable_device_authentication_desc": "Use a biometria ou a senha do seu dispositivo para desbloquear a MetaMask.", "auto_lock_desc": "Selecione o tempo para bloquear o aplicativo automaticamente.", "state_logs_desc": "Isso ajudará a MetaMask a depurar qualquer problema que você possa encontrar. Envie ao suporte da MetaMask através do ícone de hambúrguer > Enviar comentário ou responda ao seu chamado existente, se for o caso.", "autolock_immediately": "Imediatamente", @@ -2975,6 +2999,11 @@ "add_rpc_url": "Adicionar URL da RPC", "add_block_explorer_url": "Adicionar URL do explorador de blocos", "networks_desc": "Adicione e edite redes RPC personalizadas", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "Pesquisar redes", + "networks_no_results": "Nenhuma rede encontrada", "network_name_label": "Nome da rede", "network_name_placeholder": "Nome da rede (opcional)", "network_rpc_url_label": "URL da RPC", @@ -2991,7 +3020,16 @@ "network_other_networks": "Outras redes", "network_rpc_networks": "Redes RPC", "network_add_network": "Adicionar rede", + "add_chain_title": "Adicionar uma rede", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "Tentar novamente", + "add_chain_added": "Added", + "add_chain_or": "ou", + "add_chain_custom_link": "Adicionar uma rede personalizada", "network_add_custom_network": "Adicionar uma rede personalizada", + "network_add_test_network": "Add a test network", "network_add": "Adicionar", "network_save": "Salvar", "remove_network_title": "Deseja remover essa rede?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "OK", "title": "Não foi possível conectar a conta", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "Escaneie o código QR no site para se reconectar à MetaMask" }, "app_information": { "title": "Informações", @@ -3379,6 +3417,7 @@ "sell_description": "Venda criptomoedas por dinheiro" }, "asset_overview": { + "market_closed": "Mercado fechado", "send_button": "Enviar", "buy_button": "Comprar", "cash_buy_button": "Comprar a dinheiro", @@ -3399,19 +3438,6 @@ "bridge": "Ponte", "earn": "Ganhe", "convert_to_musd": "Converter para mUSD", - "merkl_rewards": { - "annual_bonus": "{{apy}}% de bônus", - "claimable_bonus": "Bônus resgatável", - "claimable_bonus_tooltip_description": "Bônus em mUSD são reivindicados na Linea.", - "terms_apply": "Sujeito a termos e condições.", - "ok": "OK", - "claim": "Resgatar", - "processing_claim": "Processando reivindicação...", - "claim_on_linea_title": "Reivindique bônus na Linea", - "claim_on_linea_description": "Seu bônus será emitido na Linea, separadamente do seu saldo em mUSD da Ethereum.", - "continue": "Continuar", - "unexpected_error": "Ocorreu um erro inesperado. Por favor, tente novamente." - }, "tron": { "daily_resource_new_energy": "Nova energia diária", "sufficient_to_cover": "Suficiente para abranger", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "Endereço do token copiado para a área de transferência" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "Código QR inválido", "invalid_qr_code_message": "O código QR que você está tentando ler não é válido.", "allow_camera_dialog_title": "Permitir acesso à câmera", "allow_camera_dialog_message": "Precisamos da sua permissão para ler códigos QR", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "Parece que você está tentando sincronizar com a extensão. Para isso, você precisará apagar sua carteira atual. \n\nDepois de apagar ou reinstalar uma nova versão do app, selecione a opção \"Sincronizar com a extensão da MetaMask\". Importante: antes de apagar sua carteira, certifique-se de ter guardado sua Frase de Recuperação Secreta.", "not_allowed_error_title": "Ativar acesso à câmera", "not_allowed_error_desc": "Para escanear um código QR, a MetaMask precisa ter acesso à câmera. Ative essa permissão através do menu de configurações do seu dispositivo.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "Código QR não reconhecido", "unrecognized_address_qr_code_desc": "Desculpe, esse código QR não está associado a um endereço de conta ou de contrato.", "url_redirection_alert_title": "Você está sendo direcionado a um link externo", "url_redirection_alert_desc": "Links podem ser usados para tentar aplicar golpes ou phishing nas pessoas. Portanto, certifique-se de só visitar sites em que você confia.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "Você não é dono desse colecionável", "known_asset_contract": "Endereço de contrato conhecido do ativo", "max": "Máximo", - "recipient_address": "Recipient address", + "recipient_address": "Endereço do destinatário", "required": "Obrigatório", "to": "Para", "total": "Total", @@ -3641,7 +3667,7 @@ "nevermind": "Deixa para lá", "edit_network_fee": "Editar taxa de gás", "edit_priority": "Editar prioridade", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "Taxa de cancelamento de gas", "gas_speedup_fee": "Taxa de gás para acelerar", "use_max": "Usar o máximo", "set_gas": "Definir", @@ -3650,7 +3676,7 @@ "transaction_fee": "Taxa de gás", "transaction_fee_less": "Nenhuma taxa", "total_amount": "Valor total", - "view_data": "View data", + "view_data": "Ver dados", "adjust_transaction_fee": "Ajustar taxa de transação", "could_not_resolve_ens": "Não foi possível resolver o ENS", "asset": "Ativo", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "Para navegar pela web descentralizada, adicione uma nova guia", "got_it": "Entendi", "max_tabs_title": "Máximo de guias atingido", - "max_tabs_desc": "Atualmente, há suporte para apenas 5 guias abertas ao mesmo tempo. Feche as guias abertas antes de adicionar outras.", + "max_tabs_desc": "Atualmente, há suporte para apenas 20 guias abertas ao mesmo tempo. Feche as guias abertas antes de adicionar outras.", "failed_to_resolve_ens_name": "Não foi possível resolver o nome ENS", "remove_bookmark_title": "Remover dos favoritos", "remove_bookmark_msg": "Tem certeza de que deseja remover este site dos favoritos?", @@ -3828,7 +3854,7 @@ "cancel_button": "Cancelar" }, "approval": { - "title": "Confirm transaction" + "title": "Confirmar transação" }, "approve": { "title": "Aprovar", @@ -3839,39 +3865,39 @@ "unavailable": "Indisponível", "tx_review_confirm": "Confirmar", "tx_review_transfer": "Transferir", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "Implementação de contrato", + "tx_review_transfer_from": "Transferir de", + "tx_review_unknown": "Método desconhecido", "tx_review_approve": "Aprovar", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "Aumentar permissão", + "tx_review_set_approval_for_all": "Definir aprovação para todos", + "tx_review_staking_claim": "Solicitação de staking", "tx_review_staking_deposit": "Depósito de staking", "tx_review_staking_unstake": "Retirar do staking", "tx_review_lending_deposit": "Depósito de empréstimo", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "Saque de empréstimo", "tx_review_perps_deposit": "Perps financiados", "tx_review_predict_deposit": "Previsões creditadas", "tx_review_predict_claim": "Ganhos resgatados", "tx_review_predict_withdraw": "Previsões retiradas", "tx_review_musd_conversion": "Conversão de mUSD", "claim": "Resgatar", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "ETH enviado", + "self_sent_ether": "Envio de ETH para você mesmo", + "received_ether": "ETH recebido", "sent_dai": "DAI enviados", "self_sent_dai": "DAI enviados a si mesmo", "received_dai": "DAI recebidos", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "Tokens enviados", + "received_tokens": "Tokens recebidos", "ether": "ETH", "sent_unit": "{{unit}} enviados", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "{{unit}} enviado(s) para você mesmo", "received_unit": "{{unit}} recebidos", "sent_collectible": "Item colecionável enviado", "received_collectible": "Item colecionável recebido", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "ETH enviado", + "send_unit": "{{unit}} enviado(s)", "send_collectible": "Enviar item colecionável", "receive_collectible": "Receber item colecionável", "sent": "Enviados", @@ -3881,17 +3907,17 @@ "send": "Enviar", "redeposit": "Redepositar", "interaction": "Interação", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "Implementação de contrato", + "to_contract": "Novo contrato", + "mint": "Mintar", "tx_details_free": "Gratuito", "tx_details_not_available": "Não disponível", "smart_contract_interaction": "Interação com contrato inteligente", "swaps_transaction": "Transação de trocas", "bridge_transaction": "Ponte", "approve": "Aprovar", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "Aumentar permissão", + "set_approval_for_all": "Definir aprovação para todos", "hash": "Hash", "from": "De", "to": "Para", @@ -3899,15 +3925,15 @@ "amount": "Valor", "fee": { "transaction_fee_in_ether": "Taxa de transação", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "Taxa de transação (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "Gas usado (unidades)", + "gas_limit": "Limite de gas (unidades)", + "gas_price": "Preço do gas (GWEI)", + "base_fee": "Taxa-base (GWEI)", + "priority_fee": "Taxa de prioridade (GWEI)", "multichain_priority_fee": "Taxa de prioridade", - "max_fee": "Max fee per gas", + "max_fee": "Taxa máxima por gas", "total": "Total", "view_on": "Ver em", "view_on_etherscan": "Ver no Etherscan", @@ -3923,13 +3949,13 @@ "nonce": "Nonce", "from_device_label": "neste dispositivo", "import_wallet_row": "Conta adicionada a este dispositivo", - "import_wallet_label": "Account added", + "import_wallet_label": "Conta adicionada", "import_wallet_tip": "Todas as transações futuras feitas neste dispositivo incluirão o rótulo \"neste dispositivo\" ao lado do carimbo de data/hora. Para transações feitas antes da inclusão da conta, o histórico não indicará quais transações de saída se originaram neste dispositivo.", "sign_title_scan": "Faça a leitura ", "sign_title_device": "com sua carteira de hardware", "sign_description_1": "Depois de assinar com sua carteira de hardware,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "Toque em obter assinatura", + "sign_get_signature": "Obter assinatura", "transaction_id": "ID da transação", "network": "Rede", "request_from": "Solicitação de", @@ -4032,7 +4058,7 @@ "title": "Redes", "other_networks": "Outras redes", "close": "Fechar", - "status_ok": "All systems operational", + "status_ok": "Todos os sistemas operando normalmente", "status_not_ok": "A rede está com alguns problemas", "want_to_add_network": "Deseja adicionar essa rede?", "add_custom_network": "Adicionar rede personalizada", @@ -4051,7 +4077,7 @@ "review": "Revisar", "view_details": "Ver detalhes", "network_details": "Detalhes da rede", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "Selecionar Confirmar ativa a verificação de detalhes da rede. Para desativar a verificação de detalhes da rede, acesse ", "network_settings_security_privacy": "Configurações > Segurança e Privacidade", "network_currency_symbol": "Símbolo da moeda", "network_block_explorer_url": "URL do explorador de blocos", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "Um provedor de rede mal-intencionado pode mentir sobre o estado da blockchain e registrar sua atividade na rede. Adicione apenas redes personalizadas em que você confia.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Informações de rede", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "Informações adicionais sobre redes", "network_warning_desc": "Essa conexão de rede depende de terceiros. Essa conexão pode ser menos confiável ou permitir que terceiros rastreiem as atividades.", "additonial_network_information_desc": "Algumas dessas redes dependem de terceiros. As conexões podem ser menos confiáveis ​​ou permitir que terceiros rastreiem atividades.", "connect_more_networks": "Conectar mais redes", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "Essa rede foi descontinuada", "network_deprecated_description": "A rede à qual você está tentando se conectar não é mais suportada pela MetaMask.", "edit_networks_title": "Editar redes", - "no_network_fee": "No network fee" + "no_network_fee": "Sem taxa de rede" }, "permissions": { "title_this_site_wants_to": "Este site quer:", @@ -4111,11 +4137,11 @@ "network_connected": "rede conectada", "see_your_accounts": "Ver suas contas e sugerir transações", "connected_to": "Conectado ao ", - "manage_permissions": "Manage permissions", + "manage_permissions": "Gerenciar permissões", "edit": "Editar", "cancel": "Cancelar", "got_it": "Entendi", - "connection_details_title": "Connection details", + "connection_details_title": "Detalhes da conexão", "connection_details_description": "Você se conectou a este site usando o navegador da MetaMask em {{connectionDateTime}}", "title_add_network_permission": "Adicionar permissão de rede", "add_this_network": "Adicionar esta rede", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "Desbloquear com o PIN do dispositivo?" }, "authentication": { - "auth_prompt_title": "Autenticação necessária", - "auth_prompt_desc": "Faça a autenticação para usar a MetaMask", - "fingerprint_prompt_title": "Autenticação necessária", - "fingerprint_prompt_desc": "Use sua impressão digital para desbloquear a MetaMask", - "fingerprint_prompt_cancel": "Cancelar" + "auth_prompt_desc": "Faça a autenticação para usar a MetaMask" }, "accountApproval": { "title": "SOLICITAÇÃO DE CONEXÃO", "walletconnect_title": "SOLICITAÇÃO DE WALLETCONNECT", "action": "Conectar a este site?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "Para retomar a conexão, selecione o número que aparece no site", + "action_reconnect_deeplink": "Deseja se reconectar a este site?", "connect": "Conectar", "resume": "Continuar", "cancel": "Cancelar", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "Não lembrar a conexão com este site", "disconnect": "Desconectar", "permission": "Ver o seu", "address": "endereço público", @@ -4218,7 +4240,7 @@ "error_title": "Ocorreu algum erro", "error_message": "Não foi possível importar a chave privada. Certifique-se de que você a inseriu corretamente.", "error_empty_message": "Você precisa inserir sua chave privada.", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "ou escaneie um código QR" }, "import_private_key_success": { "title": "Conta importada com sucesso!", @@ -4229,8 +4251,8 @@ "import_wallet_title": "Importar uma carteira", "enter_srp_subtitle": "Insira sua Frase de Recuperação Secreta", "textarea_placeholder": "Adicione um espaço entre cada palavra e se certifique de que ninguém esteja olhando", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "Insira a Frase de Recuperação Secreta da sua carteira. Você pode importar qualquer Frase de Recuperação Secreta do Ethereum, Solana ou Bitcoin.", + "subtitle": "Cole a sua Frase de Recuperação Secreta", "cta_text": "Continuar", "paste": "Colar", "clear": "Limpar tudo", @@ -4238,9 +4260,9 @@ "12_word_option": "Tenho uma frase de 12 palavras", "24_word_option": "Tenho uma frase de 24 palavras", "error_title": "Ocorreu algum erro", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "Não foi possível importar a Chave de Recuperação Secreta Certifique-se de que você a digitou corretamente.", + "error_empty_message": "Você precisa inserir sua Frase de Recuperação Secreta.", + "error_number_of_words_error_message": "Frases de Recuperação Secretas contêm 12 ou 24 palavras", "error_srp_is_case_sensitive": "Entrada inválida! A frase de recuperação secreta diferencia maiúsculas e minúsculas.", "error_srp_word_error_1": "As palavras ", "error_srp_word_error_2": " está incorreta ou contém erros de ortografia.", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " estão incorretas ou contêm erros de ortografia.", "error_invalid_srp": "Frase de recuperação secreta inválida", "error_duplicate_srp": "Essa Frase de Recuperação Secreta já foi importada.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "A conta que você está tentando importar é duplicada.", + "invalid_qr_code_title": "Código QR inválido", + "invalid_qr_code_message": "O código QR não contém uma Frase de Recuperação Secreta válida", "success_1": "Carteira", "success_2": "importadas" }, @@ -4665,7 +4687,7 @@ "button": "Proteger carteira" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "Falha em atualizar a transação", "text": "Deseja tentar novamente?", "cancel_button": "Cancelar", "retry_button": "Tentar novamente" @@ -4684,13 +4706,13 @@ "next": "Avançar", "amount_placeholder": "0,00", "link_copied": "Link copiado para a área de transferência", - "send_link_title": "Send link", + "send_link_title": "Enviar link", "description_1": "Seu link de solicitação está pronto para envio!", "description_2": "Envie este link a um amigo, e o link pedirá que ele envie", "copy_to_clipboard": "Copiar para a área de transferência", "qr_code": "QR code", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "Enviar link", + "request_qr_code": "Código QR de solicitação de pagamento", "balance": "Saldo" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "Você não tem nenhuma sessão ativa", - "end_session_title": "End session", + "end_session_title": "Encerrar sessão", "end": "Encerrar", "cancel": "Cancelar", - "session_ended_title": "Session ended", + "session_ended_title": "Sessão encerrada", "session_ended_desc": "A sessão selecionada foi encerrada", "session_already_exist": "Essa sessão já está conectada.", "close_current_session": "Encerre a sessão atual antes de iniciar outra." @@ -4765,15 +4787,14 @@ "on_network": "em {{networkName}}", "debit_card": "Cartão de débito", "select_payment_method": "Selecione o método de pagamento", - "loading_quote": "Loading quote...", "pay_with": "Pagar com", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "Comprando via {{providerName}}.", + "change_provider": "Alterar fornecedor.", "payment_error": "Algo deu errado. Tente novamente.", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "Nenhum método de pagamento disponível.", "error_fetching_quotes": "Algo deu errado. Tente novamente.", "no_quotes_available": "Nenhum provedor disponível.", - "providers": "Providers", + "providers": "Fornecedores", "continue": "Continuar", "powered_by_provider": "Desenvolvido por {{provider}}", "purchased_currency": "{{currency}} comprado", @@ -4871,6 +4892,15 @@ "log_out": "Sair de {{provider}}", "logged_out_success": "Desconectado com sucesso", "logged_out_error": "Erro ao desconectar" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "menor limite de venda", "medium_sell_limit": "limite médio de venda", "highest_sell_limit": "maior limite de venda", - "change": "Change", + "change": "Mudar", "continue_to_amount": "Continuar para o valor", "no_payment_methods_title": "Nenhuma forma de pagamento disponível em {{regionName}}", "no_cash_destinations_title": "Nenhum destino para dinheiro em espécie em {{regionName}}", @@ -5118,7 +5148,7 @@ "start_swapping": "Comece a trocar" }, "feature_off_title": "Temporariamente indisponível", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "O MetaMask Swaps está em manutenção. Volte mais tarde.", "wrong_network_title": "As trocas estão indisponíveis", "wrong_network_body": "Você só pode trocar tokens na rede principal do Ethereum.", "unallowed_asset_title": "Não é possível trocar esse token", @@ -5160,7 +5190,7 @@ "not_enough": "Não há {{symbol}} suficiente para concluir essa troca", "max_slippage": "Slippage máximo", "max_slippage_amount": "Slippage máximo {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "A diferença de cotação entre o momento em que sua ordem é enviada e o momento em que ela é confirmada é chamada de \"slippage\" (deslizamento). Sua troca será cancelada automaticamente se a variação exceder o \"slippage máximo\" que você definiu.", "slippage_warning": "Certifique-se de que sabe exatamente o que está fazendo!", "allows_up_to_decimals": "{{symbol}} admite até {{decimals}} decimais", "get_quotes": "Obter cotações", @@ -5199,7 +5229,7 @@ "edit": "Editar", "quotes_include_fee": "As cotações incluem uma taxa de {{fee}}% da MetaMask", "quotes_include_gas_and_metamask_fee": "A cotação inclui a taxa de gás e uma taxa de {{fee}}% da MetaMask", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "Toque para trocar", "swipe_to_swap": "Deslize para trocar", "swipe_to": "Deslize para", "swap": "Trocar", @@ -5259,7 +5289,7 @@ "approve": "Aprovar {{sourceToken}} para trocas: até {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "Troca pendente de ({{sourceToken}} por {{destinationToken}})", "swap_confirmed": "Troca concluída de ({{sourceToken}} por {{destinationToken}})", "approve_pending": "Aprovando {{sourceToken}} para trocas", "approve_confirmed": "{{sourceToken}} aprovado para trocas" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "Menu suspenso de rede movido para seus ativos", "description_2": "Faça trocas e pontes em um único fluxo simples", - "description_3": "Streamlined send experience", + "description_3": "Experiência de envio simplificada", "description_4": "Uma nova visão da conta" }, "more_information": "Agora você pode se concentrar em seus tokens e atividades, não nas redes por trás deles.", @@ -5406,21 +5436,21 @@ "aggressive_label": "Agressiva", "aggressive_text": "Alta probabilidade, mesmo em mercados voláteis. Use Agressiva para cobrir picos no tráfego da rede devido a situações como drops de NFTs populares.", "market_label": "Mercado", - "market_text": "Use market for fast processing at current market price.", + "market_text": "Utilize o mercado para processamento rápido ao preço de mercado atual.", "low_label": "Baixa", "low_text": "Use a opção \"baixo\" para esperar por um preço mais barato. As estimativas de tempo são muito menos precisas, pois os preços são um tanto imprevisíveis.", "link": "Saiba mais sobre a personalização do gás." }, "save": "Salvar", "submit": "Enviar", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "A taxa de prioridade máxima está baixa para as condições atuais da rede", + "max_priority_fee_high": "A taxa de prioridade máxima está mais alta que o necessário", + "max_priority_fee_speed_up_low": "A taxa de prioridade máxima deve ser de pelo menos {{speed_up_floor_value}} GWEI (10% maior que a transação inicial)", + "max_priority_fee_cancel_low": "A taxa de prioridade máxima deve ser de pelo menos {{cancel_value}} GWEI (50% maior que a transação inicial)", + "max_fee_low": "A taxa máxima está baixa para as condições atuais da rede", + "max_fee_high": "A taxa máxima está mais alta que o necessário", + "max_fee_speed_up_low": "A taxa máxima deve ser de pelo menos {{speed_up_floor_value}} GWEI (10% maior que a transação inicial)", + "max_fee_cancel_low": "A taxa máxima deve ser de pelo menos {{cancel_value}} GWEI (50% maior que a transação inicial)", "learn_more_gas_limit": "O limite de gás é o número máximo de unidades de gás que você está disposto a usar. As unidades de gás são um multiplicador da \"taxa de prioridade máxima\" e da \"taxa máxima\". ", "learn_more_max_priority_fee": "A taxa de prioridade máxima (também conhecida como \"gorjeta do minerador\") vai diretamente para os mineradores e os incentiva a priorizar sua transação. Na maioria dos casos, você paga o valor máximo definido. ", "learn_more_max_fee": "A taxa máxima é o valor máximo que você pagará (taxa-base + taxa de prioridade). ", @@ -5530,9 +5560,9 @@ "enable_remember_me_description": "Quando \"Lembrar de mim\" estiver ativado, qualquer pessoa com acesso ao seu telefone poderá acessar sua conta da MetaMask." }, "turn_off_remember_me": { - "title": "Insira sua senha para desativar \"Lembrar de mim\"", - "placeholder": "Senha", - "description": "Se desativar essa opção, você precisará de sua senha para desbloquear a MetaMask a partir de agora.", + "title": "Desativar \"Lembrar de mim\"", + "placeholder": "Confirmar senha", + "description": "Após ser desativada, a opção \"Lembrar de mim\" não poderá mais ser utilizado. Esse recurso foi descontinuado, portanto você pode desbloquear a MetaMask usando sua senha ou biometria.", "action": "Desativar \"Lembrar de mim\"" }, "dapp_connect": { @@ -5582,7 +5612,7 @@ "learn_more": "Saiba mais" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "Verificar dados de terceiros", "protect_from_scams": "Para se proteger contra golpistas, reserve um momento para confirmar os dados de terceiros.", "learn_to_verify": "Aprenda como confirmar os dados de terceiros", "spending_cap": "limite de gastos", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "Restauração necessária", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "Algo deu errado, mas não se preocupe! Vamos tentar restaurar sua carteira.", "restore_needed_action": "Restaurar carteira" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Falha ao fechar o app em execução em seu dispositivo Ledger.", "ethereum_app_not_installed": "O app Ethereum não está instalado.", "ethereum_app_not_installed_error": "Por favor, instale o app Ethereum em seu dispositivo Ledger.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "O aplicativo Ethereum não abre", + "eth_app_not_open_message": "Instale o app Ethereum em seu dispositivo Ledger.", "ledger_is_locked": "A Ledger está bloqueada", "unlock_ledger_message": "Favor desbloquear seu dispositivo Ledger", "cannot_get_account": "Não é possível obter a conta", @@ -5797,8 +5827,8 @@ "error_description": "Ocorreu uma falha na instalação de {{snap}}." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "Um bônus anual que pode ser resgatado diariamente a partir da sua carteira.", + "earn_a_percentage_bonus": "Ganhe um bônus de {{percentage}}%", "claimable_bonus": "Bônus resgatável", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "O tempo que leva para retirar seu token do protocolo e devolvê-lo à sua carteira", "receive": "Este token é utilizado para rastrear seus ativos e recompensas. Evite transferi-lo ou negociá-lo, ou você não poderá sacar seus ativos.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "Seu fator de saúde mede o risco de liquidação", "above_two_dot_zero": "Acima de 2,0", "safe_position": "Posição segura", "between_one_dot_five_and_2_dot_zero": "Entre 1,5-2,0", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "Risco médio de liquidação", "below_one_dot_five": "Abaixo de 1,5", "higher_liquidation_risk": "Risco de liquidação mais alto" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "Por que não posso sacar todo o meu saldo?", "your_withdrawal_amount_may_be_limited_by": "O valor do seu saque pode ser limitado por", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "Liquidez do pool", "not_enough_funds_available_in_the_lending_pool_right_now": "Valores insuficientes disponíveis no pool de empréstimo neste momento.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "Posições de tomada de empréstimo existentes", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "O saque poderia colocar suas atuais posições de empréstimo em risco de liquidação." } }, @@ -5998,11 +6028,11 @@ "earn_button": "Ganhe" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "Faça staking de TRX e ganhe", + "stake_any_amount": "Faça staking de qualquer quantia de TRX.", "earn_trx_rewards": "Ganhe recompensas em TRX.", "earn_trx_rewards_description": "Comece a ganhar assim que fizer stake. As recompensas acumulam progressivamente de forma automática.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "Retire do staking a qualquer momento. O processamento de retirada do staking geralmente leva 14 dias." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "Taxa de gás estimada", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "As taxas de gas são pagas aos mineradores de criptoativos que processam as transações na rede Ethereum. A MetaMask não lucra com as taxas de gas.", "gas_fluctuation": "As taxas de gás são estimadas e oscilam com base no tráfego da rede e na complexidade da transação.", "gas_learn_more": "Saiba mais sobre as taxas de gás" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "Fazendo login com", "spender": "Gastador", "now": "Agora", - "switching_to": "Switching to", + "switching_to": "Mudando para", "bridge_estimated_time": "Tempo est.", "pay_with": "Pagar com", - "receive_as": "Receive", + "receive_as": "Receber", "total": "Total", - "you_receive": "You'll receive", + "you_receive": "Você receberá", "transaction_fee": "Taxa de transação", - "transaction_fees": "Transaction fees", + "transaction_fees": "Taxas de transação", "metamask_fee": "Taxa da MetaMask", "network_fee": "Taxa de rede", "bridge_fee": "Taxa do provedor de ponte" @@ -6234,7 +6264,7 @@ "transaction_fee": "Trocaremos seus tokens por USDC.e na Polygon, a rede usada pela Predictions. Provedores de swaps talvez cobrem taxas, mas a MetaMask não cobra." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "A MetaMask fará a conversão para o token desejado para você. A MetaMask não aplica taxas quando você converte para MUSD." }, "musd_conversion": { "transaction_fee": "As taxas de conversão de mUSD incluem custos de rede e podem incluir taxas do provedor." @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "Este site está solicitando sua assinatura", "transaction_tooltip": "Este site está solicitando sua transação", "details": "Detalhes", - "qr_get_sign": "Get signature", + "qr_get_sign": "Obter assinatura", "qr_scan_text": "Leia com sua carteira de hardware", "sign_with_ledger": "Assinar com o Ledger", "smart_account": "Conta inteligente", "smart_contract": "Contrato inteligente", - "standard_account": "Standard account", + "standard_account": "Conta padrão", "siwe_message": { "url": "URL", "network": "Rede", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "Conta inteligente", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "Conta padrão", "switch": "Alternar", "switchBack": "Reverter", "includes_transaction": "Inclui {{transactionCount}} transações", @@ -6307,9 +6337,9 @@ "cancel": "Cancelar", "description": "Digite o valor que você considera adequado que seja gasto em seu nome.", "invalid_number_error": "O limite de gastos deve ser um número", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "O limite de gastos não pode estar vazio", + "no_extra_decimals_error": "O limite de gastos não pode ter mais casas decimais do que o token", + "no_zero_error": "O limite de gastos não pode ser 0", "no_zero_error_decrease_allowance": "O limite de gastos de 0 não tem efeito sobre o método \"decreaseAllowance\"", "no_zero_error_increase_allowance": "O limite de gastos de 0 não tem efeito sobre o método \"increaseAllowance\"", "save": "Salvar", @@ -6336,7 +6366,7 @@ "transferRequest": "Solicitação de transferência", "nested_transaction_heading": "Transação {{index}}", "transaction": "Transações", - "available_balance": "Available balance: ", + "available_balance": "Saldo disponível: ", "edit_amount_done": "Continuar", "deposit_edit_amount_done": "Adicionar fundos", "deposit_edit_amount_predict_withdraw": "Sacar", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "Termos e condições", "select_token": "Selecionar token", "no_tokens_found": "Nenhum token encontrado", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "Não encontramos tokens com este nome. Tente outro termo de pesquisa.", "select_network": "Selecionar rede", "all_networks": "Todas as redes", "num_networks": "{{numNetworks}} redes", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "Desmarcar tudo", "see_all": "Ver tudo", "all": "Tudo", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}} mais", "apply": "Aplicar", "slippage": "Slippage", "slippage_info": "Se entre a realização e a confirmação de sua ordem o preço mudar, teremos o que se chama de “slippage”. Sua troca será automaticamente cancelada se o slippage exceder a tolerância que você definir aqui.", @@ -6392,7 +6422,7 @@ "quote_info_title": "Avaliar", "network_fee_info_title": "Taxa de rede", "network_fee_info_content": "As taxas de rede dependem do nível de atividade da rede e da complexidade da sua transação.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "Essa taxa de rede é paga pela MetaMask, o que permite que você realize transações sem {{nativeToken}} em sua conta.", "points": "Est. de pontos", "points_tooltip": "Pontos", "points_tooltip_content_1": "Os pontos são a forma de você ganhar MetaMask Rewards ao concluir transações, como quando você faz trocas, pontes ou negocia perps.", @@ -6406,7 +6436,7 @@ "select_recipient": "Selecionar destinatário", "external_account": "Conta externa", "error_banner_description": "Esta rota comercial não está disponível no momento. Experimente mudar o valor, a rede ou o token e encontraremos a melhor opção.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "Esta rota de negociação não está disponível no momento. Tente alterar o valor, a rede ou o token e nós encontraremos a melhor opção.\n\nObserve que, se você estiver tentando negociar ações tokenizadas da Ondo (Ondo Tokenised Stocks), pode haver restrições geográficas, por exemplo, nos EUA, UE, Reino Unido e Brasil.", "insufficient_funds": "Fundos insuficientes", "insufficient_gas": "Gás insuficiente", "select_amount": "Selecionar valor", @@ -6417,9 +6447,9 @@ "title": "Ponte", "submitting_transaction": "Enviando", "fetching_quote": "Buscando cotação", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "Inclui {{feePercentage}}% de taxa da MetaMask.", "no_mm_fee": "Sem taxa da MM", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "Sem taxa da MetaMask na conversão para {{destTokenSymbol}}.", "hardware_wallet_not_supported": "Ainda não oferecemos suporte a carteiras de hardware. Use uma hot wallet para continuar.", "hardware_wallet_not_supported_solana": "Carteiras de hardware ainda não são compatíveis com Solana. Use uma hot wallet para continuar.", "price_impact_info_title": "Impacto do preço", @@ -6432,17 +6462,24 @@ "approval_needed": "Aprova token para swap.", "approval_tooltip_title": "Conceder acesso exato", "approval_tooltip_content": "Você está permitindo o acesso ao valor especificado, {{amount}} {{symbol}}. O contrato não acessará nenhum fundo adicional.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "Mínimo recebido", + "minimum_received_tooltip_title": "Mínimo recebido", "minimum_received_tooltip_content": "O valor mínimo que você receberá em caso de mudança do preço durante o processamento da sua transação, com base em sua tolerância ao slippage. Essa é uma estimativa dos nossos provedores de liquidez. Os valores finais podem ser diferentes.", + "market_closed": { + "title": "O mercado está fechado", + "description": "No momento, o mercado que dá suporte a este token está fechado. Tokens podem ser transferidos em rede a qualquer momento.", + "learn_more": "Saiba mais", + "learn_more_url": "https://status.ondo.finance/market", + "done": "Pronto" + }, "submit": "Enviar", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "Sua transação não será concluída se a variação de preço for maior que a porcentagem de slippage.", "cancel": "Cancelar", "confirm": "Confirmar", "exceeding_upper_slippage_warning": "Slippage elevado, que pode resultar em swap desfavorável", "exceeding_lower_slippage_warning": "Slippage reduzido, que pode resultar em swap desfavorável", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "Insira um valor maior que {{value}}%", + "exceeding_upper_slippage_error": "Não é possível inserir um valor maior que {{value}}%", "custom": "Personalizado" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "Recuperação da carteira", "login_with_social": "Faça login com suas contas de redes sociais", "setup": "Configurar", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "Frase de Recuperação Secreta {{num}}", "back_up": "Fazer backup", "reveal": "Revelar", "social_recovery_title": "RECUPERAÇÃO DO {{authConnection}}", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "Insira a senha", "description": "Digite a senha da sua carteira para visualizar detalhes do cartão.", + "description_unfreeze": "Digite a senha da sua carteira para retomar os gastos com o seu cartão.", "placeholder": "Senha", "confirm": "Confirmar", "cancel": "Cancelar", @@ -7001,6 +7039,7 @@ "enable_card_error": "Falha ao ativar o cartão. Tente novamente mais tarde.", "view_card_details_error": "Não foi possível carregar os detalhes do cartão. Tente novamente.", "biometric_verification_required": "Autenticação obrigatória para visualizar os detalhes do cartão.", + "unfreeze_auth_required": "Necessária autenticação para retomar gastos com seu cartão.", "warnings": { "close_spending_limit": { "title": "Você está próximo do seu limite de gastos", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "Seu cartão está congelado", - "description": "Entre em contato com o suporte para descongelar seu cartão" + "description": "Seu cartão está temporariamente congelado. Você pode descongelá-lo quando quiser." }, "blocked": { "title": "Seu cartão está bloqueado", @@ -7068,7 +7107,14 @@ "travel_description": "Reserve hotéis com até 70% de desconto", "card_tos_title": "Termos e condições", "order_metal_card": "Cartão Metal", - "order_metal_card_description": "Peça já o seu Cartão Metal físico" + "order_metal_card_description": "Peça já o seu Cartão Metal físico", + "freeze_card": "Congelar cartão", + "unfreeze_card": "Descongelar cartão", + "freeze_card_description": "Pausar todos os gastos com seu cartão", + "unfreeze_card_description": "Retomar todos os gastos com seu cartão", + "freeze_error": "Falha ao atualizar o status do cartão. Tente novamente.", + "freeze_success": "Cartão congelado com sucesso", + "unfreeze_success": "Cartão descongelado com sucesso" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "Reenvio disponível em {{seconds}} segundos" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "Adicionar à {{walletName}}", + "adding_to_wallet": "Adicionando à {{walletName}}...", + "continue_setup": "Continuar a configuração de {{walletName}}", + "wallet_not_available": "{{walletName}} não disponível", + "already_in_wallet": "Já incluído na {{walletName}}", + "success_title": "Cartão adicionado!", + "success_message": "Seu cartão MetaMask foi adicionado à {{walletName}}.", + "error_title": "Não foi possível adicionar cartão", + "error_wallet_not_available": "A carteira {{walletName}} não está disponível neste dispositivo. Certifique-se de ter configurado a carteira {{walletName}}.", + "error_wallet_not_initialized": "A carteira {{walletName}} não está inicializada. Configure sua carteira e tente novamente.", "error_card_already_in_wallet": "Este cartão já foi adicionado à {{walletName}}.", "error_card_pending": "Seu cartão está sendo configurado em {{walletName}}. Por favor, volte em alguns minutos.", "error_card_suspended": "Seu cartão na {{walletName}} foi suspenso. Entre em contato com o suporte para obter ajuda.", "error_card_not_eligible": "Este cartão não se qualifica para fornecimento de carteira digital.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "Falha ao criptografar os dados do cartão. Tente novamente.", "error_invalid_card_data": "Dados do cartão inválidos. Verifique os dados do seu cartão e tente novamente.", "error_card_not_found": "Cartão não encontrado. Tente novamente.", "error_card_provider_not_found": "Este provedor de cartão não está disponível em sua região.", "error_card_id_mismatch": "A verificação do cartão falhou. Tente novamente.", "error_card_not_active": "Seu cartão não está ativo. Ative-o primeiro.", "error_network": "Ocorreu um erro de rede. Verifique sua conexão e tente novamente.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "A solicitação expirou. Tente novamente.", + "error_server": "Ocorreu um erro no servidor. Tente novamente mais tarde.", + "error_unknown": "Ocorreu um erro inesperado. Tente novamente ou entre em contato com o suporte.", + "error_platform_not_supported": "Esta plataforma não oferece suporte ao fornecimento de carteiras móveis.", "try_again": "Tentar novamente", "cancel": "Cancelar" } @@ -7299,7 +7345,7 @@ "main_title": "Recompensas", "referral_title": "Indicações", "tab_overview_title": "Visão geral", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "Capturas de tela", "tab_activity_title": "Atividade", "referral_stats_earned_from_referrals": "Ganho por meio de indicações", "referral_stats_referrals": "Indicações", @@ -7353,7 +7399,7 @@ "verifying_rewards": "Estamos verificando se tudo está correto antes de você resgatar suas recompensas." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "Pontos ganhos" }, "onboarding": { "not_supported_region_title": "Não há suporte a essa região", @@ -7431,7 +7477,7 @@ "show_less": "Exibir menos", "linking_progress": "Adicionando contas... ({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}} inscrita(s)", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "Adicionar todas as contas" }, "referred_by_code": { "title": "Código de indicação", @@ -7514,7 +7560,7 @@ "claim_label": "Resgatar", "claimed_label": "Resgatada", "reward_claimed": "Recompensa resgatada", - "time_left": "{{time}} left", + "time_left": "{{time}} restante", "expired": "Expirada" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "Falha ao resgatar", "redeem_failure_description": "Tente novamente mais tarde.", "reward_details": "Detalhes da recompensa", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "Selecione para qual conta você deseja que esta recompensa seja enviada." }, "animation": { "could_not_load": "Não foi possível carregar" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", + "starts_date": "Começa em {{date}}", + "ends_date": "Termina em {{date}}", + "results_coming_soon": "Resultados em breve", + "tokens_on_the_way": "Tokens a caminho", + "pill_up_next": "Em seguida", + "pill_live_now": "Ao vivo agora", "pill_calculating": "Calculando", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "pill_results_ready": "Resultados prontos", + "pill_complete": "Concluído" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "Capturas de tela", + "error_title": "Não foi possível carregar capturas de tela", + "error_description": "Não foi possível carregar as capturas de tela. Tente novamente.", "retry_button": "Tentar novamente" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "Ativo", + "upcoming_title": "Próximo", + "previous_title": "Anterior", + "empty_state": "Nenhuma captura de tela disponível", + "error_title": "Não foi possível carregar capturas de tela", + "error_description": "Não foi possível carregar as capturas de tela. Tente novamente.", "retry_button": "Tentar novamente", - "refreshing": "Refreshing..." + "refreshing": "Atualizando..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "Aprovar {{approveSymbol}}", "bridge_approval_loading": "Aprovar", "bridge_send": "Fazer ponte de {{sourceSymbol}} a partir de {{sourceChain}}", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "Envio em ponte", "bridge_receive": "Receber {{targetSymbol}} sobre {{targetChain}}", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "Recebimento em ponte", "default": "Transação", "musd_convert_send": "Enviado {{sourceSymbol}} de {{sourceChain}}", "musd_claim": "Reivindicar mUSD", @@ -7607,20 +7653,20 @@ "description": "Estabelecendo conexão com {{dappName}}..." }, "show_error": { - "title": "Connection error", + "title": "Erro de conexão", "description": "Não foi possível estabelecer a conexão. Tente novamente." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "Aprovação rejeitada", + "description": "Usuário rejeitou a solicitação." }, "show_return_to_app": { "title": "Sucesso", "description": "Volte ao aplicativo para continuar." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "Conexão não encontrada", + "description": "Para continuar, estabeleça uma nova conexão a partir do aplicativo." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "Explorar", + "trending_tokens": "Tokens em alta", "price_change": "Variação de preço", "all_networks": "Todas as redes", "24h": "24h", "time": "Horário", "24_hours": "24 horas", "6_hours": "6 horas", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 hora", + "5_minutes": "5 minutos", "networks": "Redes", "sort_by": "Classificar por", "volume": "Volume", @@ -7650,32 +7696,48 @@ "high_to_low": "Alta para baixa", "low_to_high": "Baixa para alta", "apply": "Aplicar", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "Pesquisar tokens, sites, URLs", "cancel": "Cancelar", "perps": "Perps", "predictions": "Previsões", - "no_results": "No results found", + "no_results": "Nenhum resultado encontrado", "sites": "Sites", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "Websites populares", + "search_sites": "Pesquisar websites", + "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.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "Tokens em alta não estão disponíveis", + "description": "Não é possível buscar esta página neste momento", "try_again": "Tentar novamente" }, "empty_search_result_state": { "title": "Nenhum token encontrado", - "description": "We were not able to find this token" + "description": "Não foi possível encontrar este token" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "Atualização pronta", + "description_ios": "Fizemos algumas correções importantes. Recarregue para a versão mais recente da MetaMask.", + "description_android": "Fizemos algumas correções importantes. Feche e reabra a MetaMask para aplicar a atualização.", "primary_action_reload": "Recarregar", "primary_action_acknowledge": "Entendi" + }, + "homepage": { + "sections": { + "tokens": "Tokens", + "perpetuals": "Perpétuos", + "predictions": "Previsões", + "defi": "DeFi", + "nfts": "NFTs", + "import_nfts": "Importar NFTs", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/ru.json b/locales/languages/ru.json index 9b6c2d628e8..2604f68ec44 100644 --- a/locales/languages/ru.json +++ b/locales/languages/ru.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "удалены навсегда", "reset_wallet_desc_2": "из MetaMask на этом устройстве. Это нельзя отменить.", "reset_wallet_desc_login": "Чтобы восстановить кошелек, вы можете использовать свою секретную фразу восстановления или пароль от аккаунта Google или Apple. У MetaMask нет этой информации.", - "reset_wallet_desc_srp": "Чтобы восстановить свой кошелек, убедитесь, что у вас есть ваша секретная фраза для восстановления. У MetaMask нет этой информации." + "reset_wallet_desc_srp": "Чтобы восстановить свой кошелек, убедитесь, что у вас есть ваша секретная фраза для восстановления. У MetaMask нет этой информации.", + "biometric_authentication_cancelled": "Биометрическая аутентификация отменена", + "biometric_authentication_cancelled_title": "Биометрическая настройка не удалась", + "biometric_authentication_cancelled_description": "Повторно настройте биометрическую аутентификацию в настройках.", + "biometric_authentication_cancelled_button": "Подтвердить" }, "connect_hardware": { "title_select_hardware": "Подключить аппаратный кошелек", @@ -1040,7 +1044,7 @@ "title": "Сумма депозита", "get_usdc_hyperliquid": "Получить USDC • Hyperliquid", "insufficient_funds": "Недостаточно средств", - "no_funds_available": "Нет доступных средств. Сначала внесите депозит.", + "no_funds_available": "Недостаточно средств. Внесите средства или выберите другой способ оплаты", "enter_amount": "Укажите сумму", "fetching_quote": "Получение котировки", "submitting": "Отправка транзакции", @@ -1970,8 +1974,8 @@ "trade_again": "Торговать снова", "activity": { "deposit_title": "Внести депозит", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "Внесено {{amount}} {{symbol}}", + "withdrew_amount": "Выведено {{amount}} {{symbol}}", "status_completed": "Завершено", "status_failed": "Ошибка", "status_pending": "Ожидающий" @@ -2051,6 +2055,16 @@ "referral_code_text": "Использовать мой реферальный код, чтобы получить дополнительные вознаграждения." } }, + "market_insights": { + "title": "Аналитика рынка", + "updated_ago": "Обновлено {{time}}", + "disclaimer": "ИИ-аналитика. Не является финансовой консультацией.", + "whats_driving_price": "Что влияет на цену?", + "what_people_saying": "Что говорят люди", + "trade_button": "Торговать", + "sources_count": "+{{count}} источника(-ов)", + "sources_title": "Ликвидности" + }, "predict": { "title": "Прогнозы MetaMask", "prediction_markets": "Рынки прогнозов", @@ -2384,8 +2398,8 @@ "no_available_tokens": "Не видите свой токен?", "add_tokens": "Импорт токенов", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "{{tokenCount}} новый {{tokensLabel}} найден в этом счете", "token_toast": { "tokens_imported_title": "Импортированные токены", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "Десятичные знаки токена не могут быть пустыми.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "нам не удалось найти токены с таким именем.", + "tokens_empty_description": "Search for any token and import it", "select_token": "Выбрать токен", "address_must_be_smart_contract": "Обнаружен личный адрес. Введите адрес контракта токена.", "billion_abbreviation": "Б", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "Отключить все счета", "deceptive_site_ahead": "Впереди моешеннический сайт", "deceptive_site_desc": "Сайт, который вы пытаетесь посетить, небезопасен. Злоумышленники могут обманом заставить вас сделать что-то опасное.", + "malicious_site_detected": "Обнаружен вредоносный сайт", + "malicious_site_warning": "Если вы подключитесь к этому сайту, вы можете потерять все свои активы.", + "connect_anyway": "Все равно подключить", "learn_more": "Узнайте подробнее", "advisory_by": "Консультации предоставлены Ethereum Phishing Detector и PhishFort", "potential_threat": "Потенциальные угрозы включают", @@ -2846,7 +2864,11 @@ "permissions": "Разрешения", "card_title": "Карта MetaMask", "settings": "Настройки", - "log_out": "Выйти" + "networks": "Сети", + "log_out": "Выйти", + "notifications": "Уведомления", + "buy": "Купить", + "scan": "Сканировать" }, "app_settings": { "enabling_notifications": "Включение уведомлений...", @@ -2870,6 +2892,8 @@ "state_logs": "Журналы состояния", "add_network_title": "Добавить сеть", "auto_lock": "Автоблокировка", + "enable_device_authentication": "Включить аутентификацию устройства", + "enable_device_authentication_desc": "Для разблокировки MetaMask используйте биометрические данные или пароль вашего устройства.", "auto_lock_desc": "Выберите временной интервал до автоблокировки приложения.", "state_logs_desc": "Это поможет MetaMask отладить любую проблему, с которой вы можете столкнуться. Отправьте его в службу поддержки MetaMask через значок «гамбургер» > Отправьте отзыв или ответьте на свой существующий запрос о поддержке, если он у вас есть.", "autolock_immediately": "Немедленно", @@ -2975,6 +2999,11 @@ "add_rpc_url": "Добавить URL-адрес RPC", "add_block_explorer_url": "Добавить URL обозревателя блоков", "networks_desc": "Добавляйте и редактируйте пользовательские сети RPC", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "Поиск сетей", + "networks_no_results": "Сети не найдены", "network_name_label": "Имя сети", "network_name_placeholder": "Имя сети (необязательно)", "network_rpc_url_label": "URL-адрес RPC", @@ -2991,7 +3020,16 @@ "network_other_networks": "Другие сети", "network_rpc_networks": "RPC-сети", "network_add_network": "Добавить сеть", + "add_chain_title": "Добавить сеть", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "Повтор", + "add_chain_added": "Added", + "add_chain_or": "или", + "add_chain_custom_link": "Добавить пользовательскую сеть", "network_add_custom_network": "Добавить пользовательскую сеть", + "network_add_test_network": "Add a test network", "network_add": "Добавить", "network_save": "Сохранить", "remove_network_title": "Вы хотите удалить эту сеть?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "ОК", "title": "Счету не удалось подключиться", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "Отсканируйте QR-код в сайт, чтобы повторно подключиться к MetaMask" }, "app_information": { "title": "Информация", @@ -3379,6 +3417,7 @@ "sell_description": "Продавайте криптовалюту за деньги" }, "asset_overview": { + "market_closed": "Рынок закрыт", "send_button": "Отправить", "buy_button": "Купить", "cash_buy_button": "Купить за деньги", @@ -3399,19 +3438,6 @@ "bridge": "Мост", "earn": "Заработать", "convert_to_musd": "Конвертировать в mUSD", - "merkl_rewards": { - "annual_bonus": "Бонус {{apy}}%", - "claimable_bonus": "Встребуемый бонус", - "claimable_bonus_tooltip_description": "Бонусы в mUSD востребуются на Linea.", - "terms_apply": "Применяются условия.", - "ok": "ОК", - "claim": "Получить", - "processing_claim": "Обработка запроса...", - "claim_on_linea_title": "Получить бонусы от Linea", - "claim_on_linea_description": "Ваш бонус будет начислен на Linea, отдельно от вашего баланса Ethereum mUSD.", - "continue": "Продолжить", - "unexpected_error": "Возникла непредвиденная ошибка. Повторите попытку." - }, "tron": { "daily_resource_new_energy": "Новая дневная энергия", "sufficient_to_cover": "Достаточно для покрытия", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "Адрес токена скопирован в буфер обмена" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "Недействительный QR-код", "invalid_qr_code_message": "QR-код, который вы пытаетесь отсканировать, недействителен.", "allow_camera_dialog_title": "Предоставьте доступ к камере", "allow_camera_dialog_message": "Нам нужно ваше разрешение для сканирования QR-кодов", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "Похоже, вы пытаетесь выполнить синхронизацию с расширением. Для этого вам нужно удалить текущий кошелек. \n\nПосле того как вы удалите или переустановите новую версию приложения, выберите параметр «Синхронизировать с расширением MetaMask». Важно! Прежде чем удалять кошелек, обязательно сделайте резервную копию секретной фразы для восстановления.", "not_allowed_error_title": "Предоставьте доступ к камере", "not_allowed_error_desc": "Чтобы отсканировать QR-код, вам необходимо предоставить доступ к камере MetaMask в меню настроек вашего устройства.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "Нераспознанный QR-код", "unrecognized_address_qr_code_desc": "Извините, этот QR-код не связан с адресом счета или адресом контракта.", "url_redirection_alert_title": "Вы собираетесь перейти по внешней ссылке", "url_redirection_alert_desc": "Ссылки могут быть использованы для обмана или фишинга, поэтому обязательно посещайте только те сайты, которым доверяете.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "Вы не являетесь владельцем этого коллекционного актива", "known_asset_contract": "Известный адрес контракта актива", "max": "Макс.", - "recipient_address": "Recipient address", + "recipient_address": "Адрес получателя", "required": "Требуется", "to": "В адрес", "total": "Итого", @@ -3641,7 +3667,7 @@ "nevermind": "Неважно", "edit_network_fee": "Изменить плату за газ", "edit_priority": "Изменить приоритет", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "Плата за газ при отмене", "gas_speedup_fee": "Плата за газ при ускорении", "use_max": "Использовать макс.", "set_gas": "Установить", @@ -3650,7 +3676,7 @@ "transaction_fee": "Плата за газ", "transaction_fee_less": "Без платы", "total_amount": "Общая сумма", - "view_data": "View data", + "view_data": "Просмотр данных", "adjust_transaction_fee": "Изменить комиссию за транзакцию", "could_not_resolve_ens": "Не удалось разрешить ENS", "asset": "Актив", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "Для просмотра децентрализованной сети добавьте новую вкладку", "got_it": "Понятно", "max_tabs_title": "Достигнут максимум вкладок", - "max_tabs_desc": "Сейчас мы поддерживаем только 5 одновременно открытых вкладок. Закройте текущие вкладки перед добавлением новых.", + "max_tabs_desc": "Сейчас мы поддерживаем только 20 одновременно открытых вкладок. Закройте текущие вкладки перед добавлением новых.", "failed_to_resolve_ens_name": "Нам не удалось получить это имя ENS.", "remove_bookmark_title": "Удалить из Избранного", "remove_bookmark_msg": "Уверены, что хотите удалить этот сайт из черного списка?", @@ -3828,7 +3854,7 @@ "cancel_button": "Отмена" }, "approval": { - "title": "Confirm transaction" + "title": "Подтвердите транзакции" }, "approve": { "title": "Одобрить", @@ -3839,39 +3865,39 @@ "unavailable": "Недоступно", "tx_review_confirm": "Подтвердить", "tx_review_transfer": "Перевести", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "Развертывание контракта", + "tx_review_transfer_from": "Перевести из", + "tx_review_unknown": "Неизвестный способ", "tx_review_approve": "Одобрить", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "Увеличить квоту", + "tx_review_set_approval_for_all": "Одобрить все", + "tx_review_staking_claim": "Получение стейкинга", "tx_review_staking_deposit": "Депозит в стейкинг", "tx_review_staking_unstake": "Отменить стейкинг", "tx_review_lending_deposit": "Кредитный депозит", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "Кредитование вывода средств", "tx_review_perps_deposit": "Пополненные перпы", "tx_review_predict_deposit": "Прогнозы с финансированием", "tx_review_predict_claim": "Востребованные доходы", "tx_review_predict_withdraw": "Отмена прогнозов", "tx_review_musd_conversion": "Конвертация mUSD", "claim": "Получить", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "Отправлены ETH", + "self_sent_ether": "ETH отправлены себе", + "received_ether": "Полученные ETH", "sent_dai": "Отправлен DAI", "self_sent_dai": "Отправил(-а) себе DAI", "received_dai": "Получен DAI", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "Отправленные токены", + "received_tokens": "Полученные токены", "ether": "ETH", "sent_unit": "Отправлен {{unit}}", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "Отправил(-а) себе {{unit}}", "received_unit": "Получен {{unit}}", "sent_collectible": "Отправленный коллекционный актив", "received_collectible": "Полученный коллекционный актив", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "Отправлены ETH", + "send_unit": "Отправить {{unit}}", "send_collectible": "Отправить коллекционный актив", "receive_collectible": "Получить коллекционный актив", "sent": "Отправлено", @@ -3881,17 +3907,17 @@ "send": "Отправить", "redeposit": "Повторить депозит", "interaction": "Взаимодействие", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "Развертывание контракта", + "to_contract": "Новый контракт", + "mint": "Выполнить минтинг", "tx_details_free": "Бесплатно", "tx_details_not_available": "Недоступен", "smart_contract_interaction": "Взаимодействие со смарт-контрактом", "swaps_transaction": "Транзакция свопа", "bridge_transaction": "Мост", "approve": "Одобрить", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "Увеличить квоту", + "set_approval_for_all": "Одобрить все", "hash": "Хэш", "from": "От", "to": "В адрес", @@ -3899,15 +3925,15 @@ "amount": "Сумма", "fee": { "transaction_fee_in_ether": "Комиссия за транзакцию", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "Комиссия за транзакцию (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "Использовано газа (единицы)", + "gas_limit": "Лимит газа (единицы)", + "gas_price": "Цена газа (Гвей)", + "base_fee": "Базовая комиссия (Гвей)", + "priority_fee": "Плата за приоритет (GWEI)", "multichain_priority_fee": "Плата за приоритет", - "max_fee": "Max fee per gas", + "max_fee": "Макс. плата за газ", "total": "Итого", "view_on": "Посмотреть на", "view_on_etherscan": "Посмотреть на Etherscan", @@ -3923,13 +3949,13 @@ "nonce": "Одноразовый код", "from_device_label": "с этого устройства", "import_wallet_row": "Счет добавлен на это устройство", - "import_wallet_label": "Account added", + "import_wallet_label": "Счет добавлен", "import_wallet_tip": "Все будущие транзакции с этого устройства будут сопровождаться ярлыком «с этого устройства» рядом с отметкой времени. Для транзакций, совершенных до добавления счета, в этой истории не будет указано, какие исходящие транзакции были совершены с этого устройства.", "sign_title_scan": "Сканировать ", "sign_title_device": "с помощью аппаратного кошелька", "sign_description_1": "Подтвердите с помощью аппаратного кошелька", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "Нажмите «Получить подпись»", + "sign_get_signature": "Получить подпись", "transaction_id": "ID транзакции", "network": "Сеть", "request_from": "Запрос от", @@ -4032,7 +4058,7 @@ "title": "Сети", "other_networks": "Другие сети", "close": "Закрыть", - "status_ok": "All systems operational", + "status_ok": "Все системы работают", "status_not_ok": "В сети возникли роблемы", "want_to_add_network": "Хотите добавить эту сеть?", "add_custom_network": "Добавить пользовательскую сеть", @@ -4051,7 +4077,7 @@ "review": "Проверить", "view_details": "Просмотр подробностей", "network_details": "Сведения о сети", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "При выборе «Подтвердить» включается проверка сведений о сети. Вы можете отключить её в ", "network_settings_security_privacy": "Настройки > Безопасность и конфиденциальность", "network_currency_symbol": "Символ валюты", "network_block_explorer_url": "URL обозревателя блоков", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "Вредоносный сетевой провайдер может дезинформировать о состоянии блокчейна и записывать ваши действия в сети. Добавляйте только те пользовательские сети, которым доверяете.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Информация о сети", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "Дополнительная информация о сетях", "network_warning_desc": "Это сетевое подключение зависит от третьих сторон. Оно может быть менее надежным или позволять третьим лицам отслеживать активность.", "additonial_network_information_desc": "Некоторые из этих сетей являются зависимыми от третьих сторон. Соединения могут быть менее надежными или позволять третьим сторонам отслеживать активность.", "connect_more_networks": "Подключить больше сетей", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "Эта сеть устарела", "network_deprecated_description": "Сеть, к которой вы пытаетесь подключиться, больше не поддерживается в MetaMask.", "edit_networks_title": "Изменить сети", - "no_network_fee": "No network fee" + "no_network_fee": "Без комиссии сети" }, "permissions": { "title_this_site_wants_to": "Этот сайт хочет:", @@ -4111,11 +4137,11 @@ "network_connected": "сеть подключена ", "see_your_accounts": "Просматривать ваши счета и предлагать транзакции", "connected_to": "Подключен к ", - "manage_permissions": "Manage permissions", + "manage_permissions": "Управление разрешениями", "edit": "Изменить", "cancel": "Отмена", "got_it": "Понятно", - "connection_details_title": "Connection details", + "connection_details_title": "Сведения о подключении", "connection_details_description": "Вы подключились к этому сайту с помощью браузера MetaMask {{connectionDateTime}}", "title_add_network_permission": "Добавить сетевое разрешение", "add_this_network": "Добавить эту сеть", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "Разблокировать с помощью PIN-кода устройства?" }, "authentication": { - "auth_prompt_title": "Необходима аутентификация", - "auth_prompt_desc": "Авторизуйтесь, чтобы использовать MetaMask", - "fingerprint_prompt_title": "Необходима аутентификация", - "fingerprint_prompt_desc": "Разблокируйте MetaMask отпечатком пальца", - "fingerprint_prompt_cancel": "Отмена" + "auth_prompt_desc": "Авторизуйтесь, чтобы использовать MetaMask" }, "accountApproval": { "title": "ЗАПРОС НА УСТАНОВЛЕНИЯ СВЯЗИ", "walletconnect_title": "ЗАПРОС WALLETCONNECT", "action": "Подключиться к этому сайту?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "Чтобы восстановить соединение, выберите номер, который вы видите на сайте", + "action_reconnect_deeplink": "Хотите снова подключиться к этому сайту?", "connect": "Подключиться", "resume": "Возобновить", "cancel": "Отмена", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "Не запоминать это подключение к сайту", "disconnect": "Отключить", "permission": "Просмотр вашего", "address": "публичный адрес", @@ -4218,7 +4240,7 @@ "error_title": "Что-то пошло не так", "error_message": "Мы не смогли импортировать этот закрытый ключ. Убедитесь, что вы ввели его правильно.", "error_empty_message": "Вам нужно ввести свой закрытый ключ.", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "или отсканировать QR-код" }, "import_private_key_success": { "title": "Счет успешно импортирован!", @@ -4229,18 +4251,18 @@ "import_wallet_title": "Импортировать кошелек", "enter_srp_subtitle": "Введите свою секретную фразу для восстановления", "textarea_placeholder": "Добавьте пробел между всеми словами и убедитесь, что никто не смотрит", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "Введите секретную фразу для восстановления своего кошелька. Вы можете импортировать любую секретную фразу для восстановления Ethereum, Solana или Биткойна.", + "subtitle": "Вставьте свою секретную фразу для восстановления", "cta_text": "Продолжить", "paste": "Вставить", "clear": "Очистить все", "srp_number_of_words_option_title": "Количество слов", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "У меня есть фраза из 12 слов", + "24_word_option": "У меня есть фраза из 24 слов", "error_title": "Что-то пошло не так", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "Мы не смогли импортировать эту секретную фразу для восстановления. Убедитесь, что вы ввели ее правильно.", + "error_empty_message": "Вам необходимо ввести свою секретную фразу для восстановления.", + "error_number_of_words_error_message": "Секретные фразы для восстановления содержат 12 слов и 24 слова", "error_srp_is_case_sensitive": "Введены неверные данные! Секретная фраза для восстановления чувствительна к регистру.", "error_srp_word_error_1": "Слово ", "error_srp_word_error_2": " неверное или содержит орфографические ошибки.", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " неверные или содержат орфографические ошибки.", "error_invalid_srp": "Недействительная секретная фраза для восстановления", "error_duplicate_srp": "Эта секретная фраза для восстановления уже импортирована.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "Счет, которую вы пытаетесь импортировать, является дубликатом.", + "invalid_qr_code_title": "Недействительный QR-код", + "invalid_qr_code_message": "Этот QR-код не содержит действительную секретную фразу для восстановления", "success_1": "Кошелек", "success_2": "импортирован" }, @@ -4665,7 +4687,7 @@ "button": "Защитить кошелек" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "Не удалось обновить транзакцию", "text": "Хотите повторить попытку?", "cancel_button": "Отмена", "retry_button": "Повтор" @@ -4684,13 +4706,13 @@ "next": "Далее", "amount_placeholder": "0,00", "link_copied": "Ссылка скопирована в буфер обмена", - "send_link_title": "Send link", + "send_link_title": "Отправить ссылку", "description_1": "Ссылка запроса готова к отправке!", "description_2": "Отправьте эту ссылку другу, и она попросит его отправить", "copy_to_clipboard": "Копировать в буфер обмена", "qr_code": "QR-код", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "Отправить ссылку", + "request_qr_code": "QR-код запроса платежа", "balance": "Баланс" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "У вас нет активных сеансов", - "end_session_title": "End session", + "end_session_title": "Завершить сеанс", "end": "Завершить", "cancel": "Отмена", - "session_ended_title": "Session ended", + "session_ended_title": "Сеанс завершен", "session_ended_desc": "Выбранный сеанс прерван", "session_already_exist": "Этот сеанс уже подключен.", "close_current_session": "Закройте текущий сеанс перед началом нового." @@ -4765,15 +4787,14 @@ "on_network": "на {{networkName}}", "debit_card": "Дебетовая карта", "select_payment_method": "Выбрать способ оплаты", - "loading_quote": "Loading quote...", "pay_with": "Оплатить с помощью", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "Покупка через {{providerName}}.", + "change_provider": "Изменить поставщика.", "payment_error": "Возникла какая-то проблема. Повторите попытку.", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "Нет доступных способов оплаты.", "error_fetching_quotes": "Возникла какая-то проблема. Повторите попытку.", "no_quotes_available": "Нет доступных поставщиков.", - "providers": "Providers", + "providers": "Поставщики", "continue": "Продолжить", "powered_by_provider": "Работает на платформе {{provider}}", "purchased_currency": "Куплено {{currency}}", @@ -4871,6 +4892,15 @@ "log_out": "Выйти из {{provider}}", "logged_out_success": "Выполнен выход", "logged_out_error": "Ошибка выхода" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "самый низкий лимит продажи", "medium_sell_limit": "средний лимит продажи", "highest_sell_limit": "самый высокий лимит продажи", - "change": "Change", + "change": "Изменить", "continue_to_amount": "Перейти к сумме", "no_payment_methods_title": "В регионе {{regionName}} нет способов оплаты", "no_cash_destinations_title": "В регионе {{regionName}} нет пунктов приема наличных", @@ -5118,7 +5148,7 @@ "start_swapping": "Начать обмен" }, "feature_off_title": "Временно недоступен", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "Проводится техобслуживание службы свопов MetaMask. Зайдите позже.", "wrong_network_title": "Обмен недоступен", "wrong_network_body": "Вы можете обменивать токены только в главной сети Ethereum.", "unallowed_asset_title": "Невозможно обменять этот токен", @@ -5160,7 +5190,7 @@ "not_enough": "Недостаточно {{symbol}} для выполнения этого обмена", "max_slippage": "Максимальное проскальзывание", "max_slippage_amount": "Максимальное проскальзывание {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "Если курс изменяется между моментом размещения вашего ордера и его подтверждением, это называется «проскальзыванием». Ваш своп будет автоматически отменен, если проскальзывание превысит установленный вами параметр «максимальное проскальзывание».", "slippage_warning": "Убедитесь, что вы знаете, что делаете!", "allows_up_to_decimals": "{{symbol}} позволяет использовать до {{decimals}} десятичных знаков", "get_quotes": "Получить котировки", @@ -5199,7 +5229,7 @@ "edit": "Изменить", "quotes_include_fee": "Котировки включают комиссию MetaMask в размере {{fee}}%", "quotes_include_gas_and_metamask_fee": "В стоимость включен газ и комиссия MetaMask {{fee}}%", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "Нажмите для свопа", "swipe_to_swap": "Проведите для обмена", "swipe_to": "Проведите, чтобы", "swap": "Своп", @@ -5259,7 +5289,7 @@ "approve": "Одобрить использование {{sourceToken}} для обмена: до {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "Ожидающий своп ({{sourceToken}} на {{destinationToken}})", "swap_confirmed": "Обмен завершен ({{sourceToken}} на {{destinationToken}})", "approve_pending": "Одобрение использования {{sourceToken}} для обмена", "approve_confirmed": "Одобрено использование {{sourceToken}} для обмена" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "Раскрывающийся список «Сеть» перемещен в раздел «Активы»", "description_2": "Своп и Мост в одном простом процессе", - "description_3": "Streamlined send experience", + "description_3": "Оптимизирован процесс отправки", "description_4": "Свежий вид счета" }, "more_information": "Теперь вы можете сосредоточиться на своих токенах и активности, а не на сетях, стоящих за ними.", @@ -5406,21 +5436,21 @@ "aggressive_label": "Агрессивный", "aggressive_text": "Высокая вероятность даже на волатильных рынках. Используйте агрессивный подход, чтобы компенсировать скачки сетевого трафика из-за таких событий, как популярные NFT-дропы.", "market_label": "Рынок", - "market_text": "Use market for fast processing at current market price.", + "market_text": "Используйте рынок для быстрой обработки по текущей рыночной цене.", "low_label": "Низкий", "low_text": "Используйте низкий курс, чтобы дождаться более низкой цены. Временные оценки гораздо менее точны, поскольку цены в некоторой степени непредсказуемы.", "link": "Подробнее о настройке газа." }, "save": "Сохранить", "submit": "Отправить", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "Максимальная плата за приоритет низкая для текущих условий сети", + "max_priority_fee_high": "Максимальная плата за приоритет выше необходимой", + "max_priority_fee_speed_up_low": "Максимальная плата за приоритет должна быть не менее {{speed_up_floor_value}} Гвей (на 10 % выше, чем при первоначальной транзакции)", + "max_priority_fee_cancel_low": "Максимальная плата за приоритет должна быть не менее {{cancel_value}} Гвей (на 50 % выше, чем при первоначальной транзакции)", + "max_fee_low": "Максимальная комиссия низкая для текущих условий сети", + "max_fee_high": "Максимальная комиссия выше, чем необходимо", + "max_fee_speed_up_low": "Максимальная комиссия должна быть не менее {{speed_up_floor_value}} Гвей (на 10 % выше, чем при первоначальной транзакции)", + "max_fee_cancel_low": "Максимальная комиссия должна быть не менее {{cancel_value}} Гвей (на 50 % выше, чем при первоначальной транзакции)", "learn_more_gas_limit": "Лимит газа — это максимальное количество единиц газа, которое вы готовы использовать. Единицы газа являются множителем «Максимальной платы за приоритет» и «Максимальной комиссии».", "learn_more_max_priority_fee": "Максимальная плата за приоритет (также известная как «чаевые» майнеру) направляется непосредственно майнерам, чтобы они уделили приоритетное внимание вашей транзакции. ", "learn_more_max_fee": "Максимальная комиссия — это наибольшая сумма, которую вы заплатите (базовая комиссия + плата за приоритет).", @@ -5530,10 +5560,10 @@ "enable_remember_me_description": "Когда функция \"Запомнить меня\" включена, любой, у кого есть доступ к вашему телефону, может получить доступ к вашему счету MetaMask." }, "turn_off_remember_me": { - "title": "Введите пароль, чтобы отключить \"Запомнить меня\"", - "placeholder": "Пароль", - "description": "Если вы отключите эту опцию, с этого момента вам потребуется пароль для разблокировки MetaMask.", - "action": "Выключить \"Запомнить меня\"" + "title": "Выключить «Запомнить меня»", + "placeholder": "Подтвердите пароль", + "description": "После отключения функция «Запомнить меня» больше недоступна. Эта функция больше не работает, поэтому вы можете разблокировать MetaMask с помощью пароля или биометрических данных.", + "action": "Выключить «Запомнить меня»" }, "dapp_connect": { "warning": "Чтобы использовать эту функцию, обновите приложение до последней версии" @@ -5582,7 +5612,7 @@ "learn_more": "Подробнее" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "Подтвердите данные третьей стороны", "protect_from_scams": "Чтобы защитить себя от мошенников, найдите минутку, чтобы проверить данные третьей стороны.", "learn_to_verify": "Узнайте, как проверять данные третьей стороны", "spending_cap": "лимит расходования", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "Требуется восстановление", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "Что-то пошло не так, но не волнуйтесь! Попробуем восстановить ваш кошелек.", "restore_needed_action": "Восстановить кошелек" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Не удалось закрыть работающее приложение на вашем устройстве Ledger.", "ethereum_app_not_installed": "Приложение Ethereum не установлено.", "ethereum_app_not_installed_error": "Установите приложение Ethereum на своем устройстве Ledger.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "Приложение Ethereum не открыто", + "eth_app_not_open_message": "Установите приложение Ethereum на своем устройстве Ledger.", "ledger_is_locked": "Леджер заблокирован", "unlock_ledger_message": "Разблокируйте свое устройство Ledger", "cannot_get_account": "Не удается получить счет", @@ -5797,8 +5827,8 @@ "error_description": "Не удалось установить {{snap}}." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "Ежегодный бонус, который можно ежедневно выводить из вашего кошелька.", + "earn_a_percentage_bonus": "Заработайте бонус в размере {{percentage}}%", "claimable_bonus": "Встребуемый бонус", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "Время, необходимое для вывода вашего токена из протокола и возврата его в ваш кошелек", "receive": "Этот токен используется для отслеживания ваших активов и вознаграждений. Не переводите его и не торгуйте им, иначе вы не сможете вывести свои активы.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "Ваш фактор здоровья измеряет риск ликвидации", "above_two_dot_zero": "Выше 2,0", "safe_position": "Безопасное положение", "between_one_dot_five_and_2_dot_zero": "Между 1,5–2,0", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "Средний риск ликвидации", "below_one_dot_five": "Ниже 1,5", "higher_liquidation_risk": "Более высокий риск ликвидации" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "Почему я не могу снять весь свой баланс?", "your_withdrawal_amount_may_be_limited_by": "Сумма снимаемых средств может быть ограничена", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "Ликвидность пула", "not_enough_funds_available_in_the_lending_pool_right_now": "На данный момент в кредитном пуле недостаточно средств.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "Текущие заимствованные позиции", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "Вывод средств может подвергнуть ваши существующие кредитные позиции риску ликвидации." } }, @@ -5998,11 +6028,11 @@ "earn_button": "Заработать" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "Выполняйте стейкинг TRX и зарабатывайте", + "stake_any_amount": "Выполняйте стейкинг любой суммы TRX.", "earn_trx_rewards": "Зарабатывайте вознаграждения в TRX.", "earn_trx_rewards_description": "Начните зарабатывать сразу после начала стейкинга. Вознаграждения начисляются автоматически.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "Отменяйте стейкинг в любое время. Обычно обработка отмены стейкинга занимает до 14 дней." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "Примерная плата за газ", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "Плата за газ переводится криптомайнерам, которые обрабатывают транзакции в сети Ethereum. MetaMask не получает прибыли от платы за газ.", "gas_fluctuation": "Плата за газ является примерной и будет меняться в зависимости от сетевого трафика и сложности транзакции.", "gas_learn_more": "Подробнее о плате за газ" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "Вход через", "spender": "Лицо, производящее расходы", "now": "Сейчас", - "switching_to": "Switching to", + "switching_to": "Переключение на", "bridge_estimated_time": "Прим. время", "pay_with": "Оплатить с помощью", - "receive_as": "Receive", + "receive_as": "Получить", "total": "Итого", - "you_receive": "You'll receive", + "you_receive": "Вы получите", "transaction_fee": "Комиссия за транзакцию", - "transaction_fees": "Transaction fees", + "transaction_fees": "Комиссии за транзакцию", "metamask_fee": "Комиссия MetaMask", "network_fee": "Комиссия сети", "bridge_fee": "Комиссия поставщика моста" @@ -6234,7 +6264,7 @@ "transaction_fee": "Мы обменяем ваши токены на USDC.e в Polygon, сети, используемой функцией «Прогнозы». Поставщики услуг свопов могут взимать комиссию, но MetaMask не взимает ее." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMask автоматически обменяет ваш токен на желаемый. При обмене на MUSD комиссия MetaMask не взимается." }, "musd_conversion": { "transaction_fee": "В стоимость конвертации mUSD входят расходы сети, а также могут входить сборы поставщика." @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "Этот сайт запрашивает вашу подпись", "transaction_tooltip": "Этот сайт запрашивает транзакцию с вами", "details": "Подробности", - "qr_get_sign": "Get signature", + "qr_get_sign": "Получить подпись", "qr_scan_text": "Отсканируйте с помощью вашего аппаратного кошелька", "sign_with_ledger": "Подписать с помощью Ledger", "smart_account": "Смарт-счет", "smart_contract": "Смарт-контракта", - "standard_account": "Standard account", + "standard_account": "Стандартный счет", "siwe_message": { "url": "URL", "network": "Сеть", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "Смарт-счет", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "Стандартный счет", "switch": "Сменить", "switchBack": "Переключиться обратно", "includes_transaction": "Включает {{transactionCount}} транзакции(-ий)", @@ -6307,9 +6337,9 @@ "cancel": "Отмена", "description": "Введите сумму, которую вы считаете приемлемой для расходов от вашего имени.", "invalid_number_error": "Лимит расходов должен быть числом", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "Лимит расходов не может быть пустым", + "no_extra_decimals_error": "Лимит расходов не может содержать больше знаков после запятой, чем токен", + "no_zero_error": "Лимит расходов не может быть равен 0", "no_zero_error_decrease_allowance": "Лимит расходов 0 не влияет на метод «decreaseAllowance»", "no_zero_error_increase_allowance": "Лимит расходов 0 не влияет на метод «increaseAllowance»", "save": "Сохранить", @@ -6336,7 +6366,7 @@ "transferRequest": "Запрос на перевод", "nested_transaction_heading": "Транзакция {{index}}", "transaction": "Защита", - "available_balance": "Available balance: ", + "available_balance": "Доступный баланс: ", "edit_amount_done": "Продолжить", "deposit_edit_amount_done": "Внести средства", "deposit_edit_amount_predict_withdraw": "Вывести средства", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "Положения и условия", "select_token": "Выберите токен", "no_tokens_found": "Токены не найдены", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "Не удалось найти токены с этим именем. Попробуйте другой поисковый запрос.", "select_network": "Выбрать сеть", "all_networks": "Все сети", "num_networks": "{{numNetworks}} сети(-ей)", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "Выбрать все", "see_all": "Смотреть все", "all": "Все", - "more_networks": "+{{count}} more", + "more_networks": "+еще {{count}}", "apply": "Применить", "slippage": "Проскальзывание", "slippage_info": "Если цена меняется между моментом размещения и подтверждения вашего ордера, это называется «проскальзывание». Ваш своп будет автоматически отменен, если проскальзывание превысит значение, которое вы установили здесь .", @@ -6392,7 +6422,7 @@ "quote_info_title": "Курс", "network_fee_info_title": "Комиссия сети", "network_fee_info_content": "Комиссии сети зависят от загруженности сети и сложности транзакции.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "Комиссию сети оплачивает MetaMask, поэтому вы можете совершать транзакции, даже если у вас на счету нет {{nativeToken}}.", "points": "Прим. баллов", "points_tooltip": "Баллы", "points_tooltip_content_1": "Баллы — это способ заработать Вознаграждения MetaMask за выполнение транзакций, например, при обмене, создании мостов или торговле перпами.", @@ -6406,7 +6436,7 @@ "select_recipient": "Выберите получателя", "external_account": "Внешний счет", "error_banner_description": "Этот торговый маршрут сейчас недоступен. Попробуйте изменить сумму, сеть или токен, и мы найдем лучший вариант.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "Этот торговый маршрут в данный момент недоступен. Попробуйте изменить сумму, сеть или токен, и мы найдем наиболее подходящий вариант.\n\nОбратите внимание: при попытке торговли токенизированными акциями Ondo вы можете столкнуться с географическими ограничениями, например, с проблемами с доступом из США, ЕС, Великобритании и Бразилии.", "insufficient_funds": "Недостаточно средств", "insufficient_gas": "Недостаточно газа", "select_amount": "Выбрать сумму", @@ -6417,9 +6447,9 @@ "title": "Мост", "submitting_transaction": "Выполняется отправка", "fetching_quote": "Получение котировки", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "Включает {{feePercentage}}% комиссии MetaMask.", "no_mm_fee": "Без комиссии MM", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "Без комиссии MetaMask за обмен на {{destTokenSymbol}}.", "hardware_wallet_not_supported": "Аппаратные кошельки пока не поддерживаются. Используйте горячий кошелек, чтобы продолжить.", "hardware_wallet_not_supported_solana": "Аппаратные кошельки пока не поддерживаются для Solana. Используйте горячий кошелек, чтобы продолжить.", "price_impact_info_title": "Влияние на цену", @@ -6432,17 +6462,24 @@ "approval_needed": "Одобряет токен для обмена.", "approval_tooltip_title": "Предоставить точный доступ", "approval_tooltip_content": "Вы разрешаете доступ к указанной сумме: {{amount}} {{symbol}}. Контракт не будет использовать дополнительные средства.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "Полученный минимум", + "minimum_received_tooltip_title": "Полученный минимум", "minimum_received_tooltip_content": "Минимальная сумма, которую вы получите, если цена изменится во время обработки вашей транзакции, рассчитывается исходя из вашей допустимой задержки. Это оценка, предоставленная нашими поставщиками ликвидности. Окончательные суммы могут отличаться.", + "market_closed": { + "title": "Рынок закрыт", + "description": "Рынок, обеспечивающий этот токен, в настоящее время закрыт. Токены можно переводить в блокчейне в любое время.", + "learn_more": "Узнайте подробнее", + "learn_more_url": "https://status.ondo.finance/market", + "done": "Готово" + }, "submit": "Отправить", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "Ваша транзакция не будет завершена, если изменение цены превысит допустимый процент проскальзывания.", "cancel": "Отмена", "confirm": "Подтвердить", "exceeding_upper_slippage_warning": "Высокое проскальзывание может привести к невыгодному свопу", "exceeding_lower_slippage_warning": "Низкое проскальзывание может привести к невыгодному свопу", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "Введите значение более {{value}}%", + "exceeding_upper_slippage_error": "Вы не можете ввести значение более {{value}}%", "custom": "Пользовательские" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "Восстановление кошелька", "login_with_social": "Войти через социальные сети", "setup": "Настроить", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "Секретная фраза для восстановления {{num}}", "back_up": "Создать резервную копию", "reveal": "Показать", "social_recovery_title": "ВОССТАНОВЛЕНИЕ {{authConnection}}", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "Введите пароль", "description": "Введите пароль от своего кошелька, чтобы просмотреть реквизиты карты.", + "description_unfreeze": "Введите пароль от своего кошелька, чтобы возобновить расходы по карте.", "placeholder": "Пароль", "confirm": "Подтвердить", "cancel": "Отмена", @@ -7001,6 +7039,7 @@ "enable_card_error": "Не удалось активировать карту. Попробуйте позже.", "view_card_details_error": "Не удалось загрузить реквизиты карты. Повторите попытку.", "biometric_verification_required": "Для просмотра реквизитов карты требуется аутентификация.", + "unfreeze_auth_required": "Для возобновления расходов по карте и совершения покупок требуется аутентификация.", "warnings": { "close_spending_limit": { "title": "Вы приближаетесь к своему лимиту расходов", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "Ваша карта заблокирована", - "description": "Свяжитесь со службой поддержки для ее разблокировки" + "description": "Ваша карта временно заблокирована. Вы можете разблокировать её в любое время." }, "blocked": { "title": "Ваша карта заблокирована", @@ -7068,7 +7107,14 @@ "travel_description": "Бронируйте отели со скидками до 70%", "card_tos_title": "Положения и условия", "order_metal_card": "Металлическая карта", - "order_metal_card_description": "Закажите свою физическую металлическую карту прямо сейчас" + "order_metal_card_description": "Закажите свою физическую металлическую карту прямо сейчас", + "freeze_card": "Заблокировать карту", + "unfreeze_card": "Разблокировать карту", + "freeze_card_description": "Приостановить все расходы по вашей карте", + "unfreeze_card_description": "Возобновите все расходы по вашей карте", + "freeze_error": "Не удалось обновить статус карты. Попробуйте еще раз.", + "freeze_success": "Карта успешно заблокирована", + "unfreeze_success": "Карта успешно разблокирована" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "Повторная отправка станет доступна через {{seconds}} секунд(-ы)" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "Добавить в {{walletName}}", + "adding_to_wallet": "Добавление в {{walletName}}...", + "continue_setup": "Продолжить настройку {{walletName}}", + "wallet_not_available": "{{walletName}} недоступен", + "already_in_wallet": "Уже в {{walletName}}", + "success_title": "Карта добавлена!", + "success_message": "Ваша карта MetaMask добавлена в {{walletName}}.", + "error_title": "Невозможно добавить карту", + "error_wallet_not_available": "{{walletName}} недоступен на этом устройстве. Убедитесь, что у вас настроен {{walletName}}.", + "error_wallet_not_initialized": "{{walletName}} не инициализирован. Настройте свой кошелек и попробуйте снова.", "error_card_already_in_wallet": "Эта карта уже добавлена в {{walletName}}.", "error_card_pending": "Ваша карта настраивается в {{walletName}}. Зайдите через несколько минут.", "error_card_suspended": "Ваша карта в {{walletName}} заблокирована. Обратитесь в службу поддержки за помощью.", "error_card_not_eligible": "Данная карта не подходит для пополнения баланса мобильного кошелька.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "Не удалось обновить данные карты. Попробуйте еще раз.", "error_invalid_card_data": "Неверные данные карты. Проверьте данные карты и повторите попытку.", "error_card_not_found": "Карта не найдена. Повторите попытку.", "error_card_provider_not_found": "Поставщик карт недоступен в вашем регионе.", "error_card_id_mismatch": "Не удалось проверить карту. Повторите попытку.", "error_card_not_active": "Ваша карта неактивна. Сначала активируйте ее.", "error_network": "Произошла ошибка сети. Проверьте подключение и повторите попытку.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "Время выполнения запроса истекло. Попробуйте снова.", + "error_server": "Возникла ошибка сервера. Повторите попытку позже.", + "error_unknown": "Произошла непредвиденная ошибка. Попробуйте еще раз или обратитесь в службу поддержки.", + "error_platform_not_supported": "Данная платформа не поддерживает предоставление мобильных кошельков.", "try_again": "Повторить попытку", "cancel": "Отмена" } @@ -7299,7 +7345,7 @@ "main_title": "Награды", "referral_title": "Рефералы", "tab_overview_title": "Обзор", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "Снимки", "tab_activity_title": "Деятельность", "referral_stats_earned_from_referrals": "Заработано на рефералах", "referral_stats_referrals": "Рефералы", @@ -7353,7 +7399,7 @@ "verifying_rewards": "Мы проверяем правильность всех данных, прежде чем вы сможете получить свои бонусы." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "Заработанные баллы" }, "onboarding": { "not_supported_region_title": "Регион не поддерживается", @@ -7431,7 +7477,7 @@ "show_less": "Показать меньше", "linking_progress": "Добавляются счета... ({{current}} из {{total}})", "accounts_linked_count": "{{linked}}/{{total}} зарегистрировано", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "Добавить все счета" }, "referred_by_code": { "title": "Реферальный код", @@ -7514,7 +7560,7 @@ "claim_label": "Получить", "claimed_label": "Получено", "reward_claimed": "Вознаграждение получено", - "time_left": "{{time}} left", + "time_left": "Осталось {{time}}", "expired": "Просрочено" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "Не удалось использовать", "redeem_failure_description": "Повторите попытку позже.", "reward_details": "Сведения о бонусе", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "Выберите счет, на который вы хотите получить этот бонус." }, "animation": { "could_not_load": "Ошибка загрузки" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", + "starts_date": "Начинается {{date}}", + "ends_date": "Заканчивается {{date}}", + "results_coming_soon": "Скоро появятся результаты", + "tokens_on_the_way": "Токены в пути", + "pill_up_next": "Далее", + "pill_live_now": "Уже активно", "pill_calculating": "Расчет", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "pill_results_ready": "Результаты готовы", + "pill_complete": "Завершено" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "Снимки", + "error_title": "Не удалось загрузить снимки", + "error_description": "Нам не удалось загрузить снимки. Попробуйте еще раз.", "retry_button": "Повтор" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "Активно", + "upcoming_title": "Далее", + "previous_title": "Предыдущее", + "empty_state": "Нет доступных снимков", + "error_title": "Не удалось загрузить снимки", + "error_description": "Нам не удалось загрузить снимки. Попробуйте еще раз.", "retry_button": "Повтор", - "refreshing": "Refreshing..." + "refreshing": "Обновление..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "Одобрить {{approveSymbol}}", "bridge_approval_loading": "Одобрить", "bridge_send": "Мост {{sourceSymbol}} из {{sourceChain}}", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "Отправить через мост", "bridge_receive": "Получить {{targetSymbol}} в {{targetChain}}", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "Получить через мост", "default": "Транзакция", "musd_convert_send": "{{sourceSymbol}} отправлено от {{sourceChain}}", "musd_claim": "Получить mUSD", @@ -7607,20 +7653,20 @@ "description": "Установление соединения с {{dappName}}..." }, "show_error": { - "title": "Connection error", + "title": "Ошибка соединения", "description": "Не удалось установить соединение. Попробуйте ещё раз." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "В одобрении отказано", + "description": "Пользователь отклонил запрос." }, "show_return_to_app": { "title": "Успех", "description": "Чтобы продолжить, вернитесь в приложение." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "Соединение не найдено", + "description": "Для продолжения установите новое соединение через приложение." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "Обзор", + "trending_tokens": "Популярные токены", "price_change": "Изменение цены", "all_networks": "Все сети", - "24h": "24h", + "24h": "24 ч", "time": "Время", "24_hours": "24 часов", "6_hours": "6 часов", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 час", + "5_minutes": "5 минут", "networks": "Сети", "sort_by": "Сортировать по", "volume": "Объем", @@ -7650,32 +7696,48 @@ "high_to_low": "От высокого к низкому", "low_to_high": "От низкого к высокому", "apply": "Применить", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "Поиск токенов, сайтов и URL-адресов", "cancel": "Отмена", "perps": "Перпы", "predictions": "Прогнозы", - "no_results": "No results found", + "no_results": "Результаты не найдены", "sites": "Сайты", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "Популярные сайты", + "search_sites": "Поиск по сайтам", + "enable_basic_functionality": "Включить базовый функционал", + "basic_functionality_disabled_title": "Обзор недоступен", + "basic_functionality_disabled_description": "Мы не можем получить необходимые метаданные, когда базовый функционал отключен.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "Популярные токены недоступны", + "description": "В данный момент мы не можем получить доступ к этой странице", "try_again": "Повторить попытку" }, "empty_search_result_state": { "title": "Токены не найдены", - "description": "We were not able to find this token" + "description": "Нам не удалось найти этот токен" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "Обновление готово", + "description_ios": "Мы внесли ряд важных исправлений. Перезагрузите страницу, чтобы получить последнюю версию MetaMask.", + "description_android": "Мы внесли ряд важных исправлений. Закройте и снова откройте MetaMask, чтобы применить обновление.", "primary_action_reload": "Перезагрузить", "primary_action_acknowledge": "Понятно" + }, + "homepage": { + "sections": { + "tokens": "Токены", + "perpetuals": "Бессрочные контракты", + "predictions": "Прогнозы", + "defi": "DeFi", + "nfts": "NFT", + "import_nfts": "Импорт NFT", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/tl.json b/locales/languages/tl.json index 25c7118c7bc..af54d487022 100644 --- a/locales/languages/tl.json +++ b/locales/languages/tl.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "permanenteng buburahin", "reset_wallet_desc_2": "sa MetaMask gamit ang device na ito. Hindi ito maaaring bawiin.", "reset_wallet_desc_login": "Para ibalik ang iyong wallet, maaari mong gamitin ang iyong Lihim na Parirala sa Pagbawi, o ang password ng iyong Google o Apple account. Wala sa MetaMask ang impormasyong ito.", - "reset_wallet_desc_srp": "Para maibalik ang wallet mo, siguraduhing mayroon ka ng Lihim na Parirala sa Pagbawi (Secret Recovery Phrase) mo. Wala sa MetaMask ang impormasyong ito." + "reset_wallet_desc_srp": "Para maibalik ang wallet mo, siguraduhing mayroon ka ng Lihim na Parirala sa Pagbawi (Secret Recovery Phrase) mo. Wala sa MetaMask ang impormasyong ito.", + "biometric_authentication_cancelled": "Kinansela ang pag-authenticate sa biometric", + "biometric_authentication_cancelled_title": "Nabigo ang pag-setup sa biometric", + "biometric_authentication_cancelled_description": "Paki-setup muli ang pag-authenticate sa biometric mula sa mga setting.", + "biometric_authentication_cancelled_button": "Kumpirmahin" }, "connect_hardware": { "title_select_hardware": "Magkonekta ng wallet na hardware", @@ -1040,7 +1044,7 @@ "title": "Halagang idedeposito", "get_usdc_hyperliquid": "Kumuha ng USDC • Hyperliquid", "insufficient_funds": "Hindi sapat ang mga pondo", - "no_funds_available": "Walang available na mga pondo. Magdeposito muna.", + "no_funds_available": "Hindi sapat ang mga pondong available. Magdeposito ng mga pondo o pumili ng ibang paraan ng pagbabayad", "enter_amount": "Ilagay ang halaga", "fetching_quote": "Kumukuha ng quote", "submitting": "Isinusumite ang transaksyon", @@ -1970,8 +1974,8 @@ "trade_again": "Mag-trade ulit", "activity": { "deposit_title": "Magdeposito", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "Nagdeposito ng {{amount}} {{symbol}}", + "withdrew_amount": "Nag-withdraw ng {{amount}} {{symbol}}", "status_completed": "Nakumpleto", "status_failed": "Nabigo", "status_pending": "Nakabinbin" @@ -2051,6 +2055,16 @@ "referral_code_text": "Gamitin ang referral code ko para makakuha ng dagdag na reward." } }, + "market_insights": { + "title": "Mga market insight", + "updated_ago": "Na-update {{time}}", + "disclaimer": "Mga AI insight. Hindi payong pinansyal.", + "whats_driving_price": "Ano ang nagtutulak sa presyo?", + "what_people_saying": "Ano ang sinasabi ng mga tao", + "trade_button": "Mag-trade", + "sources_count": "+{{count}} (na) pinagmulan", + "sources_title": "Mga pinagmulan" + }, "predict": { "title": "Mga Predisksyon ng MetaMask", "prediction_markets": "Mga market ng prediksyon", @@ -2384,8 +2398,8 @@ "no_available_tokens": "Hindi mo ba nakikita ang iyong token?", "add_tokens": "Mag-import ng mga token", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "{{tokenCount}} (na) bagong {{tokensLabel}} ang natagpuan sa account na ito", "token_toast": { "tokens_imported_title": "Na-import na mga token", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "Hindi maaaring walang laman ang smga decimal ng token.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "Wala kaming mahanap na anumang token na may ganyang pangalan.", + "tokens_empty_description": "Search for any token and import it", "select_token": "Pumili ng token", "address_must_be_smart_contract": "Natukoy ang personal na address. Ilagay ang address ng kontrata ng token.", "billion_abbreviation": "B", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "Idiskonekta ang lahat ng account", "deceptive_site_ahead": "Papunta sa isang mapanlinlang na site", "deceptive_site_desc": "Ang site na iyong sinusubukang bisitahin ay hindi ligtas. Maaari kang linlangin ng mga umaatake na gumawa ng mapanganib na bagay.", + "malicious_site_detected": "Natukoy ang mapanganib na site", + "malicious_site_warning": "Kapag kumonekta ka sa site na ito, maaaring mawala sa iyo ang lahat ng asset mo.", + "connect_anyway": "Kumonekta Pa Rin", "learn_more": "Matuto pa", "advisory_by": "Payo na ibinigay ng Ethereum Phishing Detector at PhishFort", "potential_threat": "Kabilang sa potensyal na panganib ay", @@ -2846,7 +2864,11 @@ "permissions": "Mga Pahintulot", "card_title": "MetaMask Card", "settings": "Mga Setting", - "log_out": "Mag-log out" + "networks": "Mga Network", + "log_out": "Mag-log out", + "notifications": "Mga notipikasyon", + "buy": "Bumili", + "scan": "I-scan" }, "app_settings": { "enabling_notifications": "Pinapagana ang mga notipikasyon...", @@ -2870,6 +2892,8 @@ "state_logs": "Mga log ng estado", "add_network_title": "Magdagdag ng network", "auto_lock": "Awtomatikong i-lock", + "enable_device_authentication": "Paganahin ang Pag-authenticate sa Device", + "enable_device_authentication_desc": "Gamitin ang biometrics o passcode ng device mo para i-unlock ang MetaMask.", "auto_lock_desc": "Piliin kung gaano katagal bago awtomatikong mala-lock ang application.", "state_logs_desc": "Makakatulong ito sa MetaMask na ma-debug ang anumang isyung kaharapin mo. Pakipadala ito sa suporta sa MetaMask sa pamamagitan ng icon na hamburger > Magpadala ng Feedback, o mag-reply sa kasalukuyan mong ticket kung mayroon.", "autolock_immediately": "Kaagad", @@ -2975,6 +2999,11 @@ "add_rpc_url": "Magdagdag ng RPC URL", "add_block_explorer_url": "Magdagdag ng URL ng block explorer", "networks_desc": "Magdagdag at mag-edit ng mga custom na RPC network", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "Pumili ng mga network", + "networks_no_results": "Walang nahanap na network", "network_name_label": "Pangalan ng network", "network_name_placeholder": "Pangalan ng network (opsyonal)", "network_rpc_url_label": "RPC URL", @@ -2991,7 +3020,16 @@ "network_other_networks": "Iba pang mga network", "network_rpc_networks": "Mga RPC network", "network_add_network": "Magdagdag ng network", + "add_chain_title": "Magdagdag ng network", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "Subukang muli", + "add_chain_added": "Added", + "add_chain_or": "o", + "add_chain_custom_link": "Magdagdag ng custom na network", "network_add_custom_network": "Magdagdag ng custom na network", + "network_add_test_network": "Add a test network", "network_add": "Idagdag", "network_save": "I-save", "remove_network_title": "Gusto mo bang alisin ang network na ito?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "OK", "title": "Hindi makakonekta ang account", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "Paki-scan ang QR code sa site na ito para muling kumonekta sa MetaMask" }, "app_information": { "title": "Impormasyon", @@ -3379,6 +3417,7 @@ "sell_description": "Magbenta ng crypto kapalit ng cash" }, "asset_overview": { + "market_closed": "Sarado ang market", "send_button": "Ipadala", "buy_button": "Bumili", "cash_buy_button": "Mag-cash Buy", @@ -3399,19 +3438,6 @@ "bridge": "Mag-bridge", "earn": "Kumita", "convert_to_musd": "I-convert sa mUSD", - "merkl_rewards": { - "annual_bonus": "{{apy}}% bonus", - "claimable_bonus": "Naki-claim na bonus", - "claimable_bonus_tooltip_description": "Kini-claim ang mga mUSD na bonus sa Linea.", - "terms_apply": "Nalalapat ang mga tuntunin.", - "ok": "OK", - "claim": "I-claim", - "processing_claim": "Pinoproseso ang claim...", - "claim_on_linea_title": "I-claim ang mga bonus sa Linea", - "claim_on_linea_description": "Ibibigay ang bonus mo sa Linea, hiwalay sa balanse ng Ethereum mUSD mo.", - "continue": "Magpatuloy", - "unexpected_error": "Hindi inaasahang error. Pakisubukan muli." - }, "tron": { "daily_resource_new_energy": "Bagong enerhiya araw-araw", "sufficient_to_cover": "Sapat para matugunan", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "Nakopya sa clipboard ang address ng token" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "Di-wastong QR code", "invalid_qr_code_message": "Hindi wasto ang QR code na sinusubukan mong i-scan.", "allow_camera_dialog_title": "Payagan ang access sa camera", "allow_camera_dialog_message": "Kailangan namin ang iyong pahintulot na mag-scan ng mga QR code", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "Mukhang sinusubukan mong mag-sync sa extension. Para magawa ito, kakailanganin mong burahin ang iyong kasalukuyang wallet. \n\nSa oras na magbura o mag-install ka ng bagong bersyon ng app, piliin ang opsyong \"I-sync gamit ang MetaMask Extension\". Mahalaga! Bago burahin ang iyong wallet, tiyaking na-back up mo ang iyong Lihim na Parirala sa Pagbawi.", "not_allowed_error_title": "I-on ang access sa camera", "not_allowed_error_desc": "Para mag-scan ng QR code, kailangan mong bigyan ang MetaMask ng access sa camera mula sa menu ng mga setting ng iyong device.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "Hindi nakikilalang QR code", "unrecognized_address_qr_code_desc": "Paumanhin, ang QR code na ito ay hindi nauugnay sa isang address ng account o isang address ng kontrata.", "url_redirection_alert_title": "Bibisitahin mo ang isang panlabas na link", "url_redirection_alert_desc": "Maaaring gamitin ang mga link upang subukang manlinlang o mag-phish ng mga tao, kaya siguraduhing bisitahin lamang ang mga website na pinagkakatiwalaan mo.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "Hindi mo pag-aari ang collectible na ito", "known_asset_contract": "Kilalang address ng kontrata ng asset", "max": "Max", - "recipient_address": "Recipient address", + "recipient_address": "Address ng tatanggap", "required": "Kinakailangan", "to": "Sa/Kay", "total": "Kabuuan", @@ -3641,7 +3667,7 @@ "nevermind": "Huwag na lang", "edit_network_fee": "I-edit ang bayad sa gas", "edit_priority": "I-edit ang priyoridad", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "Bayad sa pagkansela ng gas", "gas_speedup_fee": "Bayarin sa Pagpapabilis ng Gas", "use_max": "Gamitin ang max", "set_gas": "Itakda", @@ -3650,7 +3676,7 @@ "transaction_fee": "Bayad sa gas", "transaction_fee_less": "Walang bayarin", "total_amount": "Kabuuang halaga", - "view_data": "View data", + "view_data": "Tingnan ang data", "adjust_transaction_fee": "Isaayos ang bayad sa transaksyon", "could_not_resolve_ens": "Hindi maresolba ang ENS", "asset": "Asset", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "Para i-browse ang desentralisadong web, magdagdag ng bagong tab", "got_it": "Nakuha ko", "max_tabs_title": "Naabot na ang maximum na dami ng tab", - "max_tabs_desc": "Sa kasalukuyan, 5 bukas na tab lang ang nasusuportahan namin sa isang pagkakataon. Isara ang mga ginagamit na tab bago magdagdag ng bago.", + "max_tabs_desc": "Sa kasalukuyan, 20 bukas na tab lang ang nasusuportahan namin sa isang pagkakataon. Isara ang mga ginagamit na tab bago magdagdag ng bago.", "failed_to_resolve_ens_name": "Hindi namin maresolba ang ENS name na iyon", "remove_bookmark_title": "Alisin ang paborito", "remove_bookmark_msg": "Gusto mo ba talagang alisin ang site na ito sa mga paborito mo?", @@ -3828,7 +3854,7 @@ "cancel_button": "Kanselahin" }, "approval": { - "title": "Confirm transaction" + "title": "Kumpirmahin ang transaksyon" }, "approve": { "title": "Aprubahan", @@ -3839,39 +3865,39 @@ "unavailable": "Hindi available", "tx_review_confirm": "Kumpirmahin", "tx_review_transfer": "Maglipat", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "Deployment ng kontrata", + "tx_review_transfer_from": "I-transfer mula sa/kay", + "tx_review_unknown": "Hindi alam na paraan", "tx_review_approve": "Aprubahan", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "Dagdag na allowance", + "tx_review_set_approval_for_all": "Itakda ang pag-apruba para sa lahat", + "tx_review_staking_claim": "Claim sa pag-stake", "tx_review_staking_deposit": "Deposito sa pag-stake", "tx_review_staking_unstake": "I-unstake", "tx_review_lending_deposit": "Deposito sa pagpapautang", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "Pag-withdraw sa pagpapautang", "tx_review_perps_deposit": "Mga pinondohang perp", "tx_review_predict_deposit": "Mga pinondohang prediksyon", "tx_review_predict_claim": "Mga na-claim na panalo", "tx_review_predict_withdraw": "Pagbawi ng mga prediksyon", "tx_review_musd_conversion": "Palitan ng mUSD", "claim": "I-claim", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "Naipadala ang ETH", + "self_sent_ether": "Nagpadala ka sa sarili mo ng ETH", + "received_ether": "Natanggap ang ETH", "sent_dai": "Nagpadala ng DAI", "self_sent_dai": "Nagpadala ka sa sarili mo ng DAI", "received_dai": "Nakatanggap ng DAI", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "Nagpadala ng mga token", + "received_tokens": "Nakatanggap ng mga token", "ether": "ETH", "sent_unit": "Nagpadala ng {{unit}}", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "Ipinadala sa sarili mo {{unit}}", "received_unit": "Nakatanggap ng {{unit}}", "sent_collectible": "Nagpadala ng collectible", "received_collectible": "Nakatanggap ng collectible", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "Magpadala ng ETH", + "send_unit": "Ipadala {{unit}}", "send_collectible": "Magpadala ng collectible", "receive_collectible": "Tumanggap ng collectible", "sent": "Naipadala", @@ -3881,8 +3907,8 @@ "send": "Ipadala", "redeposit": "Magdepositong muli", "interaction": "Interaksyon", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", + "contract_deploy": "Deployment ng kontrata", + "to_contract": "Bagong kontrata", "mint": "Mint", "tx_details_free": "Libre", "tx_details_not_available": "Hindi available", @@ -3890,8 +3916,8 @@ "swaps_transaction": "Transaksyon sa mga swap", "bridge_transaction": "Mag-bridge", "approve": "Aprubahan", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "Dagdag na allowance", + "set_approval_for_all": "Itakda ang pag-apruba para sa lahat", "hash": "Hash", "from": "Mula sa/kay", "to": "Sa/Kay", @@ -3899,15 +3925,15 @@ "amount": "Halaga", "fee": { "transaction_fee_in_ether": "Bayarin sa transaksyon", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "Bayarin sa transaksyon (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", + "gas_used": "Hindi nagamit na gas (mga unit)", + "gas_limit": "Limitasyon ng gas (mga unit)", + "gas_price": "Presyo ng gas (GWEI)", "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "priority_fee": "Bayad sa priyoridad (GWEI)", "multichain_priority_fee": "Bayad sa priyoridad", - "max_fee": "Max fee per gas", + "max_fee": "Max na bayad bawat gas", "total": "Kabuuan", "view_on": "Tingnan sa", "view_on_etherscan": "Tingnan sa Etherscan", @@ -3923,13 +3949,13 @@ "nonce": "Nonce", "from_device_label": "mula sa device na ito", "import_wallet_row": "Naidagdag ang account sa device na ito", - "import_wallet_label": "Account added", + "import_wallet_label": "Naidagdag ang account", "import_wallet_tip": "Ang lahat ng transaksyon sa hinaharap na gagawin mula sa device na ito ay may kasamang label na \"mula sa device na ito\" sa tabi ng timestamp. Para sa mga transaksyong may petsa bago idagdag ang account, hindi isasaad sa history na ito kung aling papalabas na transaksyon ang nagmula sa device na ito.", "sign_title_scan": "I-scan ", "sign_title_device": "gamit ang iyong wallet na hardware", "sign_description_1": "Pagkatapos mong pumirma gamit ang iyong wallet na hardware,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "I-tap ang kumuha ng lagda", + "sign_get_signature": "Kumuha ng lagda", "transaction_id": "ID ng Transaksyon", "network": "Network", "request_from": "Kahilingan mula sa/kay", @@ -4032,7 +4058,7 @@ "title": "Mga Network", "other_networks": "Iba pang mga network", "close": "Isara", - "status_ok": "All systems operational", + "status_ok": "Gumagana ang lahat ng system", "status_not_ok": "Nagkakaroon ng ilang isyu ang network", "want_to_add_network": "Gusto mo bang idagdag ang network na ito?", "add_custom_network": "Magdagdag ng custom na network", @@ -4051,7 +4077,7 @@ "review": "Suriin", "view_details": "Tingnan ang mga detalye", "network_details": "Mga detalye ng network", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "Ang pagpili sa kumpirmahin ay magbubukas ng pagsusuri sa detalye ng network. Maaari mong i-off ang pagsusuri sa detalye ng network sa ", "network_settings_security_privacy": "Settings > Seguridad at pagkapribado", "network_currency_symbol": "Simbolo ng currency", "network_block_explorer_url": "URL ng block explorer", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "Ang isang mapaminsalang network provider ay maaaring magsinungaling tungkol sa estado ng blockchain at itala ang iyong aktibidad sa network. Magdagdag lang ng mga custom na network na pinagkakatiwalaan mo.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Impormasyon ng network", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "Impormasyon sa mga karagdagang network", "network_warning_desc": "Ang koneksyon sa network na ito ay umaasa sa mga third party. Ang koneksyon na ito ay maaaring hindi gaanong maaasahan o binibigyang-daan ang mga third-party na subaybayan ang aktibidad.", "additonial_network_information_desc": "Ang ilan sa mga network na ito ay umaasa sa mga third party. Ang mga koneksyon na ito ay maaaring hindi gaanong maaasahan o binibigyang-daan ang mga third-party na mag-track ng aktibidad.", "connect_more_networks": "Ikonekta ang mas maraming network", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "Ang network na ito ay hindi na suportado", "network_deprecated_description": "Ang network na sinusubukan mong ikonekta ay hindi na suportado ng MetaMask.", "edit_networks_title": "I-edit ang mga network", - "no_network_fee": "No network fee" + "no_network_fee": "Walang bayad sa network" }, "permissions": { "title_this_site_wants_to": "Gusto ng site na ito na:", @@ -4111,11 +4137,11 @@ "network_connected": "konektado ang network ", "see_your_accounts": "Tingnan ang mga account mo at magmungkahi ng mga transaksyon", "connected_to": "Nakakonekta sa ", - "manage_permissions": "Manage permissions", + "manage_permissions": "Pamahalaan ang mga pahintulot", "edit": "I-edit", "cancel": "Kanselahin", "got_it": "Nakuha ko", - "connection_details_title": "Connection details", + "connection_details_title": "Mga detalye ng koneksyon", "connection_details_description": "Kumonekta ka sa site na ito gamit ang MetaMask browser noong {{connectionDateTime}}", "title_add_network_permission": "Magdagdag ng pahintulot ng network", "add_this_network": "Idagdag ang network na ito", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "I-unlock gamit ang PIN ng device?" }, "authentication": { - "auth_prompt_title": "Kinakailangan ang pag-authenticate", - "auth_prompt_desc": "Mag-authenticate para magamit ang MetaMask", - "fingerprint_prompt_title": "Kinakailangan ang pag-authenticate", - "fingerprint_prompt_desc": "Gamitin ang iyong fingerprint para i-unlock ang MetaMask", - "fingerprint_prompt_cancel": "Kanselahin" + "auth_prompt_desc": "Mag-authenticate para magamit ang MetaMask" }, "accountApproval": { "title": "KAHILINGANG KUMONEKTA", "walletconnect_title": "KAHILINGANG WALLETCONNECT", "action": "Kumonekta sa site na ito?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "Para ipagpatuloy ang koneksyon, piliin ang numerong nakikita mo sa site na ito", + "action_reconnect_deeplink": "Gusto mo bang kumonekta muli sa site na ito?", "connect": "Kumonekta", "resume": "Ipagpatuloy", "cancel": "Kanselahin", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "Huwag tandaan ang koneksyon sa site na ito", "disconnect": "Idiskonekta", "permission": "Tingnan ang iyong", "address": "pampublikong address", @@ -4218,7 +4240,7 @@ "error_title": "Mayroong nang mali", "error_message": "Hindi namin ma-import ang pribadong key na iyan. Pakitiyak na inilagay mo ito nang tama.", "error_empty_message": "Kailangan mong ilagay ang iyong pribadong key.", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "o mag-scan ng QR code" }, "import_private_key_success": { "title": "Matagumpay na na-import ang account!", @@ -4229,18 +4251,18 @@ "import_wallet_title": "Mag-import ng wallet", "enter_srp_subtitle": "Ilagay rito ang iyong Lihim na Parirala sa Pagbawi", "textarea_placeholder": "Maglagay ng patlang sa pagitan ng bawat salita at tiyaking walang sinuman ang nakatingin", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "Ilagay ang Secret Recovery Phrase ng wallet mo. Maaari mong i-import ang anumang Secret Recovery Phrase ng Ethereum, Solana o Bitcoin.", + "subtitle": "I-paste ang Secret Recovery Phrase mo", "cta_text": "Magpatuloy", "paste": "I-paste", "clear": "I-clear lahat", "srp_number_of_words_option_title": "Bilang ng mga salita", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "Mayroon akong 12 salita na phrase", + "24_word_option": "Mayroon akong 24 na salita na phrase", "error_title": "Mayroong nang mali", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "Hindi namin ma-import ang Secret Recovery Phrase na iyon. Tiyaking nailagay mo ito nang tama.", + "error_empty_message": "Kailangan mong ilagay ang iyong Secret Recovery Phrase.", + "error_number_of_words_error_message": "Naglalaman ang mga Secret Recovery Phrase ng 12 o 24 na salita", "error_srp_is_case_sensitive": "Di-wastong input! Case sensitive ang Lihim na Parirala sa Pagbawi.", "error_srp_word_error_1": "Ang salita ", "error_srp_word_error_2": " ay hindi tama o mali ang baybay nito.", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " ay hindi tama o mali ang baybay ng mga ito.", "error_invalid_srp": "Di-wastong Lihim na Parirala sa Pagbawi", "error_duplicate_srp": "Na-import na ang Lihim na Parirala sa Pagbawi na ito.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "May kapareho ang account na sinusubukan mong i-import.", + "invalid_qr_code_title": "Di-wastong QR code", + "invalid_qr_code_message": "Ang QR code ay hindi naglalaman ng wastong Secret Recovery Phrase", "success_1": "Wallet", "success_2": "na-import" }, @@ -4665,7 +4687,7 @@ "button": "Protektahan ang wallet" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "Nabigo ang pag-update sa transaksyon", "text": "Nais mo bang subukan muli?", "cancel_button": "Kanselahin", "retry_button": "Subukan muli" @@ -4684,13 +4706,13 @@ "next": "Susunod", "amount_placeholder": "0.00", "link_copied": "Nakopya ang link sa clipboard", - "send_link_title": "Send link", + "send_link_title": "Magpadala ng link", "description_1": "Handa nang ipadala ang link ng iyong kahilingan!", "description_2": "Ipadala ang link na ito sa isang kaibigan, at hihilingin nitong ipadala nila", "copy_to_clipboard": "Kopyahin sa clipboard", "qr_code": "QR code", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "Magpadala ng link", + "request_qr_code": "QR Code ng kahilingan sa pagbabayad", "balance": "Balanse" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "Wala kang aktibong session", - "end_session_title": "End session", + "end_session_title": "Tapusin ang session", "end": "Tapusin", "cancel": "Kanselahin", - "session_ended_title": "Session ended", + "session_ended_title": "Natapos na ang session", "session_ended_desc": "Tinapos na ang napiling session", "session_already_exist": "Nakakonekta na ang sesyon na ito.", "close_current_session": "Isara ang kasalukuyang sesyon bago magsimula ng bago." @@ -4765,15 +4787,14 @@ "on_network": "sa {{networkName}}", "debit_card": "Debit card", "select_payment_method": "Pumili ng paraan ng pagbabayad", - "loading_quote": "Loading quote...", "pay_with": "Magbayad gamit ang", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "Bumibili sa pamamagitan ng {{providerName}}.", + "change_provider": "Magpalit ng provider.", "payment_error": "Nagkaproblema. Subukang muli.", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "Walang available na mga paraan ng pagbabayad.", "error_fetching_quotes": "Nagkaproblema. Subukang muli.", "no_quotes_available": "Wala available na mga provider.", - "providers": "Providers", + "providers": "Mga Provider", "continue": "Magpatuloy", "powered_by_provider": "Pinapagana ng {{provider}}", "purchased_currency": "Bumili ng {{currency}}", @@ -4871,6 +4892,15 @@ "log_out": "Mag-log out sa {{provider}}", "logged_out_success": "Matagumpay na na-log out", "logged_out_error": "Error sa pag-log out" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "pinakamababang limit sa pagbenta", "medium_sell_limit": "katamtamang limit sa pagbenta", "highest_sell_limit": "pinakamataas na limit sa pagbenta", - "change": "Change", + "change": "Palitan", "continue_to_amount": "Magpatuloy sa halaga", "no_payment_methods_title": "Walang paraan ng pagbabayad sa {{regionName}}", "no_cash_destinations_title": "Walang destinasyon ng pera sa {{regionName}}", @@ -5118,7 +5148,7 @@ "start_swapping": "Simulang mag-swap" }, "feature_off_title": "Pansamantalang hindi available", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "Sumasailalim sa maintenance ang MetaMask Swaps. Bumalik sa ibang pagkakataon.", "wrong_network_title": "Hindi available ang mga swap", "wrong_network_body": "Makakapag-swap ka lang ng mga token sa Ethereum Main Network.", "unallowed_asset_title": "Hindi masa-swap ang token na ito", @@ -5160,7 +5190,7 @@ "not_enough": "Hindi sapat ang {{symbol}} para makumpleto ang pag-swap na ito", "max_slippage": "Pinakamataas na slippage", "max_slippage_amount": "Pinakamataas na slippage {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "Kung magbabago ang rate sa pagitan ng oras ng pag-order mo at sa oras na nakumpirma ito, tinatawag itong “slippage”. Awtomatikong makakansela ang iyong pag-swap kung lalampas ang slippage sa iyong setting ng “pinakamataas na slippage”.", "slippage_warning": "Tiyaking alam mo ang ginagawa mo!", "allows_up_to_decimals": "Pinapayagan ng {{symbol}} ang hanggang {{decimals}} (na) decimal", "get_quotes": "Kumuha ng mga quote", @@ -5199,7 +5229,7 @@ "edit": "I-edit", "quotes_include_fee": "Kasama sa quote ang {{fee}}% na bayarin sa MetaMask", "quotes_include_gas_and_metamask_fee": "Kasama na sa quote ang bayad sa gas at {{fee}}% bayad sa MetaMask", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "I-tap para i-swap", "swipe_to_swap": "I-swipe para i-swap", "swipe_to": "I-swipe para", "swap": "Mag-swap", @@ -5259,7 +5289,7 @@ "approve": "Aprubahan ang {{sourceToken}} para sa mga pag-swap: Hanggang {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "Nakabinbin ang pag-swap ng ({{sourceToken}} sa {{destinationToken}})", "swap_confirmed": "Nakumpleto na ang pag-swap ng ({{sourceToken}} sa {{destinationToken}})", "approve_pending": "Inaaprubahan ang {{sourceToken}} para sa mga pag-swap", "approve_confirmed": "Inaprubahan ang {{sourceToken}} para sa mga pag-swap" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "Inilipat ang dropdown ng network sa mga asset mo", "description_2": "I-swap at i-bridge sa isang simpleng paraan", - "description_3": "Streamlined send experience", + "description_3": "Mas maayos na karanasan sa pagpapadala", "description_4": "Isang bagong anyo ng account" }, "more_information": "Ngayon, maaari ka nang magpokus sa iyong mga token at aktibidad, hindi sa mga network sa likod nito.", @@ -5406,21 +5436,21 @@ "aggressive_label": "Agresibo", "aggressive_text": "Mataas na posibilidad, kahit na sa pabagu-bagong merkado. Gamitin ang Agresibo upang masaklaw ang mga pagtaas ng trapiko sa network dahil sa mga bagay tulad ng mga sikat na paglaglag ng NFT.", "market_label": "Market", - "market_text": "Use market for fast processing at current market price.", + "market_text": "Gamitin ang market para sa mabilis na pagproseso sa kasalukuyang market price.", "low_label": "Mababa", "low_text": "Gamitin ang mababa para maghintay ng mas murang presyo. Ang mga pagtatantya sa oras ay hindi gaanong tumpak dahil medyo mahirap hulaan ang mga presyo.", "link": "Matuto pa tungkol sa pag-customize ng gas." }, "save": "I-save", "submit": "Isumite", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "Mababa ang pinakamataas na bayad sa priyoridad para sa kasalukuyang mga kondisyon ng network", + "max_priority_fee_high": "Ang pinakamataas na bayad sa priyoridad ay mas mataas kaysa sa kinakailangan", + "max_priority_fee_speed_up_low": "Ang pinakamataas na bayad sa priyoridad ay dapat hindi bababa sa {{speed_up_floor_value}} GWEI (10% na mas mataas kaysa sa paunang transaksyon)", + "max_priority_fee_cancel_low": "Ang pinakamataas na bayad sa priyoridad ay dapat hindi bababa sa {{cancel_value}} GWEI (50% na mas mataas kaysa sa paunang transaksyon)", + "max_fee_low": "Mababa ang pinakamataas na bayad para sa kasalukuyang mga kondisyon ng network", + "max_fee_high": "Ang pinakamataas na bayad ay mas mataas kaysa sa kinakailangan", + "max_fee_speed_up_low": "Ang pinakamataas na bayad ay dapat na hindi bababa sa {{speed_up_floor_value}} GWEI (10% na mas mataas kaysa sa paunang transaksyon)", + "max_fee_cancel_low": "Ang pinakamataas na bayad ay dapat na hindi bababa sa {{cancel_value}} GWEI (50% na mas mataas kaysa sa paunang transaksyon)", "learn_more_gas_limit": "Ang limitasyon ng gas ay ang pinakamataas na yunit ng gas na handa mong gamitin. Ang mga yunit ng gas ay multiplier sa “Pinakamataas na bayad sa priyoridad” at “Pinakamataas na bayad”. ", "learn_more_max_priority_fee": "Ang pinakamataas na bayad sa priyoridad (aka \"tip ng minero\") ay direktang napupunta sa mga minero at nagbibigay-insentibo sa kanila upang unahin ang iyong transaksyon. Madalas mong babayaran ang iyong pinakamataas na setting. ", "learn_more_max_fee": "Ang pinakamataas na bayad ay ang pinakamalaking babayaran mo (batayang bayad + bayad sa priyoridad). ", @@ -5530,10 +5560,10 @@ "enable_remember_me_description": "Kapag naka-on ang tandaan ako, sinumang may access sa iyong telepono ay makaka-access sa iyong MetaMask account." }, "turn_off_remember_me": { - "title": "Ilagay ang iyong password para i-off ang Tandaan ako", - "placeholder": "Password", - "description": "Kung in-off mo ang opsyong ito, kakailanganin mo ang iyong password upang i-unlock ang MetaMask mula ngayon.", - "action": "I-off ang Tandaan ako" + "title": "I-off ang Tandaan Ako", + "placeholder": "Kumpirmahin ang password", + "description": "Kapag nai-off, hindi na muling magagamit ang Tandaan Ako. Itinigil na ang feature na ito, kaya sa halip ay maaari mong i-unlock ang MetaMask gamit ang iyong password o biometrics.", + "action": "I-off ang Tandaan Ako" }, "dapp_connect": { "warning": "Para magamit ang feature na ito, paki-update ang app sa pinakabagong bersyon" @@ -5582,7 +5612,7 @@ "learn_more": "Matuto pa" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "I-verify ang mga detalye ng third-party", "protect_from_scams": "Para protektahan ang iyong sarili laban sa mga manloloko, maglaan ng ilang sandali upang beripikahin ang mga detalye ng third party.", "learn_to_verify": "Matutunan kung paano beripikahin ang mga detalye ng third party", "spending_cap": "limitasyon sa paggastos", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "Kinailangang ibalik", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "Nagkaproblema, pero huwag mag-alala! Subukan natin na muling ibalik ang wallet mo.", "restore_needed_action": "Ibalik ang wallet" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Nabigong isara ang gumaganang app sa iyong Ledger device.", "ethereum_app_not_installed": "Hindi naka-install ang app ng Ethereum.", "ethereum_app_not_installed_error": "Paki-install ang app ng Ethereum sa iyong Ledger device.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "Hindi bukas ang Ethereum app", + "eth_app_not_open_message": "Paki-buksan ang Ethereum app sa iyong Ledger device.", "ledger_is_locked": "Naka-lock ang Ledger", "unlock_ledger_message": "Mangyaring i-unlock ang iyong Ledger device", "cannot_get_account": "Hindi makuha ang account", @@ -5797,8 +5827,8 @@ "error_description": "Nabigo ang pag-install ng {{snap}}." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "Ang taunang bonus ay maki-claim araw-araw mula sa iyong wallet.", + "earn_a_percentage_bonus": "Kumita ng {{percentage}}% bonus", "claimable_bonus": "Naki-claim na bonus", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,11 +5866,11 @@ "withdrawal_time": "Ang oras na itinatagal para ma-withdraw ang token mo mula sa protocol at maibalik ito sa iyong wallet", "receive": "Ginagamit ang token na ito para subaybayan ang mga asset at reward mo. Huwag ilipat o i-trade ang mga ito, o hindi mo mawi-withdraw ang mga asset mo.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "Sinusukat ng iyong health factor ang panganib ng liquidation", "above_two_dot_zero": "Mas mataas sa 2.0", "safe_position": "Ligtas na posisyon", "between_one_dot_five_and_2_dot_zero": "Sa pagitan ng 1.5-2.0", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "Katamtamang panganib ng liquidation", "below_one_dot_five": "Mas mababa sa 1.5", "higher_liquidation_risk": "Mas mataas na panganib ng liquidation" }, @@ -5849,7 +5879,7 @@ "your_withdrawal_amount_may_be_limited_by": "Ang iyong halaga ng pag-withdraw ay maaaring nililimitahan ng", "pool_liquidity": "Pool liquidity", "not_enough_funds_available_in_the_lending_pool_right_now": "Hindi sapat ang mga pondong available sa pool ng pagpapautang sa ngayon.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "Umiiral na mga posisyon sa panghihiram", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "Ang pag-withdraw ay maaaring maglagay sa iyong umiiral na mga posisyon ng loan sa panganib ng liquidation." } }, @@ -5998,11 +6028,11 @@ "earn_button": "Kumita" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "Mag-stake ng TRX at kumita", + "stake_any_amount": "Mag-stake ng anumang halaga ng TRX.", "earn_trx_rewards": "Kumita ng mga TRX reward.", "earn_trx_rewards_description": "Simulang kumita sa sandaling mag-stake ka. Awtomatikong naiipon ang mga reward.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "Mag-unstake anumang oras. Kadalasang tumatagal ang pag-unstake ng 14 araw para iproseso." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "Tinatayang bayad sa gas", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "Ang mga bayad sa gas ay ibinabayad sa mga minero ng crypto na nagpoproseso ng mga transaksyon sa network ng Ethereum. Ang MetaMask ay hindi kumikita mula sa mga bayad sa gas.", "gas_fluctuation": "Ang mga bayad sa gas ay tinatantya at magbabago batay sa trapiko sa network at pagiging kumplikado ng transaksyon.", "gas_learn_more": "Matuto pa tungkol sa mga bayad sa gas" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "Nagsa-sign in gamit ang", "spender": "Gumagastos", "now": "Ngayon", - "switching_to": "Switching to", + "switching_to": "Lumilipat sa", "bridge_estimated_time": "Tinatayang tagal", "pay_with": "Magbayad gamit ang", - "receive_as": "Receive", + "receive_as": "Tumanggap", "total": "Kabuuan", - "you_receive": "You'll receive", + "you_receive": "Makakatanggap ka ng", "transaction_fee": "Bayarin sa transaksyon", - "transaction_fees": "Transaction fees", + "transaction_fees": "Mga bayad sa transaksyon", "metamask_fee": "Bayad sa MetaMask", "network_fee": "Bayad sa network", "bridge_fee": "Bayarin sa provider ng bridge" @@ -6234,7 +6264,7 @@ "transaction_fee": "Isu-swap namin ang mga token mo para sa USDE.e sa Polygon, ang network na ginamit ng mga Prediksyon. Maaaring maningil ang mga swap provider, ngunit hindi ang MetaMask." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "Magsu-swap ang MetaMask ng token na gusto mo para sa iyo. Walang hihinging bayad sa MetaMask kapag nag-swap ka sa MUSD." }, "musd_conversion": { "transaction_fee": "Kasama sa mga bayarin sa palitan ng mUSD ang mga gastos sa network at maaaring may kasamang bayarin sa provider." @@ -6256,7 +6286,7 @@ "personal_sign_tooltip": "Hinihingi ng site na ito ang iyong pirma", "transaction_tooltip": "Hinihingi ng site na ito ang iyong transaksyon", "details": "Mga detalye", - "qr_get_sign": "Get signature", + "qr_get_sign": "Kumuha ng lagda", "qr_scan_text": "I-scan gamit ang iyong wallet na hardware", "sign_with_ledger": "Pumirma gamit ang Ledger", "smart_account": "Smart account", @@ -6307,9 +6337,9 @@ "cancel": "Kanselahin", "description": "Ilagay ang halagang komportable kang gastusin sa ngalan mo.", "invalid_number_error": "Dapat na numero ang iyong limitasyon sa paggastos", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "Hindi maaaring walang laman ang iyong limitasyon sa paggastos", + "no_extra_decimals_error": "Hindi maaaring mas marami ang decimal ng iyong limitasyon sa paggastos kaysa sa token", + "no_zero_error": "Hindi maaaring 0 ang iyong limitasyon sa paggastos", "no_zero_error_decrease_allowance": "Walang epekto ang 0 na limitasyon sa paggastos sa 'decreaseAllowance' na paraan", "no_zero_error_increase_allowance": "Walang epekto ang 0 na limitasyon sa paggastos sa 'increaseAllowance' na paraan", "save": "I-save", @@ -6336,7 +6366,7 @@ "transferRequest": "Kahilingan sa paglilipat", "nested_transaction_heading": "Transaksyon {{index}}", "transaction": "Transaksyon", - "available_balance": "Available balance: ", + "available_balance": "Available na balanse: ", "edit_amount_done": "Magpatuloy", "deposit_edit_amount_done": "Magdagdag ng pondo", "deposit_edit_amount_predict_withdraw": "Mag-withdraw", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "Mga Tuntunin at Kundisyon", "select_token": "Pumili ng token", "no_tokens_found": "Walang nakitang token", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "Wala kaming nakitang anumang token na may ganitong pangalan. Subukang maghanap ng iba.", "select_network": "Pumili ng network", "all_networks": "Lahat ng network", "num_networks": "{{numNetworks}} (na) network", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "Alisin ang pagkakapili sa lahat", "see_all": "Tingnan lahat", "all": "Lahat", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}} pa", "apply": "Ilapat", "slippage": "Slippage", "slippage_info": "Kung magbabago ang presyo sa pagitan ng oras ng paglalagay mo ng order at sa oras na nakumpirma ito, tinatawag itong “slippage”. Awtomatikong makakansela ang iyong pag-swap kung lalampas ang slippage sa katanggap-tanggap na itinakda mo rito.", @@ -6392,7 +6422,7 @@ "quote_info_title": "Rate", "network_fee_info_title": "Bayad sa network", "network_fee_info_content": "Nagbabago ang bayad sa network depende kung gaano karami ang gumagamit at kung gaano kahirap ang transaksyon mo.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "Ang bayarin sa network na ito ay binabayaran ng MetaMask, kaya maaari kang makipagtransaksyon nang walang {{nativeToken}} sa iyong account.", "points": "Tinatayang mga point", "points_tooltip": "Mga Point", "points_tooltip_content_1": "Ang Mga Puntos ay kung paano ka nakakakuha ng mga Reward sa MetaMask para sa pagkumpleto ng mga transaksyon, gaya ng kapag nagsu-swap, nagbi-bridge, o nagti-trade ka ng perps.", @@ -6406,7 +6436,7 @@ "select_recipient": "Pumili ng tatanggap", "external_account": "External account", "error_banner_description": "Hindi available ang ruta ng trade na ito sa ngayon. Subukang baguhin ang halaga, network, o token at hahanapin namin ang pinakamainam na solusyon.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "Hindi available ang ruta ng trade na ito sa ngayon. Subukang baguhin ang halaga, network, o token at hahanapin namin ang pinakamainam na opsyon.\n\nPakitandaan na kung sinusubukan mong mag-trade ng Ondo Tokenised na mga Stock, maaari geo-restricted ka hal. sa pamamagitan ng US, EU, UK at BR.", "insufficient_funds": "Hindi sapat ang pondo", "insufficient_gas": "Hindi sapat ang gas", "select_amount": "Pumili ng halaga", @@ -6417,9 +6447,9 @@ "title": "Mag-bridge", "submitting_transaction": "Isinusumite", "fetching_quote": "Kumukuha ng quote", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "Kabilang ang {{feePercentage}}% na bayad sa MetaMask.", "no_mm_fee": "Walang bayad sa MM", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "Walang bayad sa MetaMask na isu-swap sa {{destTokenSymbol}}.", "hardware_wallet_not_supported": "Hindi pa sinusuportahan ang mga wallet na hardware. Gumamit ng hot wallet para magpatuloy.", "hardware_wallet_not_supported_solana": "Hindi pa sinusuportahan ang mga wallet na hardware sa Solana. Gumamit ng hot wallet para magpatuloy.", "price_impact_info_title": "Epekto ng presyo", @@ -6432,17 +6462,24 @@ "approval_needed": "Inaaprubahan ang token para sa pag-swap.", "approval_tooltip_title": "Ibigay ang eksaktong access", "approval_tooltip_content": "Pinapayagan mo ang pag-access sa tinukoy na halaga, {{amount}} {{symbol}}. Hindi ia-access ng kontrata ang anumang karagdagang pondo.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "Pinakamababang natanggap", + "minimum_received_tooltip_title": "Pinakamababang natanggap", "minimum_received_tooltip_content": "Ang pinakamababang halaga na iyong matatanggap kapag nagbago ang presyo habang pinoproseso ang iyong transaksyon, batay sa iyong slippage tolerance. Pagtantiya ito mula sa aming mga liquidity provider. Posibleng mag-iba ang pinal na halaga.", + "market_closed": { + "title": "Sarado ang market", + "description": "Ang market na sumusuporta sa token na ito ay kasalukuyang sarado. Maaaring i-transfer ang mga token na nasa chain anumang oras.", + "learn_more": "Matuto pa", + "learn_more_url": "https://status.ondo.finance/market", + "done": "Tapos na" + }, "submit": "Isumite", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "Hindi magpapatuloy ang iyong transaksyon kapag nagbago ang presyo nang higit sa porsiyento ng slippage.", "cancel": "Kanselahin", "confirm": "Kumpirmahin", "exceeding_upper_slippage_warning": "Mataas na slippage, maaari itong magresulta sa hindi kanais-nais na pag-swap", "exceeding_lower_slippage_warning": "Mababang slippage, maaari itong magresulta sa hindi kanais-nais na pag-swap", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "Ilagay ang value na mas malaki sa {{value}}%", + "exceeding_upper_slippage_error": "Hindi mo maaaring ilagay ang value na mas malaki sa {{value}}%", "custom": "Custom" }, "quote_expired_modal": { @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "Ilagay ang password", "description": "Ilagay ang password ng wallet mo para tingnan ang mga detalye ng card.", + "description_unfreeze": "Ilagay ang password ng wallet mo para ipagpatuloy ang paggastos sa iyong card.", "placeholder": "Password", "confirm": "Kumpirmahin", "cancel": "Kanselahin", @@ -7001,6 +7039,7 @@ "enable_card_error": "Hindi napagana ang card. Subukan ulit mamaya.", "view_card_details_error": "Hindi mai-load ang mga detalye ng card. Pakisubukan muli.", "biometric_verification_required": "Kinakailangan ang pag-authenticate para makita ang mga detalye ng card.", + "unfreeze_auth_required": "Kinakailangan ang pag-authenticate para ipagpatuloy ang paggastos sa iyong card.", "warnings": { "close_spending_limit": { "title": "Malapit mo nang maabot ang limitasyon mo sa paggastos", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "Na-freeze ang card mo", - "description": "Pakikontakin ang suporta para ma-unfreeze ang iyong card" + "description": "Pansamantalang naka-freeze ang card mo. Maaari mo itong i-unfreeze anumang oras." }, "blocked": { "title": "Naka-block ang iyong card", @@ -7068,7 +7107,14 @@ "travel_description": "Mag-book ng mga hotel na may hanggang 70% diskuwento", "card_tos_title": "Mga tuntunin at kundisyon", "order_metal_card": "Metal Card", - "order_metal_card_description": "I-order ngayon ang pisikal na Metal Card mo" + "order_metal_card_description": "I-order ngayon ang pisikal na Metal Card mo", + "freeze_card": "I-freeze ang card", + "unfreeze_card": "I-unfreeze ang card", + "freeze_card_description": "Ihinto ang lahat ng paggastos sa iyong card", + "unfreeze_card_description": "Ipagpatuloy ang lahat ng paggastos sa iyong card", + "freeze_error": "Nabigong i-update ang katayuan ng card. Pakisubukan muli.", + "freeze_success": "Matagumpay na nai-freeze ang card", + "unfreeze_success": "Matagumpay na nai-unfreeze ang card" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "Ipadala muli ang available sa pagkalipas ng {{seconds}} segundo" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "Idagdag sa {{walletName}}", + "adding_to_wallet": "Idinadagdag sa {{walletName}}...", + "continue_setup": "Ipagpatuloy ang Pag-setup ng {{walletName}}", + "wallet_not_available": "Hindi available ang {{walletName}}", + "already_in_wallet": "Nasa {{walletName}} na", + "success_title": "Naidagdag ang card!", + "success_message": "Ang MetaMask Card mo ay naidagdag sa {{walletName}}.", + "error_title": "Hindi makapagdagdag ng card", + "error_wallet_not_available": "Hindi available ang {{walletName}} sa device na ito. Pakitiyakin na nai-set up mo ang {{walletName}}.", + "error_wallet_not_initialized": "Hindi nasimulan ang {{walletName}}. Paki-set up ang wallet mo at subukan muli.", "error_card_already_in_wallet": "Naidagdag na ang card na ito sa {{walletName}}.", "error_card_pending": "Isini-set up ang card mo sa {{walletName}}. Bumalik pagkalipas ng ilang minuto.", "error_card_suspended": "Suspendido ang card ni sa {{walletName}}. Makipag-ugnayan sa suporta para sa tulong.", "error_card_not_eligible": "Hindi kwalipikado ang card mo para sa paglalaan ng mobile wallet.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "Nabigong i-encrypt ang card data. Pakisubukan muli.", "error_invalid_card_data": "Di-wastong data ng card. Paki-verify ang mga detalye ng card mo at subukan muli.", "error_card_not_found": "Hindi nahanap ang card. Pakisubukan muli.", "error_card_provider_not_found": "Hindi available ang provider ng card sa rehiyon mo.", "error_card_id_mismatch": "Nabigo ang pag-verify sa card. Pakisubukan muli.", "error_card_not_active": "Hindi active ang card mo. Paki-activate muna ang iyong card.", "error_network": "Nagkaroon ng error sa network. Suriin ang koneksyon mo at subukang muli.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "Nag-time out ang kahilingan. Pakisubukan muli.", + "error_server": "Nagkaroon ng error sa server. Pakisubukan muli mamaya.", + "error_unknown": "Nagkaroon ng hindi inaasahang error. Pakisubukan muli o kontakin ang suporta.", + "error_platform_not_supported": "Hindi sinusuportahan ng platform na ito ang paglalaan ng mobile wallet.", "try_again": "Subukang muli", "cancel": "Kanselahin" } @@ -7299,7 +7345,7 @@ "main_title": "Mga Reward", "referral_title": "Mga Referral", "tab_overview_title": "Overview", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "Mga snapshot", "tab_activity_title": "Aktibidad", "referral_stats_earned_from_referrals": "Nakuha mula sa mga referral", "referral_stats_referrals": "Mga Referral", @@ -7353,7 +7399,7 @@ "verifying_rewards": "Sinisigurado namin na tama lahat bago mo i-claim ang mga reward mo." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "Mga point na nakuha" }, "onboarding": { "not_supported_region_title": "Hindi sinusuportahan ang rehiyon", @@ -7431,7 +7477,7 @@ "show_less": "Magpakita ng mas kaunti", "linking_progress": "Idinadagdag ang mga account... ({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}} ang na-enroll", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "Idagdag ang lahat ng account" }, "referred_by_code": { "title": "Referral Code", @@ -7514,7 +7560,7 @@ "claim_label": "I-claim", "claimed_label": "Na-claim", "reward_claimed": "Na-claim na reward", - "time_left": "{{time}} left", + "time_left": "natitirang {{time}}", "expired": "Nag-expire" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "Nabigong ma-redeem", "redeem_failure_description": "Pakisubukan uli mamaya.", "reward_details": "Mga Detalye ng Reward", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "Piliin ang account kung saan mo gustong ipadala ang reward." }, "animation": { "could_not_load": "Hindi mai-load" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", + "starts_date": "Magsisimula sa {{date}}", + "ends_date": "Matatapos sa {{date}}", + "results_coming_soon": "Paparating na ang mga resulta", + "tokens_on_the_way": "Malapit na ang mga token", + "pill_up_next": "Susunod", + "pill_live_now": "Live ngayon", "pill_calculating": "Kinakalkula", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "pill_results_ready": "Handa na ang mga Resulta", + "pill_complete": "Kumpleto na" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "Mga snapshot", + "error_title": "Hindi mai-load ang mga snapshot", + "error_description": "Hindi namin mai-load ang mga snapshot. Pakisubukan muli.", "retry_button": "Subukang muli" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "Aktibo", + "upcoming_title": "Paparating", + "previous_title": "Nakaraan", + "empty_state": "Walang available na mga snapshot", + "error_title": "Hindi mai-load ang mga snapshot", + "error_description": "Hindi namin mai-load ang mga snapshot. Pakisubukan muli.", "retry_button": "Subukang muli", - "refreshing": "Refreshing..." + "refreshing": "Nire-refresh..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "Aprubahan ang {{approveSymbol}}", "bridge_approval_loading": "Aprubahan", "bridge_send": "I-bridge ang {{sourceSymbol}} mula sa {{sourceChain}}", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "Pagpapadala ng bridge", "bridge_receive": "Tanggapin ang {{targetSymbol}} sa {{targetChain}}", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "Pagtanggap ng bridge", "default": "Transaksyon", "musd_convert_send": "Naipadala ang {{sourceSymbol}} mula sa {{sourceChain}}", "musd_claim": "I-claim ang mUSD", @@ -7607,20 +7653,20 @@ "description": "Itinatatag ang koneksyon sa {{dappName}}..." }, "show_error": { - "title": "Connection error", + "title": "Error sa koneksyon", "description": "Nabigong maitatag ang koneksyon. Pakisubukan ulit." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "Tinanggihan ang pag-apruba", + "description": "Tinanggihan ng user ang kahilingan." }, "show_return_to_app": { "title": "Tagumpay", "description": "Bumalik sa app para magpatuloy." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "Hindi Nahanap ang Koneksyon", + "description": "Magtatag ng bagong koneksyon mula sa app para magpatuloy." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "Tuklasin", + "trending_tokens": "Mga trending na token", "price_change": "Pagbabago ng presyo", "all_networks": "Lahat ng network", - "24h": "24h", + "24h": "24 na oras", "time": "Oras", "24_hours": "24 oras", "6_hours": "6 oras", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 oras", + "5_minutes": "5 minuto", "networks": "Mga Network", "sort_by": "I-sort ayon sa", "volume": "Volume", @@ -7650,32 +7696,48 @@ "high_to_low": "Mataas Pababa", "low_to_high": "Mababa Pataas", "apply": "Ilapat", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "Maghanap ng mga token, site, URL", "cancel": "Kanselahin", "perps": "Perps", "predictions": "Mga hula", - "no_results": "No results found", + "no_results": "Walang nahanap na mga resulta", "sites": "Mga Site", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "Mga sikat na site", + "search_sites": "Maghanap ng mga site", + "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.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "Hindi available ang mga trending na token", + "description": "Hindi namin makuha ang page na ito sa ngayon", "try_again": "Subukang muli" }, "empty_search_result_state": { "title": "Walang nakitang token", - "description": "We were not able to find this token" + "description": "Hindi namin nahanap ang token na ito" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "Handa na ang update", + "description_ios": "Nagsagawa kami ng ilang mahahalagang pagsasaayos. I-reload para sa pinakabagong bersyon ng MetaMask.", + "description_android": "Nagsagawa kami ng ilang mahahalagang pagsasaayos. Isara at buksang muli ang MetaMask para ilapat ang update.", "primary_action_reload": "I-reload", "primary_action_acknowledge": "Nakuha ko" + }, + "homepage": { + "sections": { + "tokens": "Mga Token", + "perpetuals": "Perpetuals", + "predictions": "Mga hula", + "defi": "DeFi", + "nfts": "Mga NFT", + "import_nfts": "Mag-import ng mga NFT", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/tr.json b/locales/languages/tr.json index cac01fead81..2ff5f88da15 100644 --- a/locales/languages/tr.json +++ b/locales/languages/tr.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "bu cihazda", "reset_wallet_desc_2": "MetaMask'ten kalıcı olarak silinecek. Bu işlem geri alınamaz.", "reset_wallet_desc_login": "Cüzdanınızı geri yüklemek için Gizli Kurtarma İfadenizi veya Google ya da Apple hesap şifrenizi kullanabilirsiniz. MetaMask bu bilgilere sahip değildir.", - "reset_wallet_desc_srp": "Cüzdanınızı geri yüklemek için Gizli Kurtarma İfadenizin hazır bulunduğundan emin olun. MetaMask bu bilgiye sahip değildir." + "reset_wallet_desc_srp": "Cüzdanınızı geri yüklemek için Gizli Kurtarma İfadenizin hazır bulunduğundan emin olun. MetaMask bu bilgiye sahip değildir.", + "biometric_authentication_cancelled": "Biyometrik doğrulama iptal edildi", + "biometric_authentication_cancelled_title": "Biyometrik Kurulum Başarısız Oldu", + "biometric_authentication_cancelled_description": "Lütfen ayarlar bölümünden biyometrik doğrulama kurulumunu tekrar yapın.", + "biometric_authentication_cancelled_button": "Onayla" }, "connect_hardware": { "title_select_hardware": "Bir donanım cüzdanı bağlayın", @@ -1040,7 +1044,7 @@ "title": "Yatırılacak miktar", "get_usdc_hyperliquid": "USDC • Hyperliquid al", "insufficient_funds": "Yetersiz bakiye", - "no_funds_available": "Kullanılabilir para yok. Lütfen önce para yatırın.", + "no_funds_available": "Yeterli bakiye yok. Para yatırın veya farklı bir ödeme yöntemi seçin", "enter_amount": "Tutar gir", "fetching_quote": "Teklif alınıyor", "submitting": "İşlem gönderiliyor", @@ -1970,8 +1974,8 @@ "trade_again": "Tekrar işlem yap", "activity": { "deposit_title": "Para Yatır", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "{{amount}} {{symbol}} yatırıldı", + "withdrew_amount": "{{amount}} {{symbol}} çekildi", "status_completed": "Tamamlandı", "status_failed": "Başarısız oldu", "status_pending": "Bekliyor" @@ -2051,6 +2055,16 @@ "referral_code_text": "Referans kodumu kullanarak ekstra ödüller kazan." } }, + "market_insights": { + "title": "Piyasa içgörüleri", + "updated_ago": "Güncelleme: {{time}}", + "disclaimer": "Yapay zeka içgörüleridir. Finansal tavsiye değildir.", + "whats_driving_price": "Fiyatı ne etkiliyor?", + "what_people_saying": "İnsanlar ne diyor", + "trade_button": "İşlem Yap", + "sources_count": "+{{count}} kaynak", + "sources_title": "Kaynakları" + }, "predict": { "title": "MetaMask Tahminler", "prediction_markets": "Tahmin piyasaları", @@ -2384,8 +2398,8 @@ "no_available_tokens": "Tokeninizi görmüyor musunuz?", "add_tokens": "Tokenleri içe aktar", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "Bu hesapta {{tokenCount}} yeni {{tokensLabel}} bulundu", "token_toast": { "tokens_imported_title": "İçe aktarılan tokenlar", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "Token ondalıkları boş olamaz.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "Bu isimde token bulamadık.", + "tokens_empty_description": "Search for any token and import it", "select_token": "Token seç", "address_must_be_smart_contract": "Kişisel adres algılandı. Token sözleşme adresini girin.", "billion_abbreviation": "MR", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "Tüm hesapların bağlantısını kes", "deceptive_site_ahead": "Aldatıcı siteye gidiliyor", "deceptive_site_desc": "Ziyaret etmeye çalıştığınız sayfa güvenli değil. Saldırganlar tehlikeli bir şey yapmanız konusunda sizi aldatabilir.", + "malicious_site_detected": "Kötü amaçlı site algılandı", + "malicious_site_warning": "Bu siteye bağlanırsanız varlıklarınızın tamamını kaybedebilirsiniz.", + "connect_anyway": "Yine de Bağlan", "learn_more": "Daha fazla bilgi edin", "advisory_by": "Ethereum Kimlik Avı Algılayıcı ve PhishFort tarafından sunulan uyarı", "potential_threat": "Potansiyel tehditler şunları içerir", @@ -2846,7 +2864,11 @@ "permissions": "İzinler", "card_title": "MetaMask Kartı", "settings": "Ayarlar", - "log_out": "Oturumu kapat" + "networks": "Ağlar", + "log_out": "Oturumu kapat", + "notifications": "Cüzdanınızın kullanımı", + "buy": "Al", + "scan": "Tara" }, "app_settings": { "enabling_notifications": "Bildirimler etkinleştiriliyor...", @@ -2870,6 +2892,8 @@ "state_logs": "Durum günlükleri", "add_network_title": "Ağ ekle", "auto_lock": "Otomatik kilitle", + "enable_device_authentication": "Cihaz Doğrulamayı Etkinleştir", + "enable_device_authentication_desc": "MetaMask'in kilidini açmak için cihazınızın biyometrik verilerini veya parolasını kullanın.", "auto_lock_desc": "Uygulama otomatik olarak kilitlenmeden süre miktarını seçin.", "state_logs_desc": "Bu, MetaMask'ın, karşılaşabileceğiniz her sorunu çözmesine yardımcı olur. Lütfen hamburger simgesinden > Geri Bildirim Gönder kısmından MetaMask destek bölümüne gönderin veya varsa mevcut sorgunuza yanıt verin.", "autolock_immediately": "Hemen", @@ -2975,6 +2999,11 @@ "add_rpc_url": "RPC URL adresi ekle", "add_block_explorer_url": "Blok Gezgini URL adresi ekle", "networks_desc": "Kişisel RPC ağı ekle ve düzenle", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "Ağ ara", + "networks_no_results": "Ağ bulunamadı", "network_name_label": "Ağ adı", "network_name_placeholder": "Ağ adı (isteğe bağlı)", "network_rpc_url_label": "RPC URL adresi", @@ -2991,7 +3020,16 @@ "network_other_networks": "Diğer ağlar", "network_rpc_networks": "RPC ağları", "network_add_network": "Ağ ekle", + "add_chain_title": "Ağ ekle", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "Tekrar Dene", + "add_chain_added": "Added", + "add_chain_or": "veya", + "add_chain_custom_link": "Özel bir ağ ekle", "network_add_custom_network": "Özel bir ağ ekle", + "network_add_test_network": "Add a test network", "network_add": "Ekle", "network_save": "Kaydet", "remove_network_title": "Bu ağı kaldırmak istiyor musunuz?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "Tamam", "title": "Hesap bağlantısı kurulamadı", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "MetaMask'e tekrar bağlanmak için lütfen sitede QR kodunu tarayın" }, "app_information": { "title": "Bilgi", @@ -3379,6 +3417,7 @@ "sell_description": "Nakit karşılığı kripto sat" }, "asset_overview": { + "market_closed": "Piyasa kapalı", "send_button": "Gönder", "buy_button": "Satın Al", "cash_buy_button": "Nakit Al", @@ -3399,19 +3438,6 @@ "bridge": "Köprü", "earn": "Kazan", "convert_to_musd": "mUSD'ye dönüştür", - "merkl_rewards": { - "annual_bonus": "%{{apy}} bonus", - "claimable_bonus": "Alınabilir bonus", - "claimable_bonus_tooltip_description": "mUSD bonusları Linea'da alınır.", - "terms_apply": "Şartlar uygulanır.", - "ok": "Tamam", - "claim": "Al", - "processing_claim": "Talep işleme alınıyor...", - "claim_on_linea_title": "Bonusları Linea üzerinde alın", - "claim_on_linea_description": "Bonusunuz, Ethereum mUSD bakiyenizden ayrı olacak şekilde, Linea üzerinde düzenlenecektir.", - "continue": "Devam et", - "unexpected_error": "Beklenmedik hata. Lütfen tekrar deneyin." - }, "tron": { "daily_resource_new_energy": "Yeni günlük enerji", "sufficient_to_cover": "Karşılanamıyor", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "Token adresi hafıza panosuna kopyalandı" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "QR kodu geçersiz", "invalid_qr_code_message": "Taramaya çalıştığınız QR kodu geçerli değil.", "allow_camera_dialog_title": "Kamera erişimine izin ver", "allow_camera_dialog_message": "QR kodlarını taramak için izninize ihtiyacımız var", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "Görünüşe göre uzantı ile senkronize etmeye çalışıyorsunuz. Bunu yapabilmek için mevcut cüzdanınızı silmeniz gerekecek. \n\nUygulamayı sildiğinizde veya uygulamanın yeni bir sürümünü tekrar yüklediğinizde \"MetaMask Uzantısı ile Senkronize Et\" seçeneğini seçin. Önemli! Cüzdanınızı silmeden önce Gizli Kurtarma İfadenizi yedeklediğinizden emin olun.", "not_allowed_error_title": "Kamera erişimini aç", "not_allowed_error_desc": "Bir QR kodunu taramak için cihazınızın ayarlar menüsünden MetaMask'e kamera erişimi vermeniz gerekecektir.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "QR kodu tanınmıyor", "unrecognized_address_qr_code_desc": "Üzgünüz, bu QR kodu bir hesap adresi ya da bir iletişim adresiyle ilişkilendirilmemiş.", "url_redirection_alert_title": "Harici bir bağlantıyı ziyaret etmek üzeresiniz", "url_redirection_alert_desc": "Bağlantılar insanları dolandırmak ya da kimlik avı için kullanılabilir bu nedenle sadece güendiğiniz web sitelerini ziyaret ettiğinizden emin olun.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "Bu koleksiyona sahip değilsiniz", "known_asset_contract": "Bilinen varlık sözleşme adresi", "max": "Maks.", - "recipient_address": "Recipient address", + "recipient_address": "Alıcı adresi", "required": "Gerekli", "to": "Kime", "total": "Toplam", @@ -3641,7 +3667,7 @@ "nevermind": "Vazgeç", "edit_network_fee": "Gaz ücretini düzenle", "edit_priority": "Önceliği düzenle", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "Gaz iptal ücreti", "gas_speedup_fee": "Gaz hızlandırma ücreti", "use_max": "Maksimumu kullan", "set_gas": "Ayarla", @@ -3650,7 +3676,7 @@ "transaction_fee": "Gaz ücreti", "transaction_fee_less": "Ücret yok", "total_amount": "Toplam tutar", - "view_data": "View data", + "view_data": "Verileri görüntüle", "adjust_transaction_fee": "İşlem ücretini ayarla", "could_not_resolve_ens": "ENS çözümlenemedi", "asset": "Varlık", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "Merkeziyetsiz webde gezinmek için yeni bir sekme ekleyin", "got_it": "Anladım", "max_tabs_title": "Maksimum sekmeye ulaşıldı", - "max_tabs_desc": "Şu anda aynı anda yalnızca 5 açık sekmeyi destekliyoruz. Yenilerini eklemeden önce lütfen mevcut sekmeleri kapatın.", + "max_tabs_desc": "Şu anda aynı anda yalnızca 20 açık sekmeyi destekliyoruz. Yenilerini eklemeden önce lütfen mevcut sekmeleri kapatın.", "failed_to_resolve_ens_name": "Bu ENS adını çözümleyemedik", "remove_bookmark_title": "Favoriyi kaldır", "remove_bookmark_msg": "Bu siteyi gerçekten favorilerinizden kaldırmak istiyor musunuz?", @@ -3828,7 +3854,7 @@ "cancel_button": "İptal" }, "approval": { - "title": "Confirm transaction" + "title": "İşlemi onayla" }, "approve": { "title": "Onayla", @@ -3839,39 +3865,39 @@ "unavailable": "Mevcut değil", "tx_review_confirm": "Onayla", "tx_review_transfer": "Transfer Et", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "Sözleşme dağıtımı", + "tx_review_transfer_from": "Şuradan transfer et", + "tx_review_unknown": "Bilinmeyen yöntem", "tx_review_approve": "Onayla", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "Harcama iznini artır", + "tx_review_set_approval_for_all": "Tümüne onay ver", + "tx_review_staking_claim": "Stake yapma talebi", "tx_review_staking_deposit": "Stake para yatırma işlemi", "tx_review_staking_unstake": "Unstake Et", "tx_review_lending_deposit": "Borç verme para yatırma işlemi", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "Borç verme para çekme işlemi", "tx_review_perps_deposit": "Fonlanan sürekli vadeli işlem sözleşmeleri", "tx_review_predict_deposit": "Fonlanmış tahminler", "tx_review_predict_claim": "Alınan kazançlar", "tx_review_predict_withdraw": "Tahmin bakiyesini çek", "tx_review_musd_conversion": "mUSD dönüştürme", "claim": "Al", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "ETH gönder", + "self_sent_ether": "Kendinize ETH gönderdiniz", + "received_ether": "ETH alındı", "sent_dai": "DAI Gönderdi", "self_sent_dai": "Sana DAI gönderdi", "received_dai": "DAI aldı", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "Token gönderildi", + "received_tokens": "Token alındı", "ether": "ETH", "sent_unit": "{{unit}} Gönderdi", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "Kendinize {{unit}} gönderdiniz", "received_unit": "{{unit}} Aldı", "sent_collectible": "Koleksiyon gönderildi", "received_collectible": "Koleksiyon alındı", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "ETH Gönder", + "send_unit": "{{unit}} gönder", "send_collectible": "Koleksiyon gönder", "receive_collectible": "Koleksiyon al", "sent": "Gönderildi", @@ -3881,8 +3907,8 @@ "send": "Gönder", "redeposit": "Tekrar para yatır", "interaction": "Etkileşim", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", + "contract_deploy": "Sözleşme dağıtımı", + "to_contract": "Yeni sözleşme", "mint": "Mint", "tx_details_free": "Ücretsiz", "tx_details_not_available": "Mevcut değil", @@ -3890,8 +3916,8 @@ "swaps_transaction": "Swap işlemi", "bridge_transaction": "Köprü", "approve": "Onayla", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "Harcama iznini artır", + "set_approval_for_all": "Tümüne onay ver", "hash": "Hash", "from": "Kimden", "to": "Kime", @@ -3899,15 +3925,15 @@ "amount": "Tutar", "fee": { "transaction_fee_in_ether": "İşlem ücreti", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "İşlem ücreti (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "Kullanılan gaz (birim)", + "gas_limit": "Gaz limiti (birim)", + "gas_price": "Gaz fiyatı (GWEI)", + "base_fee": "Baz ücreti (GWEI)", + "priority_fee": "Öncelik ücreti (GWEI)", "multichain_priority_fee": "Öncelik ücreti", - "max_fee": "Max fee per gas", + "max_fee": "Gaz başına maksimum ücret", "total": "Toplam", "view_on": "Şurada görüntüle:", "view_on_etherscan": "Etherscan üzerinde görüntüle", @@ -3923,13 +3949,13 @@ "nonce": "Nonce", "from_device_label": "bu cihazdan", "import_wallet_row": "Hesap bu cihaza eklendi", - "import_wallet_label": "Account added", + "import_wallet_label": "Hesap eklendi", "import_wallet_tip": "Gelecekte bu cihazdan yapılan tüm işlemlerde zaman damgasının yanında \"bu cihazdan\" etiketi bulunacaktır. Hesabın eklenmesinden önceki tarihlerde yapılan işlemler için bu geçmiş, hangi giden işlemlerin bu cihazdan oluşturulduğunu göstermeyecektir.", "sign_title_scan": "Tara ", "sign_title_device": "donanım cüzdanınızla", "sign_description_1": "Donanım cüzdanınızla imzaladıktan sonra", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "İmza al düğmesine tıklayın", + "sign_get_signature": "İmza al", "transaction_id": "İşlem Kimliği", "network": "Ağ", "request_from": "Talebi gönderen", @@ -4032,7 +4058,7 @@ "title": "Ağlar", "other_networks": "Diğer ağlar", "close": "Kapat", - "status_ok": "All systems operational", + "status_ok": "Tüm sistemler faal", "status_not_ok": "Ağda bazı sorunlar yaşanıyor", "want_to_add_network": "Bu ağı eklemek istiyor musunuz?", "add_custom_network": "Özel ağ ekle", @@ -4051,7 +4077,7 @@ "review": "İncele", "view_details": "Ayrıntıları görüntüle", "network_details": "Ağ bilgileri", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "Onayla seçeneğini seçildiğinde ağ bilgileri kontrolü açılır. Ağ bilgileri kontrolünü şuradan kapatabilirsiniz: ", "network_settings_security_privacy": "Ayarlar > Güvenlik ve gizlilik", "network_currency_symbol": "Para birimi sembolü", "network_block_explorer_url": "Blok Gezgini URL adresi", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "Kötü amaçlı bir ağ sağlayıcı blokzincirinin durumu hakkında yalan söyleyebilir ve ağ aktivitenizi kaydedebilir. Sadece güvendiğiniz özel ağları ekleyin.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Ağ bilgileri", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "Diğer ağ bilgileri", "network_warning_desc": "Bu ağ bağlantısı üçüncü taraflara dayalıdır. Bu bağlantı daha az güvenli olabilir veya üçüncü tarafların aktiviteleri takip etmesine olanak sağlayabilir.", "additonial_network_information_desc": "Bu ağların bazıları üçüncü taraflara dayalıdır. Bağlantılar daha az güvenilir olabilir veya üçüncü tarafların aktiviteleri takip etmesine olanak sağlayabilir.", "connect_more_networks": "Daha fazla ağ bağlan", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "Bu ağ artık kullanılmıyor", "network_deprecated_description": "Bağlanmaya çalıştığınız ağ artık MetaMask'te desteklenmiyor.", "edit_networks_title": "Ağları düzenle", - "no_network_fee": "No network fee" + "no_network_fee": "Ağ ücreti yok" }, "permissions": { "title_this_site_wants_to": "Bu site şunları yapmak istiyor:", @@ -4111,11 +4137,11 @@ "network_connected": "ağ bağlı ", "see_your_accounts": "Hesaplarınızı görün ve işlem önerin", "connected_to": "Şuraya bağlanıldı: ", - "manage_permissions": "Manage permissions", + "manage_permissions": "İzinleri yönet", "edit": "Düzenle", "cancel": "İptal", "got_it": "Anladım", - "connection_details_title": "Connection details", + "connection_details_title": "Bağlantı bilgileri", "connection_details_description": "Bu siteye {{connectionDateTime}} itibariyle MetaMask tarayıcısını kullanarak bağlandınız", "title_add_network_permission": "Ağ izni ekle", "add_this_network": "Bu ağı ekle", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "Cihazın PIN kodu ile kilidi aç?" }, "authentication": { - "auth_prompt_title": "Doğrulama gerekli", - "auth_prompt_desc": "MetaMask'ı kullanabilmek için lütfen doğrulama yapın", - "fingerprint_prompt_title": "Doğrulama gerekli", - "fingerprint_prompt_desc": "MetaMask'ın kilidini açmak için parmak izinizi kullanın", - "fingerprint_prompt_cancel": "İptal" + "auth_prompt_desc": "MetaMask'ı kullanabilmek için lütfen doğrulama yapın" }, "accountApproval": { "title": "BAĞLANTI TALEBİ", "walletconnect_title": "WALLETCONNECT TALEBİ", "action": "Bu siteye bağlan?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "Bağlantıyı sürdürmek için sitede gördüğünüz sayıyı seçin", + "action_reconnect_deeplink": "Bu siteye tekrar bağlanmak istiyor musunuz?", "connect": "Bağlan", "resume": "Sürdür", "cancel": "İptal", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "Bu site bağlantısını unut", "disconnect": "Bağlantıyı kes", "permission": "Şunu görüntüle:", "address": "genel adres", @@ -4218,7 +4240,7 @@ "error_title": "Bir şeyler ters gitti", "error_message": "Bu özel anahtarı içe aktaramadık. Lütfen doğru girdiğinizden emin olun.", "error_empty_message": "Özel anahtarınızı girmeniz gerekiyor.", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "veya bir QR kodunu tara" }, "import_private_key_success": { "title": "Hesap başarılı bir şekilde içe aktarıldı!", @@ -4229,18 +4251,18 @@ "import_wallet_title": "Bir cüzdanı içe aktar", "enter_srp_subtitle": "Gizli Kurtarma İfadenizi girin", "textarea_placeholder": "Her bir sözcüğün arasına bir boşluk ekleyin ve hiç kimsenin izlemediğinden emin olun", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "Cüzdanınızın Gizli Kurtarma İfadesini girin. Herhangi bir Ethereum, Solana veya Bitcoin Gizli Kurtarma İfadesini içe aktarabilirsiniz.", + "subtitle": "Gizli Kurtarma İfadenizi yapıştırın", "cta_text": "Devam et", "paste": "Yapıştır", "clear": "Tümünü temizle", "srp_number_of_words_option_title": "Sözcük sayısı", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "12 sözcükten oluşan bir ifadem var", + "24_word_option": "24 sözcükten oluşan bir ifadem var", "error_title": "Bir şeyler ters gitti", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "Bu Gizli Kurtarma İfadesini içe aktaramadık. Lütfen ifadeyi doğru girdiğinizden emin olun.", + "error_empty_message": "Gizli Kurtarma İfadenizi girmeniz gerekiyor.", + "error_number_of_words_error_message": "Gizli Kurtarma İfadeleri 12 veya 24 sözcük içerir", "error_srp_is_case_sensitive": "Giriş geçersiz! Gizli Kurtarma İfadesi büyük/küçük harfe duyarlıdır.", "error_srp_word_error_1": "Sözcük ", "error_srp_word_error_2": " yanlış veya yanlış yazılmış.", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " yanlış veya yanlış yazılmış.", "error_invalid_srp": "Gizli Kurtarma İfadesi geçersiz", "error_duplicate_srp": "Bu Gizli Kurtarma İfadesi zaten içe aktarıldı.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "İçe aktarmaya çalıştığınız hesap zaten mevcut.", + "invalid_qr_code_title": "QR kodu geçersiz", + "invalid_qr_code_message": "QR kodu geçerli bir Gizli Kurtarma İfadesi içermiyor", "success_1": "Cüzdan", "success_2": "i̇çe aktarıldı" }, @@ -4665,7 +4687,7 @@ "button": "Cüzdanı koru" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "İşlem güncellemesi başarısız oldu", "text": "Tekrar denemek ister misiniz?", "cancel_button": "İptal", "retry_button": "Tekrar Dene" @@ -4684,13 +4706,13 @@ "next": "Sonraki", "amount_placeholder": "0.00", "link_copied": "Bağlantı panoya kopyalandı", - "send_link_title": "Send link", + "send_link_title": "Bağlantıyı gönder", "description_1": "Talep bağlantınız gönderilmeye hazır!", "description_2": "Bu bağlantıyı bir arkadaşınıza gönderin ve ondan da göndermesi istenecek", "copy_to_clipboard": "Panoya kopyala", "qr_code": "QR kodu", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "Bağlantıyı gönder", + "request_qr_code": "Ödeme talebi QR kodu", "balance": "Bakiye" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "Aktif oturumunuz yok", - "end_session_title": "End session", + "end_session_title": "Oturumu sonlandır", "end": "Sonlandır", "cancel": "İptal", - "session_ended_title": "Session ended", + "session_ended_title": "Oturum sonlandırıldı", "session_ended_desc": "Seçilen oturuma son verildi", "session_already_exist": "Bu oturum zaten bağlı.", "close_current_session": "Yeni bir oturum başlatmadan önce geçerli oturumu kapat." @@ -4765,15 +4787,14 @@ "on_network": "{{networkName}} üzerinde", "debit_card": "Banka kartı", "select_payment_method": "Ödeme yöntemi seç", - "loading_quote": "Loading quote...", "pay_with": "Ödeme aracı", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "{{providerName}} aracılığıyla satın alınıyor.", + "change_provider": "Sağlayıcı değiştirin.", "payment_error": "Bir şeyler ters gitti. Lütfen tekrar deneyin.", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "Kullanılabilir ödeme yöntemi yok.", "error_fetching_quotes": "Bir şeyler ters gitti. Lütfen tekrar deneyin.", "no_quotes_available": "Sağlayıcı mevcut değil.", - "providers": "Providers", + "providers": "Sağlayıcılar", "continue": "Devam et", "powered_by_provider": "Destekleyen {{provider}}", "purchased_currency": "{{currency}} Satın Alındı", @@ -4871,6 +4892,15 @@ "log_out": "{{provider}} oturumunu kapat", "logged_out_success": "Oturum başarılı bir şekilde kapatıldı", "logged_out_error": "Oturum kapatılırken hata" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "en düşük sat limiti", "medium_sell_limit": "orta sat limiti", "highest_sell_limit": "en yüksek sat limiti", - "change": "Change", + "change": "Değiştir", "continue_to_amount": "Tutara devam et", "no_payment_methods_title": "{{regionName}} bölgesinde ödeme yöntemi yok", "no_cash_destinations_title": "{{regionName}} bölgesinde nakit varış yeri yok", @@ -5118,7 +5148,7 @@ "start_swapping": "Swap işlemine başlayın" }, "feature_off_title": "Geçici olarak kullanılamıyor", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "MetaMask Swaps sistemi şu anda bakımda. Lütfen daha sonra tekrar kontrol edin.", "wrong_network_title": "Swaplar kullanılamıyor", "wrong_network_body": "Sadece Ethereum Ana Ağında token swap işlemi gerçekleştirebilirsiniz.", "unallowed_asset_title": "Bu token ile swap gerçekleştirilemiyor", @@ -5160,7 +5190,7 @@ "not_enough": "Bu swap işlemini gerçekleştirmek için yeterli {{symbol}} yok", "max_slippage": "Maks. kayma", "max_slippage_amount": "Maks. kayma {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "Emrinizin verildiği zaman ile emrinizin onaylandığı zaman arasında oran değişirse buna “kayma” denir. Kayma “maksimum kayma” ayarınızı aştığı takdirde takas işleminiz otomatik olarak iptal edilir.", "slippage_warning": "Ne yaptığınızı bildiğinizden emin olun!", "allows_up_to_decimals": "{{symbol}}, {{decimals}} ondalık sayıya kadar izin verir", "get_quotes": "Teklif al", @@ -5199,7 +5229,7 @@ "edit": "Düzenle", "quotes_include_fee": "%{{fee}} MetaMask ücreti tekliflere dahildir", "quotes_include_gas_and_metamask_fee": "Gaz ve %{{fee}} MetaMask ücreti teklife dahildir", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "Takas işlemi gerçekleştirmek için dokunun", "swipe_to_swap": "Swap gerçekleştirmek için kaydır", "swipe_to": "Swap gerçekleştirmek için", "swap": "Swap Yap", @@ -5259,7 +5289,7 @@ "approve": "Swap işlemleri için {{sourceToken}} onayla: En fazla {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "Bekleyen takas işlemi ({{sourceToken}} - {{destinationToken}})", "swap_confirmed": "Swap işlemi tamamlandı ({{sourceToken}} ile {{destinationToken}})", "approve_pending": "Swap işlemleri için {{sourceToken}} onaylanıyor", "approve_confirmed": "Swap işlemleri için {{sourceToken}} onaylandı" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "Ağ açılır menüsü varlıklar sekmenize taşındı", "description_2": "Takas ve Köprü tek bir basit adımda", - "description_3": "Streamlined send experience", + "description_3": "Gönder deneyimi kolaylaştırıldı", "description_4": "Yepyeni bir hesap görünümü" }, "more_information": "Artık arkasındaki ağlara değil, token'larınıza ve faaliyetlerinize odaklanabilirsiniz.", @@ -5406,21 +5436,21 @@ "aggressive_label": "Agresif", "aggressive_text": "Volatil piyasalarda bile yüksek olasılık. Popüler NFT düşüşleri gibi şeyler nedeniyle ağ trafindeki dalgalanmaları kapsamak için Agresif özelliğini kullanın.", "market_label": "Piyasa", - "market_text": "Use market for fast processing at current market price.", + "market_text": "Güncel piyasa fiyatından hızlı işlem yapmak için piyasa özelliğini kullanın.", "low_label": "Düşük", "low_text": "Daha düşük bir fiyat beklemek için düşük seçeneğini kullanın. Fiyatlar biraz öngörülemez olduğu için süre tahminleri pek doğru olmayabilir.", "link": "Gaz ücretini kişiselleştirme hakkında daha fazla bilgi al." }, "save": "Kaydet", "submit": "Gönder", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "Maksimum öncelik ücreti geçerli ağ koşulları için düşük", + "max_priority_fee_high": "Maksimum öncelik ücreti gerekenden yüksek", + "max_priority_fee_speed_up_low": "Maksimum öncelik ücreti en az {{speed_up_floor_value}} GWEI olmalıdır (ilk işlemin %10 üzerinde)", + "max_priority_fee_cancel_low": "Maksimum öncelik ücreti en az {{cancel_value}} GWEI olmalıdır (ilk işlemin %50 üzerinde)", + "max_fee_low": "Maksimum ücret geçerli ağ koşulları için düşük", + "max_fee_high": "Maksimum ücret gerekenden yüksek", + "max_fee_speed_up_low": "Maksimum ücret en az {{speed_up_floor_value}} GWEI olmalıdır (ilk işlemin %10 üzerinde)", + "max_fee_cancel_low": "Maksimum ücret en az {{cancel_value}} GWEI olmalıdır (ilk işlemin %50 üzerinde)", "learn_more_gas_limit": "Gaz limiti kullanmak istediğiniz maksimum gaz birimidir. Gaz birimleri “Maks. öncelik ücreti” ile “Maks. ücret” çarpanıdır. ", "learn_more_max_priority_fee": "Maks. öncelik ücreti (diğer adıyla “madenci bahşişi”) doğrudan madencilere gider ve işleminizi önceliklendirmeleri için onlara teşvik sunar. Genellikle maks. ayarınızı ödersiniz.", "learn_more_max_fee": "Maks. ücret ödeyeceğiniz en fazla ücrettir (baz ücret + öncelik ücreti).", @@ -5530,9 +5560,9 @@ "enable_remember_me_description": "Beni Hatırla özelliği açıldığında telefonunuza erişimi olan herkes MetaMask hesabınıza erişim sağlayabilir." }, "turn_off_remember_me": { - "title": "Beni Hatırla özelliğini kapatmak için şifrenizi girin", - "placeholder": "Şifre", - "description": "Bu seçeneği kapatırsanız şu andan itibaren MetaMask'in kilidini açmak için şifrenizi girmeniz gerekecektir.", + "title": "Beni Hatırla özelliğini kapat", + "placeholder": "Şifreyi onayla", + "description": "Beni Hatırla özelliği kapatıldıktan sonra tekrar kullanılamaz. Bu özellik sonlandırılmış, bu yüzden bunun yerine MetaMask'in kilidini şifrenizle veya biyometrik veri ile açabilirsiniz.", "action": "Beni Hatırla özelliğini kapat" }, "dapp_connect": { @@ -5582,7 +5612,7 @@ "learn_more": "Daha fazla bilgi edin" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "Üçüncü taraf bilgilerini doğrula", "protect_from_scams": "Kendinizi dolandırıcılardan korumak için bir dakikanızı ayırarak üçüncü taraf bilgilerini doğrulayın.", "learn_to_verify": "Üçüncü taraf bilgilerinin nasıl doğrulanacağını öğrenin", "spending_cap": "harcama üst limiti", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "Geri yükleme gerekiyor", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "Bir şeyler ters gitti ama endişelenmeyin! Cüzdanınızı geri yüklemeye çalışalım.", "restore_needed_action": "Cüzdanı geri yükle" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Ledger cihazınızda çalışan uygulama kapatılamadı.", "ethereum_app_not_installed": "Ethereum uygulaması yüklü değil.", "ethereum_app_not_installed_error": "Lütfen Ledger cihazınıza Ethereum uygulamasını yükleyin.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "Ethereum uygulaması açık değil", + "eth_app_not_open_message": "Lütfen Ledger cihazınızda Ethereum uygulamasını açın.", "ledger_is_locked": "Ledger kilitli", "unlock_ledger_message": "Lütfen Ledger cihazınızın kilidini açın", "cannot_get_account": "Hesap alınamıyor", @@ -5797,8 +5827,8 @@ "error_description": "{{snap}} yüklemesi başarısız oldu." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "Cüzdanınızdan her gün alınabilen yıllık bonus.", + "earn_a_percentage_bonus": "%{{percentage}} bonus kazanın", "claimable_bonus": "Alınabilir bonus", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "Token'inizi protokolden çekip cüzdanınıza geri almanızın süresi", "receive": "Bu token, varlıklarınızı ve ödüllerinizi takip etmek için kullanılır. Bunu transfer etmeyin veya bununla işlem yapmayın, aksi takdirde varlıklarınızı çekemezsiniz.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "Sağlık faktörünüz likidasyon riskini ölçer", "above_two_dot_zero": "2,0 üzeri", "safe_position": "Güvenli pozisyon", "between_one_dot_five_and_2_dot_zero": "1,5-2,0 arası", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "Orta likidasyon riski", "below_one_dot_five": "1,5 altı", "higher_liquidation_risk": "Yüksek likidasyon riski" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "Neden tüm bakiyemi çekemiyorum?", "your_withdrawal_amount_may_be_limited_by": "Para çekme tutarınız şununla sınırlı olabilir", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "Havuz likiditesi", "not_enough_funds_available_in_the_lending_pool_right_now": "Şu anda borç verme havuzunda yeterli para yok.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "Mevcut borç alma pozisyonları", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "Para çekmek, mevcut kredi pozisyonlarınızı likidasyon riskine atabilir." } }, @@ -5998,11 +6028,11 @@ "earn_button": "Kazan" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "TRX stake et ve kazan", + "stake_any_amount": "Dilediğiniz tutarda TRX stake edin.", "earn_trx_rewards": "TRX ödülleri kazan.", "earn_trx_rewards_description": "Stake eder etmez kazanmaya başlayın. Ödüller otomatik olarak birikir.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "Dilediğiniz zaman unstake edin. Unstake işleminin tamamlanması 14 gün sürer." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "Tahmini gaz ücreti", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "Gaz ücretleri, Ethereum ağında işlemleri gerçekleştiren kripto madencilerine ödenir. MetaMask gaz ücretlerinden herhangi bir kazanç elde etmemektedir.", "gas_fluctuation": "Gaz ücretleri tahmini olup ağ trafiği ve işlem karmaşıklığına göre dalgalanır.", "gas_learn_more": "Gaz ücretleri hakkında daha fazla bilgi edinin" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "Şununla giriş yapılıyor:", "spender": "Harcayan", "now": "Şimdi", - "switching_to": "Switching to", + "switching_to": "Geçiş yapılan", "bridge_estimated_time": "Tah. süre", "pay_with": "Ödeme aracı", - "receive_as": "Receive", + "receive_as": "Al", "total": "Toplam", - "you_receive": "You'll receive", + "you_receive": "Elinize geçecek miktar:", "transaction_fee": "İşlem ücreti", - "transaction_fees": "Transaction fees", + "transaction_fees": "İşlem ücretleri", "metamask_fee": "MetaMask ücreti", "network_fee": "Ağ ücreti", "bridge_fee": "Köprü sağlayıcı ücreti" @@ -6234,7 +6264,7 @@ "transaction_fee": "Tokenlerinizi Tahminler tarafından kullanılan Polygon ağı üzerinden USDC ile takas edeceğiz. Takas sağlayıcıları ücret alabilir ancak MetaMask ücret talep etmez." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMask, sizin için istediğiniz tokena takas gerçekleştirecek. MUSD'ye takas gerçekleştirirken MetaMask ücreti uygulanmaz." }, "musd_conversion": { "transaction_fee": "mUSD dönüştürme ücretlerine ağ ücretleri dahildir ve sağlayıcı ücretleri dahil olabilir." @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "Bu site imzanızı istiyor", "transaction_tooltip": "Bu site, işleminizi istiyor", "details": "Ayrıntılar", - "qr_get_sign": "Get signature", + "qr_get_sign": "İmza al", "qr_scan_text": "Donanım cüzdanınızla tarayın", "sign_with_ledger": "Ledger ile oturum aç", "smart_account": "Akıllı hesap", "smart_contract": "Akıllı sözleşme", - "standard_account": "Standard account", + "standard_account": "Standart hesap", "siwe_message": { "url": "URL Adresi", "network": "Ağ", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "Akıllı hesap", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "Standart hesap", "switch": "Değiştir", "switchBack": "Geri dön", "includes_transaction": "{{transactionCount}} işlem içerir", @@ -6307,9 +6337,9 @@ "cancel": "İptal et", "description": "Sizin adınıza harcanması konusunda rahat hissettiğiniz tutarı girin.", "invalid_number_error": "Harcama üst limiti bir sayı olmalıdır", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "Harcama üst limiti boş olamaz", + "no_extra_decimals_error": "Harcama üst limiti tokendan daha fazla ondalık basamak içeremez", + "no_zero_error": "Harcama üst limiti 0 olamaz", "no_zero_error_decrease_allowance": "0 harcama üst limitinin 'decreaseAllowance' yönteminde hiçbir etkisi yoktur", "no_zero_error_increase_allowance": "0 harcama üst limitinin 'increaseAllowance' yönteminde hiçbir etkisi yoktur", "save": "Kaydet", @@ -6336,7 +6366,7 @@ "transferRequest": "Transfer talebi", "nested_transaction_heading": "{{index}} işlemi", "transaction": "İşlem", - "available_balance": "Available balance: ", + "available_balance": "Kullanılabilir bakiye: ", "edit_amount_done": "Devam et", "deposit_edit_amount_done": "Fon ekle", "deposit_edit_amount_predict_withdraw": "Çek", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "Şart ve Koşullar", "select_token": "Token seç", "no_tokens_found": "Token bulunamadı", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "Bu isimde token bulamadık. Farklı bir arama yapmayı deneyin.", "select_network": "Ağ seç", "all_networks": "Tüm ağlar", "num_networks": "{{numNetworks}} ağ", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "Tümünün seçimini kaldır", "see_all": "Tümünü gör", "all": "Tümü", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}} tane daha", "apply": "Uygula", "slippage": "Kayma", "slippage_info": "Emrinizin verildiği zaman ile onaylandığı zaman arasında fiyat değişirse buna “kayma” denir. Kayma burada belirlediğiniz toleransı aşarsa swap işleminiz otomatik olarak iptal edilir.", @@ -6392,7 +6422,7 @@ "quote_info_title": "Oran", "network_fee_info_title": "Ağ ücreti", "network_fee_info_content": "Ağ ücretleri, ağın yoğunluğuna ve işlemin karmaşıklığına bağlıdır.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "Bu ağ ücreti MetaMask tarafından ödenir, bu nedenle hesabınızda {{nativeToken}} olmadan işlem yapabilirsiniz.", "points": "Tah. puanlar", "points_tooltip": "Puan", "points_tooltip_content_1": "Takas, köprü ve sürekli vadeli işlem sözleşmeleri gibi işlemleri tamamladığınızda puan toplar ve MetaMask Ödülleri kazanırsınız.", @@ -6406,7 +6436,7 @@ "select_recipient": "Alıcı seç", "external_account": "Harici hesap", "error_banner_description": "Bu işlem rotası şu anda kullanılamıyor. Tutarı, ağı veya token'ı değiştirmeyi deneyin ve sizin için en iyi seçeneği bulalım.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "Bu işlem rotası şu anda kullanılamıyor. Tutarı, ağı veya tokenı değiştirmeyi deneyin ve en iyi seçeneği bulalım.\n\nOndo Tokenize Edilmiş Hisse Senetleri ile işlem yapmaya çalışıyorsanız ABD, AB, Birleşik Krallık ve Brezilya gibi bölgelerde coğrafi kısıtlamaya tabi olabilirsiniz.", "insufficient_funds": "Para yetersiz", "insufficient_gas": "Yetersiz gaz", "select_amount": "Tutar seçin", @@ -6417,9 +6447,9 @@ "title": "Köprü", "submitting_transaction": "Gönderiliyor", "fetching_quote": "Teklif alınıyor", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "%{{feePercentage}} MetaMask ücreti dahildir.", "no_mm_fee": "MM ücreti yok", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "{{destTokenSymbol}} takasında MetaMask ücreti yok.", "hardware_wallet_not_supported": "Donanım cüzdanları henüz desteklenmiyor. Devam etmek için sıcak cüzdan kullanın.", "hardware_wallet_not_supported_solana": "Donanım cüzdanları henüz Solana için desteklenmiyor. Devam etmek için sıcak cüzdan kullanın.", "price_impact_info_title": "Piyasa etkisi", @@ -6432,17 +6462,24 @@ "approval_needed": "Takas işlemi için token'ı onaylar.", "approval_tooltip_title": "Tam erişim ver", "approval_tooltip_content": "{{amount}} {{symbol}} belirtilen tutara erişim veriyorsunuz. Sözleşmenin daha fazla fona erişimi olmayacak.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "Alınacak minimum", + "minimum_received_tooltip_title": "Alınacak minimum", "minimum_received_tooltip_content": "Kayma toleransınıza göre işleminiz gerçekleştirilirken fiyat değişirse alacağınız minimum tutar. Bu, likidite sağlayıcılarımızdan alınan bir tahmindir. Nihai tutarlar değişiklik gösterebilir.", + "market_closed": { + "title": "Piyasa kapalı", + "description": "Bu tokenı destekleyen piyasa şu anda kapalı. Zincir içi token transferi her zaman yapılabilir.", + "learn_more": "Daha fazla bilgi edin", + "learn_more_url": "https://status.ondo.finance/market", + "done": "Bitti" + }, "submit": "Gönder", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "Fiyat kayma yüzdesinden daha fazla değişirse işleminiz gerçekleşmez.", "cancel": "İptal", "confirm": "Onayla", "exceeding_upper_slippage_warning": "Yüksek kayma; bu durum istenmeyen bir takas ile sonuçlanabilir", "exceeding_lower_slippage_warning": "Düşük kayma; bu durum istenmeyen bir takas ile sonuçlanabilir", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "%{{value}} değerin üzerinde olan bir değer girin", + "exceeding_upper_slippage_error": "%{{value}} değerin üzerinde olan bir değer giremezsiniz", "custom": "Özel" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "Cüzdan kurtarma", "login_with_social": "Sosyal medya hesapları ile oturum aç", "setup": "Kur", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "Gizli Kurtarma İfadesi {{num}}", "back_up": "Yedekle", "reveal": "Açığa Çıkar", "social_recovery_title": "{{authConnection}} KURTARMA", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "Şifre girin", "description": "Kart bilgilerini görüntülemek için cüzdan şifrenizi girin.", + "description_unfreeze": "Kartınızda harcama yapmaya devam edebilmek için cüzdan şifrenizi girin.", "placeholder": "Şifre", "confirm": "Onayla", "cancel": "İptal et", @@ -7001,6 +7039,7 @@ "enable_card_error": "Kart etkinleştirilemedi. Lütfen daha sonra tekrar deneyin.", "view_card_details_error": "Kart bilgileri yüklenemedi. Lütfen tekrar deneyin.", "biometric_verification_required": "Kart bilgilerini görüntülemek için kimlik doğrulama gereklidir.", + "unfreeze_auth_required": "Kartınızda harcama yapmaya devam edebilmeniz için doğrulama gereklidir.", "warnings": { "close_spending_limit": { "title": "Harcama limitinize yakınsınız", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "Kartınız dondurulmuş", - "description": "Kartınızın dondurulma işlemini iptal etmek için lütfen destek ekibiyle iletişime geçin" + "description": "Kartınız geçici olarak donduruldu. Dondurma işlemini dilediğiniz zaman iptal edebilirsiniz." }, "blocked": { "title": "Kartınız bloke edilmiş", @@ -7068,7 +7107,14 @@ "travel_description": "%70'e varan indirimlerle otel rezervasyonu yapın", "card_tos_title": "Şart ve koşullar", "order_metal_card": "Metal Kart", - "order_metal_card_description": "Hemen Fiziksel Metal Kart siparişinizi verin" + "order_metal_card_description": "Hemen Fiziksel Metal Kart siparişinizi verin", + "freeze_card": "Kartı dondur", + "unfreeze_card": "Kartı dondurma işlemini iptal et", + "freeze_card_description": "Kartındaki tüm harcamaları duraklat", + "unfreeze_card_description": "Kartındaki tüm harcamaları sürdür", + "freeze_error": "Kart statüsü güncellenemedi. Lütfen tekrar deneyin.", + "freeze_success": "Kart başarıyla donduruldu", + "unfreeze_success": "Kart dondurma işlemi başarıyla iptal edildi" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "{{seconds}} saniye sonra tekrar gönderilebilir" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "Şuraya ekle: {{walletName}}", + "adding_to_wallet": "Şuraya ekleniyor: {{walletName}}...", + "continue_setup": "{{walletName}} Kurulumuna Devam Et", + "wallet_not_available": "{{walletName}} kullanılamıyor", + "already_in_wallet": "Zaten {{walletName}} içinde", + "success_title": "Kart eklendi!", + "success_message": "MetaMask Kartınız {{walletName}} cüzdanına eklendi.", + "error_title": "Kart eklenemiyor", + "error_wallet_not_available": "{{walletName}} bu cihazda kullanılamıyor. Lütfen {{walletName}} kurulumunuzun yapıldığından emin olun.", + "error_wallet_not_initialized": "{{walletName}} başlatılmadı. Lütfen cüzdanınızın kurulumunu yapın ve tekrar deneyin.", "error_card_already_in_wallet": "Bu kart zaten {{walletName}} cüzdanına eklendi.", "error_card_pending": "{{walletName}} cüzdanında kart kurulumunuz yapılıyor. Lütfen birkaç dakika sonra tekrar kontrol edin.", "error_card_suspended": "{{walletName}} cüzdanındaki kartınız askıya alındı. Yardım için lütfen destek bölümüyle iletişime geçin.", "error_card_not_eligible": "Bu kart mobil cüzdan kurulumuna uygun değildir.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "Kart verileri şifrelenemedi. Lütfen tekrar deneyin.", "error_invalid_card_data": "Kart verileri geçersiz. Lütfen kart bilgilerinizi doğrulayın ve tekrar deneyin.", "error_card_not_found": "Kart bulunamadı. Lütfen tekrar deneyin.", "error_card_provider_not_found": "Kart sağlayıcısı bölgenizde kullanılamıyor.", "error_card_id_mismatch": "Kart doğrulama işlemi başarısız oldu. Lütfen tekrar deneyin.", "error_card_not_active": "Kartınız aktif değil. Lütfen önce kartınızı aktif hale getirin.", "error_network": "Ağ hatası oluştu. Lütfen bağlantınızı kontrol edip tekrar deneyin.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "Talep zaman aşımına uğradı. Lütfen tekrar deneyin.", + "error_server": "Sunucu hatası meydana geldi. Lütfen daha sonra tekrar deneyin.", + "error_unknown": "Beklenmedik bir hata oluştu. Lütfen tekrar deneyin veya destek bölümüyle iletişime geçin.", + "error_platform_not_supported": "Bu platform mobil cüzdan kurulumunu desteklemiyor.", "try_again": "Tekrar dene", "cancel": "İptal et" } @@ -7299,7 +7345,7 @@ "main_title": "Ödüller", "referral_title": "Referanslar", "tab_overview_title": "Genel Bakış", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "Anlık görüntüler", "tab_activity_title": "Aktivite", "referral_stats_earned_from_referrals": "Referanslardan kazanılan", "referral_stats_referrals": "Referanslar", @@ -7353,7 +7399,7 @@ "verifying_rewards": "Ödüllerinizi almadan önce her şeyin doğru olduğunu teyit ediyoruz." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "Puan kazanıldı" }, "onboarding": { "not_supported_region_title": "Bölge desteklenmiyor", @@ -7431,7 +7477,7 @@ "show_less": "Daha az göster", "linking_progress": "Hesaplar ekleniyor... ({{current}}/{{total}})", "accounts_linked_count": "{{linked}}/{{total}} kayıtlı", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "Tüm hesapları ekle" }, "referred_by_code": { "title": "Referans Kodu", @@ -7514,7 +7560,7 @@ "claim_label": "Al", "claimed_label": "Alındı", "reward_claimed": "Ödül alındı", - "time_left": "{{time}} left", + "time_left": "{{time}} kaldı", "expired": "Süresi doldu" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "Kullanılamadı", "redeem_failure_description": "Lütfen daha sonra tekrar deneyin.", "reward_details": "Ödül Bilgileri", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "Bu ödülün gönderilmesini istediğiniz hesabı seçin." }, "animation": { "could_not_load": "Yüklenemedi" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", + "starts_date": "Başlangıç tarihi {{date}}", + "ends_date": "Bitiş tarihi {{date}}", + "results_coming_soon": "Sonuçlar çok yakında", + "tokens_on_the_way": "Tokenlar yolda", + "pill_up_next": "Sırada", + "pill_live_now": "Şimdi canlı", "pill_calculating": "Hesaplanıyor", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "pill_results_ready": "Sonuçlar Hazır", + "pill_complete": "Tamamlandı" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "Anlık görüntüler", + "error_title": "Anlık görüntüler yüklenemiyor", + "error_description": "Anlık görüntüleri yükleyemedik. Lütfen tekrar deneyin.", "retry_button": "Tekrar Dene" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "Aktif", + "upcoming_title": "Yaklaşan", + "previous_title": "Önceki", + "empty_state": "Anlık görüntüler kullanılamıyor", + "error_title": "Anlık görüntüler yüklenemiyor", + "error_description": "Anlık görüntüleri yükleyemedik. Lütfen tekrar deneyin.", "retry_button": "Tekrar Dene", - "refreshing": "Refreshing..." + "refreshing": "Yenileniyor..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "{{approveSymbol}} onayla", "bridge_approval_loading": "Onayla", "bridge_send": "{{sourceChain}} üzerinden {{sourceSymbol}} köprüle", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "Köprü gönder", "bridge_receive": "{{targetChain}} üzerinde {{targetSymbol}} al", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "Köprü al", "default": "İşlem", "musd_convert_send": "{{sourceChain}} alanından {{sourceSymbol}} gönderildi", "musd_claim": "mUSD al", @@ -7607,20 +7653,20 @@ "description": "{{dappName}} ile bağlantı kuruluyor..." }, "show_error": { - "title": "Connection error", + "title": "Bağlantı hatası", "description": "Bağlantı kurulamadı. Lütfen tekrar deneyin." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "Onay reddedildi", + "description": "Kullanıcı talebi reddetti." }, "show_return_to_app": { "title": "Başarılı", "description": "Devam etmek için uygulamaya geri dönün." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "Bağlantı Bulunamadı", + "description": "Devam etmek için lütfen uygulamadan yeni bir bağlantı kurun." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "Keşfet", + "trending_tokens": "Trend tokenlar", "price_change": "Fiyat değişikliği", "all_networks": "Tüm ağlar", - "24h": "24h", + "24h": "24s", "time": "Zaman", "24_hours": "24 saat", "6_hours": "6 saat", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 saat", + "5_minutes": "5 dakika", "networks": "Ağlar", "sort_by": "Şuna göre sırala", "volume": "Hacim", @@ -7650,32 +7696,48 @@ "high_to_low": "Yüksekten düşüğe", "low_to_high": "Düşükten yükseğe", "apply": "Uygula", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "Token, site, URL ara", "cancel": "İptal", "perps": "Sürekli Vadeli", "predictions": "Tahminler", - "no_results": "No results found", + "no_results": "Sonuç bulunamadı", "sites": "Siteler", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "Popüler siteler", + "search_sites": "Siteleri ara", + "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.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "Trend tokenlar kullanılamıyor", + "description": "Şu anda bu sayfayı getiremiyoruz", "try_again": "Tekrar dene" }, "empty_search_result_state": { "title": "Token bulunamadı", - "description": "We were not able to find this token" + "description": "Bu tokenı bulamadık" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "Güncelleme hazır", + "description_ios": "Bazı önemli düzeltmeler yaptık. MetaMask'in son sürümünü tekrar yükleyin.", + "description_android": "Bazı önemli düzeltmeler yaptık. Güncellemeyi uygulamak için MetaMask'i kapatıp tekrar açın.", "primary_action_reload": "Tekrar Yükle", "primary_action_acknowledge": "Anladım" + }, + "homepage": { + "sections": { + "tokens": "tokenlar", + "perpetuals": "Sürekli Vadeli İşlemler", + "predictions": "Tahminler", + "defi": "DeFi", + "nfts": "NFT'ler", + "import_nfts": "NFT'leri içe aktar", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/vi.json b/locales/languages/vi.json index f7ee29f3c9f..59d842f14b6 100644 --- a/locales/languages/vi.json +++ b/locales/languages/vi.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "xóa vĩnh viễn", "reset_wallet_desc_2": "khỏi MetaMask trên thiết bị này. Hành động này không thể hoàn tác.", "reset_wallet_desc_login": "Để khôi phục ví, bạn có thể sử dụng Cụm từ khôi phục bí mật hoặc mật khẩu tài khoản Google hoặc Apple của bạn. MetaMask không lưu trữ thông tin này.", - "reset_wallet_desc_srp": "Để khôi phục ví, hãy đảm bảo bạn có Cụm từ khôi phục bí mật. MetaMask không lưu trữ thông tin này." + "reset_wallet_desc_srp": "Để khôi phục ví, hãy đảm bảo bạn có Cụm từ khôi phục bí mật. MetaMask không lưu trữ thông tin này.", + "biometric_authentication_cancelled": "Xác thực sinh trắc học đã bị hủy", + "biometric_authentication_cancelled_title": "Thiết lập sinh trắc học thất bại", + "biometric_authentication_cancelled_description": "Vui lòng thiết lập lại xác thực sinh trắc học trong phần cài đặt.", + "biometric_authentication_cancelled_button": "Xác nhận" }, "connect_hardware": { "title_select_hardware": "Kết nối ví cứng", @@ -1040,7 +1044,7 @@ "title": "Số tiền cần nạp", "get_usdc_hyperliquid": "Nhận USDC • Hyperliquid", "insufficient_funds": "Không đủ tiền", - "no_funds_available": "Không có sẵn tiền. Vui lòng nạp tiền trước.", + "no_funds_available": "Số tiền khả dụng không đủ. Vui lòng nạp tiền hoặc chọn phương thức thanh toán khác", "enter_amount": "Nhập số tiền", "fetching_quote": "Tìm nạp báo giá", "submitting": "Đang gửi giao dịch", @@ -1970,8 +1974,8 @@ "trade_again": "Giao dịch lại", "activity": { "deposit_title": "Nạp tiền", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "Đã nạp {{amount}} {{symbol}}", + "withdrew_amount": "Đã rút {{amount}} {{symbol}}", "status_completed": "Đã hoàn tất", "status_failed": "Không thành công", "status_pending": "Chờ xử lý" @@ -2051,6 +2055,16 @@ "referral_code_text": "Sử dụng mã giới thiệu của tôi để nhận thêm phần thưởng." } }, + "market_insights": { + "title": "Thông tin thị trường", + "updated_ago": "Đã cập nhật {{time}}", + "disclaimer": "Phân tích AI. Không phải lời khuyên tài chính.", + "whats_driving_price": "Điều gì đang thúc đẩy giá?", + "what_people_saying": "Mọi người đang bàn luận về điều gì", + "trade_button": "Giao dịch", + "sources_count": "+{{count}} nguồn", + "sources_title": "Có tính thanh khoản cao nhất" + }, "predict": { "title": "MetaMask Dự đoán", "prediction_markets": "Thị trường dự đoán", @@ -2384,8 +2398,8 @@ "no_available_tokens": "Không thấy Token của bạn?", "add_tokens": "Nhập token", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "Đã tìm thấy {{tokenCount}} {{tokensLabel}} mới trong tài khoản này", "token_toast": { "tokens_imported_title": "Token đã nhập", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "Số thập phân Token không thể để trống.", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "Chúng tôi không thể tìm ra Token nào có tên đó.", + "tokens_empty_description": "Search for any token and import it", "select_token": "Chọn token", "address_must_be_smart_contract": "Đã phát hiện địa chỉ cá nhân. Hãy nhập địa chỉ hợp đồng của Token.", "billion_abbreviation": "Tỷ", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "Ngắt kết nối tất cả các tài khoản", "deceptive_site_ahead": "Phía trước là trang web lừa đảo", "deceptive_site_desc": "Trang web mà bạn đang cố gắng truy cập không an toàn. Những kẻ tấn công có thể lừa bạn thực hiện một hành động nguy hiểm.", + "malicious_site_detected": "Phát hiện trang web độc hại", + "malicious_site_warning": "Nếu bạn kết nối với trang web này, bạn có thể mất toàn bộ tài sản của mình.", + "connect_anyway": "Vẫn kết nối", "learn_more": "Tìm hiểu thêm", "advisory_by": "Cảnh báo được cung cấp bởi Trình phát hiện lừa đảo qua mạng Ethereum và PhishFort", "potential_threat": "Các mối đe dọa tiềm ẩn bao gồm", @@ -2846,7 +2864,11 @@ "permissions": "Quyền", "card_title": "Thẻ MetaMask", "settings": "Cài đặt", - "log_out": "Đăng xuất" + "networks": "Mạng", + "log_out": "Đăng xuất", + "notifications": "Thông báo", + "buy": "Mua", + "scan": "Quét" }, "app_settings": { "enabling_notifications": "Đang bật thông báo...", @@ -2870,6 +2892,8 @@ "state_logs": "Nhật ký trạng thái", "add_network_title": "Thêm mạng", "auto_lock": "Tự động khóa", + "enable_device_authentication": "Bật xác thực thiết bị", + "enable_device_authentication_desc": "Sử dụng sinh trắc học hoặc mật mã của thiết bị để mở khóa MetaMask.", "auto_lock_desc": "Chọn khoảng thời gian trước khi ứng dụng tự động khóa.", "state_logs_desc": "Điều này sẽ giúp MetaMask gỡ lỗi bất kỳ sự cố nào bạn có thể gặp phải. Vui lòng gửi đến bộ phận hỗ trợ MetaMask thông qua biểu tượng bánh hamburger > Gửi phản hồi hoặc trả lời phiếu hỏi đáp hiện tại của bạn nếu có.", "autolock_immediately": "Ngay lập tức", @@ -2975,6 +2999,11 @@ "add_rpc_url": "Thêm URL RPC", "add_block_explorer_url": "Thêm URL trình khám phá khối", "networks_desc": "Thêm và sửa mạng RPC tùy chỉnh", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "Tìm kiếm mạng", + "networks_no_results": "Không tìm thấy mạng nào", "network_name_label": "Tên mạng", "network_name_placeholder": "Tên mạng (không bắt buộc)", "network_rpc_url_label": "URL RPC", @@ -2991,7 +3020,16 @@ "network_other_networks": "Mạng khác", "network_rpc_networks": "Mạng RPC", "network_add_network": "Thêm mạng", + "add_chain_title": "Thêm mạng", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "Thử lại", + "add_chain_added": "Added", + "add_chain_or": "hoặc", + "add_chain_custom_link": "Thêm mạng tùy chỉnh", "network_add_custom_network": "Thêm mạng tùy chỉnh", + "network_add_test_network": "Add a test network", "network_add": "Thêm", "network_save": "Lưu", "remove_network_title": "Bạn có muốn gỡ bỏ mạng này?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "OK", "title": "Tài khoản không thể kết nối", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "Vui lòng quét mã QR trên trang web để kết nối lại với MetaMask" }, "app_information": { "title": "Thông tin", @@ -3379,6 +3417,7 @@ "sell_description": "Bán tiền mã hóa lấy tiền mặt" }, "asset_overview": { + "market_closed": "Thị trường đã đóng cửa", "send_button": "Gửi", "buy_button": "Mua", "cash_buy_button": "Mua bằng tiền mặt", @@ -3399,19 +3438,6 @@ "bridge": "Cầu nối", "earn": "Kiếm lợi nhuận", "convert_to_musd": "Chuyển đổi sang mUSD", - "merkl_rewards": { - "annual_bonus": "Thưởng {{apy}}%", - "claimable_bonus": "Thưởng có thể nhận", - "claimable_bonus_tooltip_description": "Đã nhận thưởng mUSD trên Linea.", - "terms_apply": "Áp dụng điều khoản.", - "ok": "OK", - "claim": "Nhận", - "processing_claim": "Đang xử lý nhận thưởng...", - "claim_on_linea_title": "Nhận thưởng trên Linea", - "claim_on_linea_description": "Khoản thưởng của bạn sẽ được phát hành trên Linea, tách biệt với số dư mUSD Ethereum của bạn.", - "continue": "Tiếp tục", - "unexpected_error": "Lỗi không mong muốn. Vui lòng thử lại." - }, "tron": { "daily_resource_new_energy": "Năng lượng hằng ngày mới", "sufficient_to_cover": "Đủ để thực hiện", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "Đã sao chép địa chỉ token vào bộ nhớ đệm" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "Mã QR không hợp lệ", "invalid_qr_code_message": "Mã QR bạn đang cố quét không hợp lệ.", "allow_camera_dialog_title": "Cho phép truy cập camera", "allow_camera_dialog_message": "Chúng tôi cần sự cho phép của bạn để quét mã QR", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "Có vẻ như bạn đang cố đồng bộ với tiện ích mở rộng. Để làm như vậy, bạn sẽ cần xóa ví hiện tại của mình. \n\nSau khi bạn đã xóa hoặc cài đặt lại phiên bản ứng dụng mới, hãy chọn tùy chọn \"Đồng bộ với Tiện ích mở rộng MetaMask\". Lưu ý quan trọng! Trước khi xóa ví, hãy đảm bảo rằng bạn đã sao lưu Cụm từ khôi phục bí mật.", "not_allowed_error_title": "Bật quyền truy cập camera", "not_allowed_error_desc": "Để quét mã QR, bạn cần cấp quyền truy cập camera cho MetaMask từ trình đơn cài đặt của thiết bị.", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "Không nhận diện được mã QR", "unrecognized_address_qr_code_desc": "Rất tiếc, mã QR này không được liên kết với bất kỳ địa chỉ tài khoản hoặc địa chỉ hợp đồng nào.", "url_redirection_alert_title": "Bạn sắp truy cập một liên kết bên ngoài", "url_redirection_alert_desc": "Các liên kết có thể được sử dụng để cố gắng gian lận hoặc lừa đảo mọi người, vì vậy hãy đảm bảo chỉ truy cập các trang web mà bạn tin tưởng.", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "Bạn không sở hữu bộ sưu tập này", "known_asset_contract": "Địa chỉ hợp đồng tài sản đã biết", "max": "Tối đa", - "recipient_address": "Recipient address", + "recipient_address": "Địa chỉ người nhận", "required": "Bắt buộc", "to": "Đến", "total": "Tổng", @@ -3641,7 +3667,7 @@ "nevermind": "Đừng bận tâm", "edit_network_fee": "Sửa phí gas", "edit_priority": "Sửa mức ưu tiên", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "Phí hủy gas", "gas_speedup_fee": "Phí gas tăng tốc", "use_max": "Dùng tối đa", "set_gas": "Thiết lập", @@ -3650,7 +3676,7 @@ "transaction_fee": "Phí gas", "transaction_fee_less": "Không tốn phí", "total_amount": "Tổng số tiền", - "view_data": "View data", + "view_data": "Xem dữ liệu", "adjust_transaction_fee": "Chỉnh sửa phí giao dịch", "could_not_resolve_ens": "Không thể giải quyết ENS", "asset": "Tài sản", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "Để duyệt trang web phi tập trung, hãy thêm một thẻ mới", "got_it": "Đã hiểu", "max_tabs_title": "Đã đạt đến số lượng tab tối đa", - "max_tabs_desc": "Chúng tôi hiện chỉ hỗ trợ tối đa 5 tab mở cùng lúc. Vui lòng đóng các tab hiện có trước khi thêm tab mới.", + "max_tabs_desc": "Chúng tôi hiện chỉ hỗ trợ tối đa 20 tab mở cùng lúc. Vui lòng đóng các tab hiện có trước khi thêm tab mới.", "failed_to_resolve_ens_name": "Chúng tôi không thể giải quyết tên ENS đó", "remove_bookmark_title": "Gỡ bỏ mục ưa thích", "remove_bookmark_msg": "Bạn có thực sự muốn gỡ bỏ trang web này ra khỏi mục ưa thích?", @@ -3828,7 +3854,7 @@ "cancel_button": "Hủy" }, "approval": { - "title": "Confirm transaction" + "title": "Xác nhận giao dịch" }, "approve": { "title": "Phê duyệt", @@ -3839,39 +3865,39 @@ "unavailable": "Không khả dụng", "tx_review_confirm": "Xác nhận", "tx_review_transfer": "Chuyển", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "Triển khai hợp đồng", + "tx_review_transfer_from": "Chuyển từ", + "tx_review_unknown": "Phương thức không xác định", "tx_review_approve": "Phê duyệt", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "Tăng hạn mức", + "tx_review_set_approval_for_all": "Thiết lập phê duyệt tất cả", + "tx_review_staking_claim": "Nhận tài sản ký gửi", "tx_review_staking_deposit": "Nạp tài sản ký gửi", "tx_review_staking_unstake": "Hủy ký gửi", "tx_review_lending_deposit": "Nạp tài sản cho vay", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "Rút tiền cho vay", "tx_review_perps_deposit": "Hợp đồng vĩnh cửu đã được nạp tiền", "tx_review_predict_deposit": "Dự đoán đã nạp tiền", "tx_review_predict_claim": "Tiền thắng đã nhận", "tx_review_predict_withdraw": "Rút tiền dự đoán", "tx_review_musd_conversion": "Chuyển đổi mUSD", "claim": "Nhận", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "Đã gửi ETH", + "self_sent_ether": "Đã gửi ETH cho chính bạn", + "received_ether": "Đã nhận ETH", "sent_dai": "Đã gửi DAI", "self_sent_dai": "Đã gửi DAI cho chính bạn", "received_dai": "Đã nhận DAI", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "Đã gửi token", + "received_tokens": "Đã nhận token", "ether": "ETH", "sent_unit": "Đã gửi {{unit}}", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "Đã gửi {{unit}} cho chính bạn", "received_unit": "Đã nhận {{unit}}", "sent_collectible": "Đã gửi bộ sưu tập", "received_collectible": "Đã nhận bộ sưu tập", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "Gửi ETH", + "send_unit": "Gửi {{unit}}", "send_collectible": "Gửi bộ sưu tập", "receive_collectible": "Nhận bộ sưu tập", "sent": "Đã gửi", @@ -3881,17 +3907,17 @@ "send": "Gửi", "redeposit": "Nạp tiền lại", "interaction": "Tương tác", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "Triển khai hợp đồng", + "to_contract": "Hợp đồng mới", + "mint": "Đúc", "tx_details_free": "Miễn phí", "tx_details_not_available": "Không có sẵn", "smart_contract_interaction": "Tương tác hợp đồng thông minh", "swaps_transaction": "Giao dịch hoán đổi", "bridge_transaction": "Cầu nối", "approve": "Phê duyệt", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "Tăng hạn mức", + "set_approval_for_all": "Thiết lập phê duyệt tất cả", "hash": "Mã băm", "from": "Từ", "to": "Đến", @@ -3899,15 +3925,15 @@ "amount": "Số tiền", "fee": { "transaction_fee_in_ether": "Phí giao dịch", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "Phí giao dịch (USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "Gas đã sử dụng (đơn vị)", + "gas_limit": "Hạn mức gas (đơn vị)", + "gas_price": "Giá gas (GWEI)", + "base_fee": "Phí cơ sở (GWEI)", + "priority_fee": "Phí ưu tiên (GWEI)", "multichain_priority_fee": "Phí ưu tiên", - "max_fee": "Max fee per gas", + "max_fee": "Phí tối đa trên mỗi gas", "total": "Tổng", "view_on": "Xem trên", "view_on_etherscan": "Xem trên Etherscan", @@ -3923,13 +3949,13 @@ "nonce": "Nonce", "from_device_label": "từ thiết bị này", "import_wallet_row": "Tài khoản đã được thêm vào thiết bị này", - "import_wallet_label": "Account added", + "import_wallet_label": "Đã thêm tài khoản", "import_wallet_tip": "Tất cả giao dịch trong tương lai được thực hiện từ thiết bị này sẽ có nhãn \"từ thiết bị này\" bên cạnh dấu thời gian. Đối với các giao dịch được ghi ngày trước khi thêm tài khoản, lịch sử này sẽ không cho biết giao dịch gửi đi nào bắt nguồn từ thiết bị này.", "sign_title_scan": "Quét ", "sign_title_device": "bằng ví cứng của bạn", "sign_description_1": "Sau khi bạn đã ký bằng ví cứng,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "Nhấn vào lấy chữ ký", + "sign_get_signature": "Lấy chữ ký", "transaction_id": "ID giao dịch", "network": "Mạng", "request_from": "Yêu cầu từ", @@ -4032,7 +4058,7 @@ "title": "Mạng", "other_networks": "Mạng khác", "close": "Đóng", - "status_ok": "All systems operational", + "status_ok": "Tất cả hệ thống đều hoạt động", "status_not_ok": "Mạng đang gặp một số sự cố", "want_to_add_network": "Bạn muốn thêm mạng này?", "add_custom_network": "Thêm mạng tùy chỉnh", @@ -4051,7 +4077,7 @@ "review": "Xem lại", "view_details": "Xem chi tiết", "network_details": "Chi tiết về mạng", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "Việc chọn xác nhận sẽ bật kiểm tra thông tin mạng. Bạn có thể tắt kiểm tra thông tin mạng trong ", "network_settings_security_privacy": "Cài đặt > Bảo mật và quyền riêng tư", "network_currency_symbol": "Biểu tượng tiền tệ", "network_block_explorer_url": "URL của Trình khám phá khối", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "Một nhà cung cấp mạng độc hại có thể nói dối về trạng thái của chuỗi khối và ghi lại hoạt động của bạn trên mạng. Chỉ thêm các mạng tùy chỉnh mà bạn tin tưởng.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Thông tin mạng", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "Thông tin mạng bổ sung", "network_warning_desc": "Kết nối mạng này dựa vào các bên thứ ba. Kết nối này có thể kém tin cậy hơn hoặc cho phép các bên thứ ba theo dõi hoạt động.", "additonial_network_information_desc": "Một vài mạng trong số này phụ thuộc vào bên thứ ba. Kết nối có thể kém tin cậy hơn hoặc cho phép bên thứ ba theo dõi hoạt động.", "connect_more_networks": "Kết nối thêm mạng", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "Mạng này đã ngừng sử dụng", "network_deprecated_description": "Mạng bạn đang cố gắng kết nối không còn được hỗ trợ trên MetaMask.", "edit_networks_title": "Chỉnh sửa mạng", - "no_network_fee": "No network fee" + "no_network_fee": "Không có phí mạng" }, "permissions": { "title_this_site_wants_to": "Trang web này muốn:", @@ -4111,11 +4137,11 @@ "network_connected": "đã kết nối mạng ", "see_your_accounts": "Xem tài khoản của bạn và đề xuất giao dịch", "connected_to": "Đã kết nối với ", - "manage_permissions": "Manage permissions", + "manage_permissions": "Quản lý quyền", "edit": "Chỉnh sửa", "cancel": "Hủy", "got_it": "Đã hiểu", - "connection_details_title": "Connection details", + "connection_details_title": "Chi tiết kết nối", "connection_details_description": "Bạn đã kết nối với trang web này bằng trình duyệt MetaMask vào {{connectionDateTime}}", "title_add_network_permission": "Thêm quyền truy cập mạng", "add_this_network": "Thêm mạng này", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "Mở khóa bằng mã PIN của thiết bị?" }, "authentication": { - "auth_prompt_title": "Yêu cầu xác thực", - "auth_prompt_desc": "Vui lòng xác thực để sử dụng MetaMask", - "fingerprint_prompt_title": "Yêu cầu xác thực", - "fingerprint_prompt_desc": "Sử dụng vân tay của bạn để mở khóa MetaMask", - "fingerprint_prompt_cancel": "Hủy" + "auth_prompt_desc": "Vui lòng xác thực để sử dụng MetaMask" }, "accountApproval": { "title": "YÊU CẦU KẾT NỐI", "walletconnect_title": "YÊU CẦU WALLETCONNECT", "action": "Kết nối với trang web này?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "Để tiếp tục kết nối, hãy chọn số bạn thấy trên trang web", + "action_reconnect_deeplink": "Bạn có muốn kết nối lại với trang web này không?", "connect": "Kết nối", "resume": "Bắt đầu lại", "cancel": "Hủy", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "Không ghi nhớ kết nối với trang web này", "disconnect": "Ngắt kết nối", "permission": "Xem", "address": "địa chỉ công khai", @@ -4218,7 +4240,7 @@ "error_title": "Đã xảy ra sự cố", "error_message": "Chúng tôi không thể nhập khóa cá nhân. Vui lòng đảm bảo bạn đã nhập chính xác.", "error_empty_message": "Chúng tôi cần nhập khóa cá nhân của bạn.", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "hoặc quét mã QR" }, "import_private_key_success": { "title": "Tài khoản đã được nhập thành công!", @@ -4229,18 +4251,18 @@ "import_wallet_title": "Nhập ví", "enter_srp_subtitle": "Nhập Cụm từ khôi phục bí mật của bạn", "textarea_placeholder": "Thêm dấu cách giữa mỗi từ và đảm bảo không có ai đang nhìn", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "Nhập Cụm từ khôi phục bí mật của ví. Bạn có thể nhập bất kỳ Cụm từ khôi phục bí mật nào của Ethereum, Solana hoặc Bitcoin.", + "subtitle": "Dán Cụm từ khôi phục bí mật của bạn", "cta_text": "Tiếp tục", "paste": "Dán", "clear": "Xóa tất cả", "srp_number_of_words_option_title": "Số lượng từ", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "Tôi có cụm từ gồm 12 từ", + "24_word_option": "Tôi có cụm từ gồm 24 từ", "error_title": "Đã xảy ra sự cố", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "Chúng tôi không thể nhập Cụm từ khôi phục bí mật đó. Vui lòng đảm bảo bạn đã nhập chính xác.", + "error_empty_message": "Bạn cần nhập Cụm từ khôi phục bí mật của mình.", + "error_number_of_words_error_message": "Cụm từ khôi phục bí mật gồm 12 hoặc 24 từ", "error_srp_is_case_sensitive": "Dữ liệu đã nhập không hợp lệ! Cụm từ khôi phục bí mật có phân biệt chữ hoa và chữ thường.", "error_srp_word_error_1": "Từ ", "error_srp_word_error_2": " không đúng hoặc sai chính tả.", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " không đúng hoặc sai chính tả.", "error_invalid_srp": "Cụm từ khôi phục bí mật không hợp lệ", "error_duplicate_srp": "Cụm từ khôi phục bí mật này đã được nhập rồi.", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "Tài khoản bạn đang cố nhập là tài khoản trùng lặp.", + "invalid_qr_code_title": "Mã QR không hợp lệ", + "invalid_qr_code_message": "Mã QR không chứa Cụm từ khôi phục bí mật hợp lệ", "success_1": "Ví", "success_2": "đã nhập" }, @@ -4665,7 +4687,7 @@ "button": "Bảo vệ ví" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "Cập nhật giao dịch thất bại", "text": "Bạn có muốn thử lại?", "cancel_button": "Hủy", "retry_button": "Thử lại" @@ -4684,13 +4706,13 @@ "next": "Tiếp", "amount_placeholder": "0,00", "link_copied": "Đã sao chép liên kết vào bộ nhớ đệm", - "send_link_title": "Send link", + "send_link_title": "Gửi liên kết", "description_1": "Liên kết mà bạn yêu cầu đã sẵn sàng để gửi!", "description_2": "Gửi liên kết này cho một người bạn, và nó sẽ đề nghị họ gửi", "copy_to_clipboard": "Sao chép vào bộ nhớ đệm", "qr_code": "Mã QR", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "Gửi liên kết", + "request_qr_code": "Mã QR yêu cầu thanh toán", "balance": "Số dư" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "Bạn không có phiên đang hoạt động nào", - "end_session_title": "End session", + "end_session_title": "Kết thúc phiên", "end": "Kết thúc", "cancel": "Hủy", - "session_ended_title": "Session ended", + "session_ended_title": "Phiên đã kết thúc", "session_ended_desc": "Phiên được chọn đã bị chấm dứt", "session_already_exist": "Phiên này đã được kết nối.", "close_current_session": "Đóng phiên hiện tại trước khi bắt đầu một phiên mới." @@ -4765,15 +4787,14 @@ "on_network": "trên {{networkName}}", "debit_card": "Thẻ ghi nợ", "select_payment_method": "Chọn phương thức thanh toán", - "loading_quote": "Loading quote...", "pay_with": "Thanh toán bằng", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "Mua qua {{providerName}}.", + "change_provider": "Thay đổi nhà cung cấp.", "payment_error": "Đã xảy ra lỗi. Vui lòng thử lại.", - "no_payment_methods_available": "No payment methods are available.", + "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.", "no_quotes_available": "Hiện không có nhà cung cấp nào.", - "providers": "Providers", + "providers": "Nhà cung cấp", "continue": "Tiếp tục", "powered_by_provider": "Cung cấp bởi {{provider}}", "purchased_currency": "Đã mua {{currency}}", @@ -4871,6 +4892,15 @@ "log_out": "Đăng xuất khỏi {{provider}}", "logged_out_success": "Đã đăng xuất thành công", "logged_out_error": "Lỗi khi đăng xuất" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "giới hạn bán thấp nhất", "medium_sell_limit": "giới hạn bán trung bình", "highest_sell_limit": "giới hạn bán cao nhất", - "change": "Change", + "change": "Thay đổi", "continue_to_amount": "Tiếp tục đến số lượng", "no_payment_methods_title": "Không có phương thức thanh toán tại {{regionName}}", "no_cash_destinations_title": "Không có điểm nhận tiền tại {{regionName}}", @@ -5118,7 +5148,7 @@ "start_swapping": "Bắt đầu hoán đổi" }, "feature_off_title": "Tạm thời không được cung cấp", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "MetaMask Hoán đổi đang được bảo trì. Vui lòng quay lại sau.", "wrong_network_title": "Giao dịch hoán đổi không có sẵn", "wrong_network_body": "Bạn chỉ có thể hoán đổi những Token trên Mạng chính của Ethereum.", "unallowed_asset_title": "Không thể hoán đổi Token này", @@ -5160,7 +5190,7 @@ "not_enough": "Không đủ {{symbol}} để hoàn thành giao dịch hoán đổi này", "max_slippage": "Mức trượt giá tối đa", "max_slippage_amount": "Mức trượt giá tối đa {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "Nếu tỷ giá thay đổi giữa thời điểm bạn đặt lệnh và xác nhận, điều đó được gọi là “trượt giá”. Giao dịch hoán đổi của bạn sẽ tự động bị hủy nếu mức trượt giá vượt quá cài đặt “trượt giá tối đa” của bạn.", "slippage_warning": "Đảm bảo bạn hiểu rõ điều mình đang làm!", "allows_up_to_decimals": "{{symbol}} cho phép tối đa {{decimals}} số thập phân", "get_quotes": "Nhận báo giá", @@ -5199,7 +5229,7 @@ "edit": "Sửa", "quotes_include_fee": "Báo giá bao gồm {{fee}}% phí MetaMask", "quotes_include_gas_and_metamask_fee": "Báo giá bao gồm phí gas và phí MetaMask {{fee}}%", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "Nhấn để hoán đổi", "swipe_to_swap": "Vuốt để hoán đổi", "swipe_to": "Vuốt để", "swap": "Hoán đổi", @@ -5259,7 +5289,7 @@ "approve": "Phê duyệt {{sourceToken}} cho giao dịch hoán đổi: Tối đa {{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "Hoán đổi đang chờ xử lý ({{sourceToken}} sang {{destinationToken}})", "swap_confirmed": "Hoán đổi hoàn tất ({{sourceToken}} sang {{destinationToken}})", "approve_pending": "Đang phê duyệt {{sourceToken}} cho giao dịch hoán đổi", "approve_confirmed": "{{sourceToken}} đã được phê duyệt cho giao dịch hoán đổi" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "Trình đơn chọn mạng đã chuyển vào phần tài sản", "description_2": "Hoán đổi và Cầu nối trong một quy trình đơn giản", - "description_3": "Streamlined send experience", + "description_3": "Trải nghiệm gửi được tối ưu hóa", "description_4": "Giao diện tài khoản mới mẻ" }, "more_information": "Giờ đây bạn có thể tập trung vào token và hoạt động của mình, thay vì các mạng đằng sau.", @@ -5406,21 +5436,21 @@ "aggressive_label": "Cao", "aggressive_text": "Khả năng thành công cao, ngay cả khi thị trường biến động. Sử dụng phí Linh hoạt để bù đắp khi lưu lượng mạng tăng vọt trong những trường hợp như phát hành NFT nổi tiếng.", "market_label": "Thị trường", - "market_text": "Use market for fast processing at current market price.", + "market_text": "Sử dụng lệnh thị trường để xử lý nhanh ở mức giá thị trường hiện tại.", "low_label": "Thấp", "low_text": "Sử dụng mức thấp để chờ giá rẻ hơn. Thời gian dự kiến sẽ kém chính xác hơn nhiều do mức giá tương đối khó dự đoán.", "link": "Tìm hiểu thêm về việc tùy chỉnh gas." }, "save": "Lưu", "submit": "Gửi", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "Phí ưu tiên tối đa thấp so với điều kiện mạng hiện tại", + "max_priority_fee_high": "Phí ưu tiên tối đa cao hơn mức cần thiết", + "max_priority_fee_speed_up_low": "Phí ưu tiên tối đa phải ít nhất là {{speed_up_floor_value}} GWEI (cao hơn 10% so với giao dịch ban đầu)", + "max_priority_fee_cancel_low": "Phí ưu tiên tối đa phải ít nhất là {{cancel_value}} GWEI (cao hơn 50% so với giao dịch ban đầu)", + "max_fee_low": "Phí tối đa thấp so với điều kiện mạng hiện tại", + "max_fee_high": "Phí tối đa cao hơn mức cần thiết", + "max_fee_speed_up_low": "Phí tối đa phải ít nhất là {{speed_up_floor_value}} GWEI (cao hơn 10% so với giao dịch ban đầu)", + "max_fee_cancel_low": "Phí tối đa phải ít nhất là {{cancel_value}} GWEI (cao hơn 50% so với giao dịch ban đầu)", "learn_more_gas_limit": "Giới hạn gas là số đơn vị gas tối đa mà bạn sẵn sàng sử dụng. Đơn vị gas là một hệ số nhân với \"Phí ưu tiên tối đa\" và \"Phí tối đa\". ", "learn_more_max_priority_fee": "Phí ưu tiên tối đa (còn gọi là \"tiền thưởng cho người khai thác\") được chuyển thẳng tới người khai thác và khuyến khích họ ưu tiên cho giao dịch của bạn. Thường thì bạn sẽ trả theo mức cài đặt tối đa của mình. ", "learn_more_max_fee": "Phí tối đa là số tiền tối đa bạn sẽ trả (phí cơ bản + phí ưu tiên). ", @@ -5530,9 +5560,9 @@ "enable_remember_me_description": "Khi bật Ghi nhớ, bất kỳ ai có quyền truy cập vào điện thoại của bạn đều có thể truy cập vào tài khoản MetaMask của bạn." }, "turn_off_remember_me": { - "title": "Nhập mật khẩu để tắt Ghi nhớ", - "placeholder": "Mật khẩu", - "description": "Nếu bạn tắt tùy chọn này, bạn sẽ cần nhập mật khẩu để mở khóa MetaMask kể từ bây giờ.", + "title": "Tắt Ghi nhớ", + "placeholder": "Xác nhận mật khẩu", + "description": "Sau khi tắt, tính năng Ghi nhớ sẽ không thể sử dụng lại. Tính năng này đã bị ngừng hỗ trợ, vì vậy bạn có thể mở khóa MetaMask bằng mật khẩu hoặc sinh trắc học.", "action": "Tắt Ghi nhớ" }, "dapp_connect": { @@ -5582,7 +5612,7 @@ "learn_more": "Tìm hiểu thêm" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "Xác minh thông tin bên thứ ba", "protect_from_scams": "Để bảo vệ chính mình khỏi những kẻ lừa đảo, hãy dành chút thời gian để xác minh thông tin bên thứ ba.", "learn_to_verify": "Tìm hiểu cách xác minh thông tin bên thứ ba", "spending_cap": "giới hạn chi tiêu", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "Cần khôi phục", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "Đã xảy ra sự cố, nhưng đừng lo! Hãy thử khôi phục ví của bạn.", "restore_needed_action": "Khôi phục ví" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "Không đóng được ứng dụng đang chạy trên thiết bị Ledger của bạn.", "ethereum_app_not_installed": "Chưa cài đặt ứng dụng Ethereum.", "ethereum_app_not_installed_error": "Vui lòng cài đặt ứng dụng Ethereum trên thiết bị Ledger của bạn.", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "Ứng dụng Ethereum chưa được mở", + "eth_app_not_open_message": "Vui lòng mở ứng dụng Ethereum trên thiết bị Ledger của bạn.", "ledger_is_locked": "Ledger đã khóa", "unlock_ledger_message": "Vui lòng mở khóa thiết bị Ledger của bạn", "cannot_get_account": "Không thể lấy tài khoản", @@ -5797,8 +5827,8 @@ "error_description": "Cài đặt {{snap}} thất bại." }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "Thưởng hằng năm có thể nhận hằng ngày từ ví của bạn.", + "earn_a_percentage_bonus": "Nhận thưởng {{percentage}}%", "claimable_bonus": "Thưởng có thể nhận", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "Thời gian cần thiết để rút token khỏi giao thức và nhận lại trong ví của bạn", "receive": "Token này được sử dụng để theo dõi tài sản và phần thưởng của bạn. Không chuyển nhượng hoặc giao dịch chúng, nếu không bạn sẽ không thể rút tài sản của mình.", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "Chỉ số an toàn đo lường rủi ro bị thanh lý", "above_two_dot_zero": "Trên 2,0", "safe_position": "Vị thế an toàn", "between_one_dot_five_and_2_dot_zero": "Từ 1,5 – 2,0", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "Rủi ro thanh lý trung bình", "below_one_dot_five": "Dưới 1,5", "higher_liquidation_risk": "Rủi ro thanh lý cao hơn" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "Tại sao tôi không thể rút toàn bộ số dư?", "your_withdrawal_amount_may_be_limited_by": "Số tiền bạn có thể rút có thể bị giới hạn bởi", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "Khả năng thanh khoản của bể", "not_enough_funds_available_in_the_lending_pool_right_now": "Hiện không có đủ tiền trong bể cho vay.", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "Các vị thế vay hiện có", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "Việc rút tiền có thể khiến các vị thế khoản vay hiện có của bạn gặp rủi ro bị thanh lý." } }, @@ -5998,11 +6028,11 @@ "earn_button": "Kiếm lợi nhuận" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "Ký gửi TRX và kiếm tiền", + "stake_any_amount": "Ký gửi bất kỳ số lượng TRX nào.", "earn_trx_rewards": "Nhận phần thưởng TRX.", "earn_trx_rewards_description": "Bắt đầu nhận thưởng ngay khi bạn ký gửi. Phần thưởng sẽ tự động cộng dồn.", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "Hủy ký gửi bất cứ lúc nào. Quá trình hủy ký gửi mất 14 ngày để xử lý." }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "Phí gas ước tính", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "Phí gas được trả cho thợ đào tiền mã hóa, họ là những người xử lý các giao dịch trên mạng Ethereum. MetaMask không thu lợi nhuận từ phí gas.", "gas_fluctuation": "Phí gas được ước tính và sẽ dao động dựa trên lưu lượng mạng và độ phức tạp của giao dịch.", "gas_learn_more": "Tìm hiểu thêm về phí gas" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "Đăng nhập bằng", "spender": "Người chi tiêu", "now": "Ngay bây giờ", - "switching_to": "Switching to", + "switching_to": "Chuyển sang", "bridge_estimated_time": "Thời gian ước tính", "pay_with": "Thanh toán bằng", - "receive_as": "Receive", + "receive_as": "Nhận", "total": "Tổng cộng", - "you_receive": "You'll receive", + "you_receive": "Bạn sẽ nhận được", "transaction_fee": "Phí giao dịch", - "transaction_fees": "Transaction fees", + "transaction_fees": "Phí giao dịch", "metamask_fee": "Phí MetaMask", "network_fee": "Phí mạng", "bridge_fee": "Phí nhà cung cấp cầu nối" @@ -6234,7 +6264,7 @@ "transaction_fee": "Chúng tôi sẽ hoán đổi token của bạn sang USDC.e trên Polygon, mạng được thị trường Dự đoán sử dụng. Các nhà cung cấp dịch vụ hoán đổi có thể tính phí, nhưng MetaMask thì không." }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMask sẽ hoán đổi sang token mà bạn mong muốn. Không áp dụng phí MetaMask khi bạn hoán đổi sang MUSD." }, "musd_conversion": { "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." @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "Trang web này đang yêu cầu chữ ký của bạn", "transaction_tooltip": "Trang web này đang yêu cầu giao dịch của bạn", "details": "Chi tiết", - "qr_get_sign": "Get signature", + "qr_get_sign": "Lấy chữ ký", "qr_scan_text": "Quét bằng ví cứng của bạn", "sign_with_ledger": "Ký bằng Ledger", "smart_account": "Tài khoản thông minh", "smart_contract": "Hợp đồng thông minh", - "standard_account": "Standard account", + "standard_account": "Tài khoản tiêu chuẩn", "siwe_message": { "url": "URL", "network": "Mạng", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "Tài khoản thông minh", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "Tài khoản tiêu chuẩn", "switch": "Chuyển", "switchBack": "Chuyển trở lại", "includes_transaction": "Bao gồm {{transactionCount}} giao dịch", @@ -6307,9 +6337,9 @@ "cancel": "Hủy", "description": "Nhập số tiền mà bạn cảm thấy thoải mái khi được chi tiêu thay mặt cho bạn.", "invalid_number_error": "Hạn mức chi tiêu phải là một con số", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "Hạn mức chi tiêu không được để trống", + "no_extra_decimals_error": "Hạn mức chi tiêu không được có nhiều chữ số thập phân hơn token", + "no_zero_error": "Hạn mức chi tiêu không được bằng 0", "no_zero_error_decrease_allowance": "Hạn mức chi tiêu bằng 0 không có hiệu lực đối với phương thức 'decreaseAllowance'", "no_zero_error_increase_allowance": "Hạn mức chi tiêu bằng 0 không có hiệu lực đối với phương thức \"increaseAllowance\"", "save": "Lưu", @@ -6336,7 +6366,7 @@ "transferRequest": "Yêu cầu chuyển tiền", "nested_transaction_heading": "Giao dịch {{index}}", "transaction": "Bảo vệ", - "available_balance": "Available balance: ", + "available_balance": "Số dư khả dụng: ", "edit_amount_done": "Tiếp tục", "deposit_edit_amount_done": "Nạp tiền", "deposit_edit_amount_predict_withdraw": "Rút tiền", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "Điều khoản và điều kiện", "select_token": "Chọn token", "no_tokens_found": "Không tìm thấy token", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "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", "all_networks": "Tất cả mạng", "num_networks": "{{numNetworks}} mạng", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "Bỏ chọn tất cả", "see_all": "Xem tất cả", "all": "Tất cả", - "more_networks": "+{{count}} more", + "more_networks": "+{{count}} khác", "apply": "Áp dụng", "slippage": "Trượt giá", "slippage_info": "Nếu giá thay đổi giữa thời điểm bạn đặt lệnh và xác nhận, điều đó gọi là “trượt giá”. Giao dịch hoán đổi của bạn sẽ tự động bị hủy nếu mức trượt giá vượt quá mức sai lệch bạn đã thiết lập ở đây.", @@ -6392,7 +6422,7 @@ "quote_info_title": "Tỷ lệ", "network_fee_info_title": "Phí mạng", "network_fee_info_content": "Phí mạng phụ thuộc vào mức độ tắc nghẽn của mạng và độ phức tạp của giao dịch.", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "Phí mạng này do MetaMask thanh toán, vì vậy bạn có thể giao dịch mà không cần {{nativeToken}} trong tài khoản.", "points": "Điểm ước tính", "points_tooltip": "Điểm", "points_tooltip_content_1": "Điểm là cách bạn nhận được Phần thưởng MetaMask khi hoàn thành các giao dịch như hoán đổi, cầu nối hoặc giao dịch hợp đồng vĩnh cửu.", @@ -6406,7 +6436,7 @@ "select_recipient": "Chọn người nhận", "external_account": "Tài khoản bên ngoài", "error_banner_description": "Lộ trình giao dịch này hiện không khả dụng. Hãy thử thay đổi số lượng, mạng hoặc token và chúng tôi sẽ tìm phương án tốt nhất.", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "Tuyến giao dịch này hiện không khả dụng. Hãy thử thay đổi số lượng, mạng hoặc token và chúng tôi sẽ tìm phương án tốt nhất.\n\nLưu ý, nếu bạn đang cố giao dịch Ondo Tokenised Stocks, bạn có thể bị hạn chế theo khu vực, ví dụ như tại Mỹ, Liên minh Châu Âu, Vương quốc Anh và Brazil.", "insufficient_funds": "Không đủ tiền", "insufficient_gas": "Không đủ gas", "select_amount": "Chọn số lượng", @@ -6417,9 +6447,9 @@ "title": "Cầu nối", "submitting_transaction": "Đang gửi", "fetching_quote": "Tìm nạp báo giá", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "Bao gồm {{feePercentage}}% phí của MetaMask.", "no_mm_fee": "Không có phí MM", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "Không mất phí MetaMask khi hoán đổi sang {{destTokenSymbol}}.", "hardware_wallet_not_supported": "Ví cứng hiện chưa được hỗ trợ. Hãy sử dụng ví nóng để tiếp tục.", "hardware_wallet_not_supported_solana": "Ví cứng hiện chưa được hỗ trợ cho Solana. Hãy sử dụng ví nóng để tiếp tục.", "price_impact_info_title": "Mức tác động giá", @@ -6432,17 +6462,24 @@ "approval_needed": "Phê duyệt token để hoán đổi.", "approval_tooltip_title": "Cấp quyền truy cập chính xác", "approval_tooltip_content": "Bạn đang cho phép truy cập đúng số lượng đã chỉ định, {{amount}} {{symbol}}. Hợp đồng sẽ không truy cập thêm bất kỳ khoản tiền nào khác.", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "Số lượng tối thiểu nhận được", + "minimum_received_tooltip_title": "Số lượng tối thiểu nhận được", "minimum_received_tooltip_content": "Số lượng tối thiểu bạn sẽ nhận được nếu giá thay đổi trong quá trình xử lý giao dịch, dựa trên mức trượt giá bạn cho phép. Đây là con số ước tính từ các nhà cung cấp thanh khoản. Số lượng cuối cùng có thể khác.", + "market_closed": { + "title": "Thị trường đã đóng cửa", + "description": "Thị trường hỗ trợ token này hiện đang đóng cửa. Token vẫn có thể được chuyển trên chuỗi bất cứ lúc nào.", + "learn_more": "Tìm hiểu thêm", + "learn_more_url": "https://status.ondo.finance/market", + "done": "Hoàn tất" + }, "submit": "Gửi", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "Giao dịch của bạn sẽ không được thực hiện nếu giá thay đổi vượt quá phần trăm trượt giá.", "cancel": "Hủy", "confirm": "Xác nhận", "exceeding_upper_slippage_warning": "Độ trượt giá cao, điều này có thể dẫn đến giao dịch hoán đổi không thuận lợi", "exceeding_lower_slippage_warning": "Độ trượt giá thấp, điều này có thể dẫn đến giao dịch hoán đổi không thuận lợi", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "Nhập giá trị lớn hơn {{value}}%", + "exceeding_upper_slippage_error": "Bạn không thể nhập giá trị lớn hơn {{value}}%", "custom": "Tùy chỉnh" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "Khôi phục ví", "login_with_social": "Đăng nhập bằng tài khoản mạng xã hội", "setup": "Thiết lập", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "Cụm từ khôi phục bí mật {{num}}", "back_up": "Sao lưu", "reveal": "Hiển thị", "social_recovery_title": "KHÔI PHỤC {{authConnection}}", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "Nhập mật khẩu", "description": "Nhập mật khẩu ví của bạn để xem thông tin thẻ.", + "description_unfreeze": "Nhập mật khẩu ví của bạn để tiếp tục chi tiêu bằng thẻ.", "placeholder": "Mật khẩu", "confirm": "Xác nhận", "cancel": "Hủy", @@ -7001,6 +7039,7 @@ "enable_card_error": "Không thể kích hoạt thẻ. Vui lòng thử lại sau.", "view_card_details_error": "Không thể tải thông tin thẻ. Vui lòng thử lại.", "biometric_verification_required": "Yêu cầu xác thực để xem thông tin thẻ.", + "unfreeze_auth_required": "Yêu cầu xác thực để tiếp tục chi tiêu bằng thẻ của bạn.", "warnings": { "close_spending_limit": { "title": "Bạn sắp đạt đến hạn mức chi tiêu", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "Thẻ của bạn bị đóng băng", - "description": "Vui lòng liên hệ bộ phận hỗ trợ để hủy đóng băng thẻ của bạn" + "description": "Thẻ của bạn tạm thời bị khóa. Bạn có thể mở khóa bất cứ lúc nào." }, "blocked": { "title": "Thẻ của bạn bị chặn", @@ -7068,7 +7107,14 @@ "travel_description": "Đặt phòng khách sạn với mức giảm giá lên đến 70%", "card_tos_title": "Điều khoản và điều kiện", "order_metal_card": "Thẻ Metal", - "order_metal_card_description": "Đặt hàng Thẻ Metal vật lý ngay" + "order_metal_card_description": "Đặt hàng Thẻ Metal vật lý ngay", + "freeze_card": "Khóa thẻ", + "unfreeze_card": "Mở khóa thẻ", + "freeze_card_description": "Tạm dừng mọi chi tiêu trên thẻ của bạn", + "unfreeze_card_description": "Tiếp tục mọi chi tiêu trên thẻ của bạn", + "freeze_error": "Không thể cập nhật trạng thái thẻ. Vui lòng thử lại.", + "freeze_success": "Đã khóa thẻ thành công", + "unfreeze_success": "Đã mở khóa thẻ thành công" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "Có thể gửi lại sau {{seconds}} giây" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "Thêm vào {{walletName}}", + "adding_to_wallet": "Đang thêm vào {{walletName}}...", + "continue_setup": "Tiếp tục thiết lập {{walletName}}", + "wallet_not_available": "{{walletName}} không khả dụng", + "already_in_wallet": "Đã có trong {{walletName}}", + "success_title": "Đã thêm thẻ!", + "success_message": "Thẻ MetaMask của bạn đã được thêm vào {{walletName}}.", + "error_title": "Không thể thêm thẻ", + "error_wallet_not_available": "{{walletName}} không khả dụng trên thiết bị này. Vui lòng đảm bảo bạn đã thiết lập {{walletName}}.", + "error_wallet_not_initialized": "{{walletName}} chưa được khởi tạo. Vui lòng thiết lập ví của bạn và thử lại.", "error_card_already_in_wallet": "Thẻ này đã được thêm vào {{walletName}}.", "error_card_pending": "Thẻ của bạn đang được thiết lập trong {{walletName}}. Vui lòng quay lại sau vài phút.", "error_card_suspended": "Thẻ của bạn trong {{walletName}} đã bị tạm ngưng. Vui lòng liên hệ bộ phận hỗ trợ để được trợ giúp.", "error_card_not_eligible": "Thẻ này không đủ điều kiện để thêm vào ví di động.", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "Không thể mã hóa dữ liệu thẻ. Vui lòng thử lại.", "error_invalid_card_data": "Dữ liệu thẻ không hợp lệ. Vui lòng kiểm tra lại thông tin thẻ và thử lại.", "error_card_not_found": "Không tìm thấy thẻ. Vui lòng thử lại.", "error_card_provider_not_found": "Nhà cung cấp thẻ không khả dụng tại khu vực của bạn.", "error_card_id_mismatch": "Xác minh thẻ thất bại. Vui lòng thử lại.", "error_card_not_active": "Thẻ của bạn chưa được kích hoạt. Vui lòng kích hoạt thẻ trước.", "error_network": "Đã xảy ra lỗi mạng. Vui lòng kiểm tra kết nối và thử lại.", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "Yêu cầu đã hết thời gian chờ. Vui lòng thử lại.", + "error_server": "Đã xảy ra lỗi máy chủ. Vui lòng thử lại sau.", + "error_unknown": "Đã xảy ra lỗi không mong muốn. Vui lòng thử lại hoặc liên hệ bộ phận hỗ trợ.", + "error_platform_not_supported": "Nền tảng này không hỗ trợ thêm ví di động.", "try_again": "Thử lại", "cancel": "Hủy" } @@ -7299,7 +7345,7 @@ "main_title": "Phần thưởng", "referral_title": "Giới thiệu", "tab_overview_title": "Tổng quan", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "Ảnh chụp nhanh", "tab_activity_title": "Hoạt động", "referral_stats_earned_from_referrals": "Nhận được từ giới thiệu", "referral_stats_referrals": "Giới thiệu", @@ -7353,7 +7399,7 @@ "verifying_rewards": "Chúng tôi đang kiểm tra mọi thứ trước khi bạn nhận phần thưởng." }, "season_status": { - "points_earned": "Points earned" + "points_earned": "Điểm đã tích lũy" }, "onboarding": { "not_supported_region_title": "Khu vực không được hỗ trợ", @@ -7431,7 +7477,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", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "Thêm tất cả tài khoản" }, "referred_by_code": { "title": "Mã giới thiệu", @@ -7514,7 +7560,7 @@ "claim_label": "Nhận", "claimed_label": "Đã nhận", "reward_claimed": "Phần thưởng đã nhận", - "time_left": "{{time}} left", + "time_left": "Còn {{time}}", "expired": "Đã hết hạn" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "Đổi không thành công", "redeem_failure_description": "Vui lòng thử lại sau.", "reward_details": "Chi tiết phần thưởng", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "Chọn tài khoản mà bạn muốn nhận phần thưởng này." }, "animation": { "could_not_load": "Không thể tải" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", + "starts_date": "Bắt đầu {{date}}", + "ends_date": "Kết thúc {{date}}", + "results_coming_soon": "Kết quả sẽ sớm được công bố", + "tokens_on_the_way": "Token đang được gửi đến", + "pill_up_next": "Sắp tới", + "pill_live_now": "Đang diễn ra", "pill_calculating": "Đang tính toán", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "pill_results_ready": "Kết quả đã sẵn sàng", + "pill_complete": "Hoàn tất" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "Ảnh chụp nhanh", + "error_title": "Không thể tải ảnh chụp nhanh", + "error_description": "Chúng tôi không thể tải ảnh chụp nhanh. Vui lòng thử lại.", "retry_button": "Thử lại" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "Đang hoạt động", + "upcoming_title": "Sắp tới", + "previous_title": "Trước đó", + "empty_state": "Không có ảnh chụp nhanh nào", + "error_title": "Không thể tải ảnh chụp nhanh", + "error_description": "Chúng tôi không thể tải ảnh chụp nhanh. Vui lòng thử lại.", "retry_button": "Thử lại", - "refreshing": "Refreshing..." + "refreshing": "Đang làm mới..." } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "Phê duyệt {{approveSymbol}}", "bridge_approval_loading": "Phê duyệt", "bridge_send": "Cầu nối {{sourceSymbol}} từ {{sourceChain}}", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "Gửi qua cầu nối", "bridge_receive": "Nhận {{targetSymbol}} trên {{targetChain}}", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "Nhận qua cầu nối", "default": "Giao dịch", "musd_convert_send": "Đã gửi {{sourceSymbol}} từ {{sourceChain}}", "musd_claim": "Nhận mUSD", @@ -7607,20 +7653,20 @@ "description": "Đang thiết lập kết nối với {{dappName}}..." }, "show_error": { - "title": "Connection error", + "title": "Lỗi kết nối", "description": "Không thể thiết lập kết nối. Vui lòng thử lại." }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "Phê duyệt bị từ chối", + "description": "Người dùng đã từ chối yêu cầu." }, "show_return_to_app": { "title": "Thành công", "description": "Quay lại ứng dụng để tiếp tục." }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "Không tìm thấy kết nối", + "description": "Vui lòng thiết lập kết nối mới từ ứng dụng để tiếp tục." } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "Khám phá", + "trending_tokens": "Token xu hướng", "price_change": "Biến động giá", "all_networks": "Tất cả mạng", - "24h": "24h", + "24h": "24 giờ", "time": "Thời gian", "24_hours": "24 giờ", "6_hours": "6 giờ", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 giờ", + "5_minutes": "5 phút", "networks": "Mạng", "sort_by": "Sắp xếp theo", "volume": "Khối lượng", @@ -7650,32 +7696,48 @@ "high_to_low": "Cao đến thấp", "low_to_high": "Thấp đến cao", "apply": "Áp dụng", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "Tìm kiếm token, trang web, URL", "cancel": "Hủy", "perps": "Vĩnh cửu", "predictions": "Dự đoán", - "no_results": "No results found", + "no_results": "Không tìm thấy kết quả", "sites": "Trang web", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "Trang web phổ biến", + "search_sites": "Tìm kiếm trang web", + "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.", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "Token xu hướng không khả dụng", + "description": "Chúng tôi không thể tìm nạp trang này ngay lúc này", "try_again": "Thử lại" }, "empty_search_result_state": { "title": "Không tìm thấy token", - "description": "We were not able to find this token" + "description": "Chúng tôi không thể tìm thấy token này" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "Sẵn sàng cập nhật", + "description_ios": "Chúng tôi đã thực hiện một số sửa lỗi quan trọng. Tải lại để sử dụng phiên bản MetaMask mới nhất.", + "description_android": "Chúng tôi đã thực hiện một số sửa lỗi quan trọng. Đóng và mở lại MetaMask để áp dụng bản cập nhật.", "primary_action_reload": "Tải lại", "primary_action_acknowledge": "Tôi đã hiểu" + }, + "homepage": { + "sections": { + "tokens": "Token", + "perpetuals": "Hợp đồng vĩnh cửu", + "predictions": "Dự đoán", + "defi": "DeFi", + "nfts": "NFT", + "import_nfts": "Nhập NFT", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } diff --git a/locales/languages/zh.json b/locales/languages/zh.json index 2a0a59917ff..f643c308a66 100644 --- a/locales/languages/zh.json +++ b/locales/languages/zh.json @@ -458,7 +458,11 @@ "reset_wallet_desc_bold": "在该设备上从", "reset_wallet_desc_2": "MetaMask 永久删除。此操作无法撤销。", "reset_wallet_desc_login": "要恢复您的钱包,您可以使用您的私钥助记词,或您的 Google 账户/ Apple 账户密码。MetaMask 没有这些信息。", - "reset_wallet_desc_srp": "要恢复您的钱包,请确保您已备份私钥助记词。MetaMask 不会存储此信息。" + "reset_wallet_desc_srp": "要恢复您的钱包,请确保您已备份私钥助记词。MetaMask 不会存储此信息。", + "biometric_authentication_cancelled": "生物特征认证已取消", + "biometric_authentication_cancelled_title": "生物特征设置失败", + "biometric_authentication_cancelled_description": "请从设置中重新设置生物特征认证。", + "biometric_authentication_cancelled_button": "确认" }, "connect_hardware": { "title_select_hardware": "连接硬件钱包", @@ -1040,7 +1044,7 @@ "title": "要存入的金额", "get_usdc_hyperliquid": "获取 USDC • Hyperliquid", "insufficient_funds": "资金不足", - "no_funds_available": "无可用资金。请先充值。", + "no_funds_available": "资金不足。请存入资金或选择其他支付方式", "enter_amount": "输入金额", "fetching_quote": "正在获取报价", "submitting": "正在提交交易", @@ -1970,8 +1974,8 @@ "trade_again": "再次交易", "activity": { "deposit_title": "保证金", - "deposited_amount": "Deposited {{amount}} {{symbol}}", - "withdrew_amount": "Withdrew {{amount}} {{symbol}}", + "deposited_amount": "已存入 {{amount}} {{symbol}}", + "withdrew_amount": "已提取 {{amount}} {{symbol}}", "status_completed": "已完成", "status_failed": "失败", "status_pending": "待定" @@ -2051,6 +2055,16 @@ "referral_code_text": "使用我的推荐码即可赚取额外奖励。" } }, + "market_insights": { + "title": "市场洞察", + "updated_ago": "更新于 {{time}}", + "disclaimer": "人工智能分析见解。不构成财务建议。", + "whats_driving_price": "推动价格的因素有哪些?", + "what_people_saying": "大家都在说什么", + "trade_button": "交易", + "sources_count": "+{{count}} 个来源", + "sources_title": "来源" + }, "predict": { "title": "MetaMask 预测", "prediction_markets": "预测市场", @@ -2384,8 +2398,8 @@ "no_available_tokens": "没有看到您的代币?", "add_tokens": "添加资产", "are_you_sure_exit": "Are you sure you want to exit?", - "search_information_not_saved": "Your search information will not be saved.", "import_token": "Would you like to import this token?", + "import_tokens": "Would you like to import these tokens?", "tokens_detected_in_account": "在此账户中找到{{tokenCount}}个新的{{tokensLabel}}", "token_toast": { "tokens_imported_title": "已导入的代币", @@ -2650,6 +2664,7 @@ "decimals_cant_be_empty": "代币小数位数不能为空。", "decimals_is_required": "Decimal is required. Find it on:", "no_tokens_found": "我们无法找到具有该名称的任何代币。", + "tokens_empty_description": "Search for any token and import it", "select_token": "选择代币", "address_must_be_smart_contract": "检测到个人地址。输入代币合约地址。", "billion_abbreviation": "十亿", @@ -2752,6 +2767,9 @@ "disconnect_all_accounts": "断开所有账户的连接", "deceptive_site_ahead": "前方为欺骗性网站", "deceptive_site_desc": "您尝试访问的网站并不安全。攻击者可能会诱使您做一些危险的事情", + "malicious_site_detected": "检测到恶意网站", + "malicious_site_warning": "如果您访问此网站,您可能会损失所有资产。", + "connect_anyway": "仍然连接", "learn_more": "了解详情", "advisory_by": "建议由以太坊网络钓鱼探测器和PhishFort提供", "potential_threat": "潜在威胁包括", @@ -2846,7 +2864,11 @@ "permissions": "许可", "card_title": "MetaMask 卡", "settings": "设置", - "log_out": "退出登录" + "networks": "网络", + "log_out": "退出登录", + "notifications": "通知", + "buy": "买入", + "scan": "扫描" }, "app_settings": { "enabling_notifications": "正在启用通知...", @@ -2870,6 +2892,8 @@ "state_logs": "状态记录", "add_network_title": "添加网络", "auto_lock": "自动锁定", + "enable_device_authentication": "启用设备认证", + "enable_device_authentication_desc": "使用您设备的生物识别功能或密码来解锁 MetaMask。", "auto_lock_desc": "选择应用程序自动锁定之前空闲的时间。", "state_logs_desc": "这将帮助 MetaMask 调试您可能遇到的任何问题。请通过电子邮件将其发送到 support@metamask.io", "autolock_immediately": "立即", @@ -2975,6 +2999,11 @@ "add_rpc_url": "添加 RPC(远程过程调用)URL", "add_block_explorer_url": "添加区块浏览器 URL", "networks_desc": "添加并编辑自定义 RPC 网络", + "networks_enabled": "Enabled Networks", + "networks_test_networks": "Test Networks", + "networks_additional": "Additional Networks", + "networks_search_placeholder": "搜索网络", + "networks_no_results": "未找到网络", "network_name_label": "网络名称", "network_name_placeholder": "网络名称(可选)", "network_rpc_url_label": "RPC(远程过程调用)URL", @@ -2991,7 +3020,16 @@ "network_other_networks": "其他网络", "network_rpc_networks": "RPC 网络", "network_add_network": "添加网络", + "add_chain_title": "添加网络", + "add_chain_search_placeholder": "Search by name, chain ID, or currency", + "add_chain_loading": "Loading networks…", + "add_chain_error": "Failed to load networks. Please try again.", + "add_chain_retry": "重试", + "add_chain_added": "Added", + "add_chain_or": "或", + "add_chain_custom_link": "添加自定义网络", "network_add_custom_network": "添加自定义网络", + "network_add_test_network": "Add a test network", "network_add": "添加", "network_save": "保存", "remove_network_title": "是否要删除此网络?", @@ -3283,7 +3321,7 @@ "sdk_feedback_modal": { "ok": "OK", "title": "账户无法连接", - "info": "Please scan the QR code on the site to reconnect to MetaMask" + "info": "请扫描网站上的二维码重新连接 MetaMask" }, "app_information": { "title": "信息", @@ -3379,6 +3417,7 @@ "sell_description": "卖出加密货币换取现金" }, "asset_overview": { + "market_closed": "已休市", "send_button": "发送", "buy_button": "买入", "cash_buy_button": "现金买入", @@ -3399,19 +3438,6 @@ "bridge": "桥接", "earn": "赚取", "convert_to_musd": "兑换为 mUSD", - "merkl_rewards": { - "annual_bonus": "{{apy}}% 奖励", - "claimable_bonus": "可领取奖励", - "claimable_bonus_tooltip_description": "mUSD 奖励在 Linea 上领取。", - "terms_apply": "具体条款适用。", - "ok": "确定", - "claim": "领取", - "processing_claim": "正在处理领取……", - "claim_on_linea_title": "在 Linea 上领取奖励", - "claim_on_linea_description": "您的奖励将在 Linea 网络上发放,与您在以太坊上的 mUSD 余额相互独立。", - "continue": "继续", - "unexpected_error": "发生意外错误。请重试。" - }, "tron": { "daily_resource_new_energy": "每日新能量", "sufficient_to_cover": "足以覆盖", @@ -3530,7 +3556,7 @@ "address_copied_to_clipboard": "代币地址已复制到剪贴板" }, "qr_scanner": { - "invalid_qr_code_title": "Invalid QR code", + "invalid_qr_code_title": "无效二维码", "invalid_qr_code_message": "您尝试扫描的二维码无效。", "allow_camera_dialog_title": "允许访问摄像头", "allow_camera_dialog_message": "我们需要您的许可才能扫描二维码", @@ -3543,7 +3569,7 @@ "attempting_sync_from_wallet_error": "好像您正尝试与扩展程序同步。要进行同步,请转至“设置”>“高级”>“与 MetaMask 扩展程序同步”", "not_allowed_error_title": "开启摄像头访问功能", "not_allowed_error_desc": "要扫描二维码,您需要从设备的设置菜单中授予 MetaMask 摄像头访问权限。", - "unrecognized_address_qr_code_title": "Unrecognized QR code", + "unrecognized_address_qr_code_title": "二维码无法识别", "unrecognized_address_qr_code_desc": "对不起,此二维码没有关联的账户地址或合约地址。", "url_redirection_alert_title": "您即将访问外部链接", "url_redirection_alert_desc": "链接可能会被用于诈骗或网络钓鱼,因此请确保您只访问您信任的网站。", @@ -3622,7 +3648,7 @@ "invalid_collectible_ownership": "您并不是此收藏品的所有者", "known_asset_contract": "已知资产合约地址", "max": "最大", - "recipient_address": "Recipient address", + "recipient_address": "接收人地址", "required": "必需", "to": "至", "total": "总计", @@ -3641,7 +3667,7 @@ "nevermind": "没关系", "edit_network_fee": "编辑网络费", "edit_priority": "编辑优先级", - "gas_cancel_fee": "Gas cancellation fee", + "gas_cancel_fee": "燃料取消费", "gas_speedup_fee": "燃料加速费", "use_max": "使用最大值", "set_gas": "设置", @@ -3650,7 +3676,7 @@ "transaction_fee": "网络费", "transaction_fee_less": "不收费", "total_amount": "总数额", - "view_data": "View data", + "view_data": "查看数据", "adjust_transaction_fee": "调整交易费用", "could_not_resolve_ens": "无法解析 ENS", "asset": "资产", @@ -3795,7 +3821,7 @@ "no_tabs_desc": "要浏览去中心化网络,请添加新标签页", "got_it": "知道了", "max_tabs_title": "达到最大标签页数量限制", - "max_tabs_desc": "我们目前仅支持同时打开 5 个标签页。请在添加新标签页前关闭现有标签页。", + "max_tabs_desc": "我们目前仅支持同时打开 20 个标签页。请在添加新标签页前关闭现有标签页。", "failed_to_resolve_ens_name": "我们无法解析该 ENS 名称", "remove_bookmark_title": "删除收藏", "remove_bookmark_msg": "是否确实想要从您的收藏夹中删除此站点?", @@ -3828,7 +3854,7 @@ "cancel_button": "取消" }, "approval": { - "title": "Confirm transaction" + "title": "确认交易" }, "approve": { "title": "批准", @@ -3839,39 +3865,39 @@ "unavailable": "不可用", "tx_review_confirm": "确认", "tx_review_transfer": "转账", - "tx_review_contract_deployment": "Contract deployment", - "tx_review_transfer_from": "Transfer from", - "tx_review_unknown": "Unknown method", + "tx_review_contract_deployment": "合约部署", + "tx_review_transfer_from": "转移自", + "tx_review_unknown": "未知方法", "tx_review_approve": "批准", - "tx_review_increase_allowance": "Increase allowance", - "tx_review_set_approval_for_all": "Set approval for all", - "tx_review_staking_claim": "Staking claim", + "tx_review_increase_allowance": "增加限额", + "tx_review_set_approval_for_all": "设置批准所有", + "tx_review_staking_claim": "质押申请", "tx_review_staking_deposit": "质押存款", "tx_review_staking_unstake": "解除质押", "tx_review_lending_deposit": "出借存款", - "tx_review_lending_withdraw": "Lending withdrawal", + "tx_review_lending_withdraw": "出借提款", "tx_review_perps_deposit": "已注资的永续合约", "tx_review_predict_deposit": "带资金押注的预测", "tx_review_predict_claim": "已领取的获胜投注", "tx_review_predict_withdraw": "预测提现", "tx_review_musd_conversion": "mUSD 兑换", "claim": "领取", - "sent_ether": "Sent ETH", - "self_sent_ether": "Sent yourself ETH", - "received_ether": "Received ETH", + "sent_ether": "已发送 ETH", + "self_sent_ether": "已向自己发送 ETH", + "received_ether": "已接收 ETH", "sent_dai": "已发送 DAI", "self_sent_dai": "已向自己发送 DAI", "received_dai": "已接收 DAI", - "sent_tokens": "Sent tokens", - "received_tokens": "Received tokens", + "sent_tokens": "已发送代币", + "received_tokens": "已接收代币", "ether": "ETH", "sent_unit": "已发送 {{unit}} 个", - "self_sent_unit": "Sent yourself {{unit}}", + "self_sent_unit": "已向自己发送 {{unit}}", "received_unit": "已接收 {{unit}} 个", "sent_collectible": "已发送收藏品", "received_collectible": "已收到的收藏品", - "send_ether": "Send ETH", - "send_unit": "Send {{unit}}", + "send_ether": "发送 ETH", + "send_unit": "发送 {{unit}}", "send_collectible": "发送收藏品", "receive_collectible": "接收收藏品", "sent": "已发送", @@ -3881,17 +3907,17 @@ "send": "发送", "redeposit": "再次存款", "interaction": "交互", - "contract_deploy": "Contract deployment", - "to_contract": "New contract", - "mint": "Mint", + "contract_deploy": "合约部署", + "to_contract": "新合约", + "mint": "铸币", "tx_details_free": "免费", "tx_details_not_available": "不可用", "smart_contract_interaction": "智能合约交互", "swaps_transaction": "兑换交易", "bridge_transaction": "桥接", "approve": "批准", - "increase_allowance": "Increase allowance", - "set_approval_for_all": "Set approval for all", + "increase_allowance": "增加限额", + "set_approval_for_all": "设置批准所有", "hash": "哈希", "from": "自", "to": "至", @@ -3899,15 +3925,15 @@ "amount": "数额", "fee": { "transaction_fee_in_ether": "交易费用", - "transaction_fee_in_usd": "Transaction fee (USD)" + "transaction_fee_in_usd": "交易费用(USD)" }, - "gas_used": "Gas used (units)", - "gas_limit": "Gas limit (units)", - "gas_price": "Gas price (GWEI)", - "base_fee": "Base fee (GWEI)", - "priority_fee": "Priority fee (GWEI)", + "gas_used": "已使用燃料(单位)", + "gas_limit": "燃料限制(单位)", + "gas_price": "燃料价格(GWEI)", + "base_fee": "基础费用(GWEI)", + "priority_fee": "优先费用(GWEI)", "multichain_priority_fee": "优先费用", - "max_fee": "Max fee per gas", + "max_fee": "每单位燃料的最大费用", "total": "总计", "view_on": "在以下位置查看:", "view_on_etherscan": "在 Etherscan 上查看", @@ -3923,13 +3949,13 @@ "nonce": "nonce", "from_device_label": "从这台设备上", "import_wallet_row": "账户已添加到该设备", - "import_wallet_label": "Account added", + "import_wallet_label": "已添加账户", "import_wallet_tip": "以后所有从这台设备进行的交易都会在时间戳旁边加上 “来自这台设备”的标签。对于添加账户之前的交易,该历史记录不会显示哪些传出交易来自该设备。", "sign_title_scan": "扫描 ", "sign_title_device": "使用您的硬件钱包", "sign_description_1": "您使用硬件钱包签名后,", - "sign_description_2": "Tap on get signature", - "sign_get_signature": "Get signature", + "sign_description_2": "点击‘’获取签名‘’", + "sign_get_signature": "获取签名", "transaction_id": "交易 ID", "network": "网络", "request_from": "请求来自", @@ -4032,7 +4058,7 @@ "title": "网络", "other_networks": "其他网络", "close": "关闭", - "status_ok": "All systems operational", + "status_ok": "所有系统运转正常", "status_not_ok": "网络遇到一些问题", "want_to_add_network": "想要添加此网络吗?", "add_custom_network": "添加自定义网络", @@ -4051,7 +4077,7 @@ "review": "查看", "view_details": "查看详情", "network_details": "网络详情", - "network_select_confirm_use_safe_check": "Selecting confirm turns on network details check. You can turn off network details check in ", + "network_select_confirm_use_safe_check": "选择“确认”即开启网络详情检查。您可以在下方关闭网络详情检查:", "network_settings_security_privacy": "“设置 > 安全和隐私”中", "network_currency_symbol": "货币符号", "network_block_explorer_url": "区块浏览器 URL", @@ -4066,7 +4092,7 @@ "malicious_network_warning": "恶意网络提供商可能会谎报区块链的状态,并记录您的网络活动。仅添加您信任的自定义网络。", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "网络信息", - "additional_network_information_title": "Additional networks information", + "additional_network_information_title": "其他网络信息", "network_warning_desc": "此网络连接依赖于第三方。这种连接可靠程度可能会较低,或会使第三方能够跟踪活动。", "additonial_network_information_desc": "这些网络中的其中一些依赖于第三方。此连接可能不太可靠,或使第三方可进行活动跟踪。", "connect_more_networks": "连接更多网络", @@ -4096,7 +4122,7 @@ "network_deprecated_title": "该网络已弃用", "network_deprecated_description": "您尝试连接的网络在 MetaMask 上不再受支持。", "edit_networks_title": "编辑网络", - "no_network_fee": "No network fee" + "no_network_fee": "无网络费" }, "permissions": { "title_this_site_wants_to": "此网站想要:", @@ -4111,11 +4137,11 @@ "network_connected": "网络已连接 ", "see_your_accounts": "查看您的账户并建议交易", "connected_to": "已连接到 ", - "manage_permissions": "Manage permissions", + "manage_permissions": "管理许可", "edit": "编辑", "cancel": "取消", "got_it": "知道了", - "connection_details_title": "Connection details", + "connection_details_title": "连接详情", "connection_details_description": "您于 {{connectionDateTime}} 使用 MetaMask 浏览器连接到此网站", "title_add_network_permission": "添加网络许可", "add_this_network": "添加此网络", @@ -4181,22 +4207,18 @@ "enable_device_passcode_android": "使用设备 PIN 登录?" }, "authentication": { - "auth_prompt_title": "需要身份验证", - "auth_prompt_desc": "请进行身份验证以便使用 MetaMask", - "fingerprint_prompt_title": "需要身份验证", - "fingerprint_prompt_desc": "使用您的指纹解锁 MetaMask", - "fingerprint_prompt_cancel": "取消" + "auth_prompt_desc": "请进行身份验证以便使用 MetaMask" }, "accountApproval": { "title": "连接请求", "walletconnect_title": "WalletConnect 请求", "action": "连接此站点?", - "action_reconnect": "To resume connection, choose the number you see on the site", - "action_reconnect_deeplink": "Do you want to reconnect to this site?", + "action_reconnect": "要恢复连接,请选择网站上显示的数字", + "action_reconnect_deeplink": "是否要重新连接到此网站?", "connect": "连接", "resume": "恢复", "cancel": "取消", - "donot_rememberme": "Do not remember this site connection", + "donot_rememberme": "不记住此网站连接", "disconnect": "断开连接", "permission": "查看您的", "address": "公钥", @@ -4218,7 +4240,7 @@ "error_title": "出错了......", "error_message": "我们无法导入该私钥。请确保输入正确。", "error_empty_message": "您需要输入您的私钥。", - "or_scan_a_qr_code": "or scan a QR code" + "or_scan_a_qr_code": "或扫描二维码" }, "import_private_key_success": { "title": "账户导入成功!", @@ -4229,18 +4251,18 @@ "import_wallet_title": "导入钱包", "enter_srp_subtitle": "输入您的私钥助记词", "textarea_placeholder": "在每个单词之间加空格,并确保无人窥视", - "description": "Enter your wallet's Secret Recovery Phrase. You can import any Ethereum, Solana, or Bitcoin Secret Recovery Phrase.", - "subtitle": "Paste your Secret Recovery Phrase", + "description": "输入您钱包的私钥助记词。您可以导入任何以太坊、Solana 或比特币私钥助记词。", + "subtitle": "粘贴您的私钥助记词", "cta_text": "继续", "paste": "粘贴", "clear": "全部清除", "srp_number_of_words_option_title": "单词数量", - "12_word_option": "I have a 12-word phrase", - "24_word_option": "I have a 24-word phrase", + "12_word_option": "我有 12 个单词组成的助记词", + "24_word_option": "我有 24 个单词组成的助记词", "error_title": "出错了......", - "error_message": "We couldn't import that Secret Recovery Phrase. Please make sure you entered it correctly.", - "error_empty_message": "You need to enter your Secret Recovery Phrase.", - "error_number_of_words_error_message": "Secret Recovery Phrases contain 12 or 24 words", + "error_message": "我们无法导入此私钥助记词。请确保您输入正确。", + "error_empty_message": "您需要输入您的私钥助记词。", + "error_number_of_words_error_message": "私钥助记词包含 12 或 24 个单词", "error_srp_is_case_sensitive": "输入无效!私钥助记词需区分大小写。", "error_srp_word_error_1": "单词 ", "error_srp_word_error_2": " 不正确或拼写错误。", @@ -4249,9 +4271,9 @@ "error_multiple_srp_word_error_3": " 不正确或拼写错误。", "error_invalid_srp": "私钥助记词无效", "error_duplicate_srp": "此私钥助记词已导入。", - "error_duplicate_account": "The account you are trying to import is a duplicate.", - "invalid_qr_code_title": "Invalid QR code", - "invalid_qr_code_message": "The QR code does not contain a valid Secret Recovery Phrase", + "error_duplicate_account": "您尝试导入的账户是重复的。", + "invalid_qr_code_title": "无效二维码", + "invalid_qr_code_message": "此二维码不包含有效的私钥助记词", "success_1": "钱包", "success_2": "已导入" }, @@ -4665,7 +4687,7 @@ "button": "保护钱包" }, "transaction_update_retry_modal": { - "title": "Transaction update failed", + "title": "交易更新失败", "text": "您想再试一次吗?", "cancel_button": "取消", "retry_button": "重试" @@ -4684,13 +4706,13 @@ "next": "下一步", "amount_placeholder": "0.00", "link_copied": "链接已复制到剪贴板", - "send_link_title": "Send link", + "send_link_title": "发送链接", "description_1": "您的请求链接已经可以发送!", "description_2": "将此链接发送给朋友,然后它将要求他们发送", "copy_to_clipboard": "复制到剪贴板", "qr_code": "二维码", - "send_link": "Send link", - "request_qr_code": "Payment request QR code", + "send_link": "发送链接", + "request_qr_code": "付款请求二维码", "balance": "余额" }, "receive_request": { @@ -4725,10 +4747,10 @@ }, "walletconnect_sessions": { "no_active_sessions": "您没有活动会话", - "end_session_title": "End session", + "end_session_title": "结束会话", "end": "结束", "cancel": "取消", - "session_ended_title": "Session ended", + "session_ended_title": "会话已结束", "session_ended_desc": "已终止选定的会话", "session_already_exist": "此会话已连接。", "close_current_session": "在开始新会话之前,请先关闭当前会话。" @@ -4765,15 +4787,14 @@ "on_network": "在 {{networkName}} 上", "debit_card": "借记卡", "select_payment_method": "选择付款方式", - "loading_quote": "Loading quote...", "pay_with": "支付方式:", - "buying_via": "Buying via {{providerName}}.", - "change_provider": "Change provider.", + "buying_via": "正在通过 {{providerName}} 购买。", + "change_provider": "更换提供商。", "payment_error": "出错了。请重试。", - "no_payment_methods_available": "No payment methods are available.", + "no_payment_methods_available": "无可用支付方式。", "error_fetching_quotes": "出错了。请重试。", "no_quotes_available": "没有可用的提供商。", - "providers": "Providers", + "providers": "提供商", "continue": "继续", "powered_by_provider": "由 {{provider}} 提供技术支持", "purchased_currency": "已购买 {{currency}}", @@ -4871,6 +4892,15 @@ "log_out": "退出 {{provider}}", "logged_out_success": "成功退出登录", "logged_out_error": "退出登录时出错" + }, + "token_unavailable_modal": { + "title": "Not available", + "description": "{{token}} is not available with {{provider}} in your region.", + "change_token": "Change token", + "change_provider": "Change provider" + }, + "provider_picker_modal": { + "title": "Choose a provider" } }, "fiat_on_ramp_aggregator": { @@ -4925,7 +4955,7 @@ "lowest_sell_limit": "最低卖出限制", "medium_sell_limit": "中等卖出限制", "highest_sell_limit": "最高卖出限制", - "change": "Change", + "change": "更换", "continue_to_amount": "继续前往金额", "no_payment_methods_title": "{{regionName}} 暂无可用支付方式", "no_cash_destinations_title": "{{regionName}} 暂无现金目的地", @@ -5118,7 +5148,7 @@ "start_swapping": "开始兑换" }, "feature_off_title": "暂时无法使用", - "feature_off_body": "MetaMask Swaps are undergoing maintenance. Please check back later.", + "feature_off_body": "MetaMask Swaps 正在进行维护。请稍后再查看。", "wrong_network_title": "兑换不可用", "wrong_network_body": "您只能在以太坊主网上兑换代币。", "unallowed_asset_title": "不能兑换此代币", @@ -5160,7 +5190,7 @@ "not_enough": "没有足够的 {{symbol}} 以完成这次兑换", "max_slippage": "最大滑点", "max_slippage_amount": "最大滑点 {{slippage}}", - "slippage_info": "If the rate changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “max slippage” setting.", + "slippage_info": "如果汇率在您下单和确认之间发生变化,这被称为 “滑移”。如果滑移超过您的 “最大滑移”设置,您的兑换将自动取消。", "slippage_warning": "确保您知道您在做什么!", "allows_up_to_decimals": "{{symbol}} 允许多最多 {{decimals}} 个小数", "get_quotes": "获取报价", @@ -5199,7 +5229,7 @@ "edit": "编辑", "quotes_include_fee": "报价包括 %{{fee}} 的 MetaMask 费用", "quotes_include_gas_and_metamask_fee": "报价包括燃料费和 {{fee}}% 的 MetaMask 费用", - "tap_to_swap": "Tap to swap", + "tap_to_swap": "点按以兑换", "swipe_to_swap": "滑动以兑换", "swipe_to": "滑动以", "swap": "兑换", @@ -5259,7 +5289,7 @@ "approve": "批准{{sourceToken}}进行交换:最多可以达到{{upTo}}" }, "notification_label": { - "swap_pending": "Pending swap ({{sourceToken}} to {{destinationToken}})", + "swap_pending": "待定兑换({{sourceToken}} 至 {{destinationToken}})", "swap_confirmed": "交换完成({{sourceToken}}至{{destinationToken}})。", "approve_pending": "正在批准{{sourceToken}}用于交换", "approve_confirmed": "{{sourceToken}}已获准进行兑换" @@ -5334,7 +5364,7 @@ "descriptions": { "description_1": "网络下拉菜单已移至您的资产", "description_2": "一站式完成兑换与桥接", - "description_3": "Streamlined send experience", + "description_3": "更简化的发送体验", "description_4": "全新账户视图" }, "more_information": "现在您可以专注于您的代币和活动,而无需担心其背后的网络。", @@ -5406,21 +5436,21 @@ "aggressive_label": "激进型", "aggressive_text": "即使是在动荡的市场中,可能性也很大。使用激进型(Aggressive)来应对热门 NFT drop 等导致的网络流量激增。", "market_label": "市场型", - "market_text": "Use market for fast processing at current market price.", + "market_text": "使用市价以当前市场价格快速交易。", "low_label": "低", "low_text": "使用“低级型”等待更低价格。由于价格具有一定不可预测性,时间预估的准确性会大幅降低。", "link": "了解更多关于自定义燃料的信息。" }, "save": "保存", "submit": "提交", - "max_priority_fee_low": "Max priority fee is low for current network conditions", - "max_priority_fee_high": "Max priority fee is higher than necessary", - "max_priority_fee_speed_up_low": "Max priority fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_priority_fee_cancel_low": "Max priority fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", - "max_fee_low": "Max fee is low for current network conditions", - "max_fee_high": "Max fee is higher than necessary", - "max_fee_speed_up_low": "Max fee must be at least {{speed_up_floor_value}} GWEI (10% higher than initial transaction)", - "max_fee_cancel_low": "Max fee must be at least {{cancel_value}} GWEI (50% higher than initial transaction)", + "max_priority_fee_low": "就目前的网络条件而言,最大优先费用较低", + "max_priority_fee_high": "最大优先费用高于必要的水平", + "max_priority_fee_speed_up_low": "最大优先费用必须至少是 {{speed_up_floor_value}} GWEI(比初始交易高 10%)", + "max_priority_fee_cancel_low": "最大优先费用必须至少是 {{cancel_value}} GWEI(比初始交易高 50%)", + "max_fee_low": "就目前的网络条件而言,最大费用较低", + "max_fee_high": "最大费用高于必要的水平", + "max_fee_speed_up_low": "最大费用必须至少是 {{speed_up_floor_value}} GWEI(比初始交易高 10%)", + "max_fee_cancel_low": "最大费用必须至少是 {{cancel_value}} GWEI(比初始交易高 50%)", "learn_more_gas_limit": "燃料限额是您愿意使用的最大燃料单位。燃料单位是“最大优先费”和“最大费用”的乘数。", "learn_more_max_priority_fee": "最大优先费(又称 “矿工费”)直接给矿工,激励他们优先处理您的交易。您通常会支付您的最大设置。", "learn_more_max_fee": "最大费用是您将支付的最大费用(基本费用 + 优先费用)。", @@ -5530,9 +5560,9 @@ "enable_remember_me_description": "在“记住我”功能打开时,任何有访问您手机权限的人都可以访问您的 MetaMask 账户。" }, "turn_off_remember_me": { - "title": "输入密码以关闭“记住我”", - "placeholder": "密码", - "description": "如果关闭此选项,从现在起,您需要使用密码来解锁 MetaMask。", + "title": "关闭“记住我”", + "placeholder": "确认密码", + "description": "一旦关闭,将无法再次使用“记住我”功能。此功能已停止服务,您可以使用密码或生物识别方式解锁 MetaMask。", "action": "关闭“记住我”" }, "dapp_connect": { @@ -5582,7 +5612,7 @@ "learn_more": "了解详情" }, "token_allowance": { - "verify_third_party_details": "Verify third-party details", + "verify_third_party_details": "核实第三方的详细信息", "protect_from_scams": "为了免受骗子欺诈,请花点时间核实第三方的详细信息。", "learn_to_verify": "了解如何核实第三方的详细信息", "spending_cap": "支出上限", @@ -5596,7 +5626,7 @@ }, "restore_wallet": { "restore_needed_title": "需要恢复", - "restore_needed_description": "Something went wrong, but don’t worry! Let’s try to restore your wallet.", + "restore_needed_description": "遇到了一些问题,但别担心!我们可以尝试恢复您的钱包。", "restore_needed_action": "恢复钱包" }, "wallet_restored": { @@ -5666,8 +5696,8 @@ "running_app_close_error": "无法关闭正在Ledger设备上运行的应用程序。", "ethereum_app_not_installed": "未安装以太坊应用程序。", "ethereum_app_not_installed_error": "请在Ledger设备上安装以太坊应用程序。", - "eth_app_not_open": "Ethereum app not open", - "eth_app_not_open_message": "Please open the Ethereum app on your Ledger device.", + "eth_app_not_open": "以太坊应用未打开", + "eth_app_not_open_message": "请在您的 Ledger 设备上打开以太坊应用。", "ledger_is_locked": "Ledger已锁定", "unlock_ledger_message": "请解锁您的Ledger设备", "cannot_get_account": "无法获取账户", @@ -5797,8 +5827,8 @@ "error_description": "{{snap}}安装失败。" }, "earn": { - "claimable_bonus_tooltip": "An annual bonus claimable daily from your wallet.", - "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "claimable_bonus_tooltip": "可从您的钱包中每日领取的年度奖励。", + "earn_a_percentage_bonus": "获得 {{percentage}}% 的奖励", "claimable_bonus": "可领取奖励", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -5836,20 +5866,20 @@ "withdrawal_time": "从协议中提取代币并转入钱包所需的时间", "receive": "此代币用于追踪您的资产和奖励。请勿转账或交易这些代币,否则您将无法提取资产。", "health_factor": { - "your_health_factor_measures_liquidation_risk": "Your health factor measures liquidation risk", + "your_health_factor_measures_liquidation_risk": "您的健康系数用于衡量清算风险", "above_two_dot_zero": "高于 2.0", "safe_position": "安全位置", "between_one_dot_five_and_2_dot_zero": "介于 1.5-2.0 之间", - "medium_liquidation_risk": "Medium liquidation risk", + "medium_liquidation_risk": "中等清算风险", "below_one_dot_five": "低于 1.5", "higher_liquidation_risk": "更高的清算风险" }, "lending_risk_aware_withdrawal_tooltip": { "why_cant_i_withdraw_full_balance": "为什么我无法提取全部余额?", "your_withdrawal_amount_may_be_limited_by": "您的提款金额可能会受到以下因素限制:", - "pool_liquidity": "Pool liquidity", + "pool_liquidity": "资金池流动性", "not_enough_funds_available_in_the_lending_pool_right_now": "目前出借池中没有足够的可用资金。", - "existing_borrow_positions": "Existing borrow positions", + "existing_borrow_positions": "现有借款仓位", "withdrawing_could_put_your_existing_loans_at_risk_of_liquidation": "提取资金可能会使您现有的借出头寸面临清算风险。" } }, @@ -5998,11 +6028,11 @@ "earn_button": "赚取" }, "trx_learn_more": { - "title": "Stake TRX and earn", - "stake_any_amount": "Stake any amount of TRX.", + "title": "质押 TRX 并赚取奖励", + "stake_any_amount": "质押任意金额的 TRX。", "earn_trx_rewards": "赚取 TRX 奖励。", "earn_trx_rewards_description": "质押后,即刻开始赚取奖励。奖励将自动累积。", - "flexible_unstaking_description": "Unstake anytime. Unstaking takes 14 days to process." + "flexible_unstaking_description": "随时解除质押。通常需要 14 天的时间来处理。" }, "day": { "zero": "", @@ -6151,7 +6181,7 @@ }, "estimated_gas_fee": { "title": "估算的燃料费", - "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. MetaMask does not profit from gas fees.", + "gas_recipient": "燃料费支付给在以太坊网络上处理交易的加密矿工。MetaMask 不会从燃料费中获利。", "gas_fluctuation": "燃料费是估算的,并将根据网络流量和交易复杂性而波动。", "gas_learn_more": "了解更多有关燃料费的信息" }, @@ -6186,14 +6216,14 @@ "signing_in_with": "使用以下方式登录", "spender": "支出者", "now": "立即", - "switching_to": "Switching to", + "switching_to": "正切换至", "bridge_estimated_time": "预计时间", "pay_with": "支付方式:", - "receive_as": "Receive", + "receive_as": "接收", "total": "总额", - "you_receive": "You'll receive", + "you_receive": "您将收到", "transaction_fee": "交易费用", - "transaction_fees": "Transaction fees", + "transaction_fees": "交易费用", "metamask_fee": "MetaMask 费用", "network_fee": "网络费", "bridge_fee": "桥接服务商费用" @@ -6234,7 +6264,7 @@ "transaction_fee": "我们将在预测所使用的 Polygon 网络上将您的代币兑换为 USDC.e。兑换提供商可能收取费用,但 MetaMask 不收取费用。" }, "predict_withdraw": { - "transaction_fee": "MetaMask will swap to your desired token for you. No MetaMask fee applies when you swap to MUSD." + "transaction_fee": "MetaMask 将为您兑换为您想要的代币。兑换至 MUSD 时,MetaMask 不收取任何费用。" }, "musd_conversion": { "transaction_fee": "mUSD 兑换费用包含网络成本,且可能包含服务商费用。" @@ -6256,12 +6286,12 @@ "personal_sign_tooltip": "此网站要求您签名", "transaction_tooltip": "此网站正在请求您的交易", "details": "详情", - "qr_get_sign": "Get signature", + "qr_get_sign": "获取签名", "qr_scan_text": "使用您的硬件钱包扫描", "sign_with_ledger": "使用 Ledger 签名", "smart_account": "智能账户", "smart_contract": "智能合约", - "standard_account": "Standard account", + "standard_account": "标准账户", "siwe_message": { "url": "URL", "network": "网络", @@ -6295,7 +6325,7 @@ }, "7702_functionality": { "smartAccountLabel": "智能账户", - "standardAccountLabel": "Standard account", + "standardAccountLabel": "标准账户", "switch": "切换", "switchBack": "切换回", "includes_transaction": "包含 {{transactionCount}} 笔交易", @@ -6307,9 +6337,9 @@ "cancel": "取消", "description": "输入您愿意让他人代表您花费的金额。", "invalid_number_error": "支出限额必须为数字", - "no_empty_error": "Spending cap can't be empty", - "no_extra_decimals_error": "Spending cap can't have more decimals than the token", - "no_zero_error": "Spending cap can't be 0", + "no_empty_error": "支出限额不能为空", + "no_extra_decimals_error": "支出限额的小数位数不能超过该代币的小数位数", + "no_zero_error": "支出限额不能为 0", "no_zero_error_decrease_allowance": "支出限额设为 0 对“decreaseAllowance”方法无影响", "no_zero_error_increase_allowance": "支出限额设为 0 对“increaseAllowance”方法无影响", "save": "保存", @@ -6336,7 +6366,7 @@ "transferRequest": "转账请求", "nested_transaction_heading": "交易 {{index}}", "transaction": "交易", - "available_balance": "Available balance: ", + "available_balance": "可用余额: ", "edit_amount_done": "继续", "deposit_edit_amount_done": "充值", "deposit_edit_amount_predict_withdraw": "提取", @@ -6366,7 +6396,7 @@ "terms_and_conditions": "条款和条件", "select_token": "选择代币", "no_tokens_found": "找不到代币", - "no_tokens_found_description": "We couldn't find any tokens with this name. Try a different search.", + "no_tokens_found_description": "我们未找到与此名称匹配的代币。请尝试其他搜索方式。", "select_network": "选择网络", "all_networks": "所有网络", "num_networks": "{{numNetworks}} 个网络", @@ -6375,7 +6405,7 @@ "deselect_all_networks": "取消全部选择", "see_all": "查看全部", "all": "所有", - "more_networks": "+{{count}} more", + "more_networks": "额外 +{{count}} 个", "apply": "应用", "slippage": "滑点", "slippage_info": "如果价格在您下单和确认之间发生变化,称为“滑点”。如果滑点超过您在此设置的最大幅度,您的兑换将自动取消。", @@ -6392,7 +6422,7 @@ "quote_info_title": "费率", "network_fee_info_title": "网络费", "network_fee_info_content": "网络费用取决于网络的繁忙程度及交易的复杂程度。", - "network_fee_info_content_sponsored": "This network fee is paid by MetaMask, so you can transact without {{nativeToken}} in your account.", + "network_fee_info_content_sponsored": "这笔网络费用由 MetaMask 承担,因此即使您的账户中没有 {{nativeToken}},您也可以进行交易。", "points": "预计积分", "points_tooltip": "积分", "points_tooltip_content_1": "积分是您通过完成交易(如兑换、桥接或永续合约交易)获得 MetaMask Rewards 的方式。", @@ -6406,7 +6436,7 @@ "select_recipient": "选择接收方", "external_account": "外部账户", "error_banner_description": "目前该交易路线不可用。请调整交易金额、切换网络或更换代币类型,我们将为您自动匹配最优通道。", - "stock_token_error_banner_description": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option.\n\nPlease note if you are attempting to trade Ondo Tokenised Stocks you may be geo-restricted e.g. via US, EU, UK and BR.", + "stock_token_error_banner_description": "此交易路径当前不可用。请尝试调整金额、网络或代币,我们将为您找到最佳方案。\n\n请注意,如果您想要交易 Ondo 代币化股票,可能会受到地域限制(例如美国、欧盟、英国和巴西等地区)。", "insufficient_funds": "资金不足", "insufficient_gas": "燃料不足", "select_amount": "选择金额", @@ -6417,9 +6447,9 @@ "title": "桥接", "submitting_transaction": "正在提交", "fetching_quote": "正在获取报价", - "fee_disclaimer": "Includes {{feePercentage}}% MetaMask fee.", + "fee_disclaimer": "包含 {{feePercentage}}% MetaMask 费用。", "no_mm_fee": "无 MM 费用", - "no_mm_fee_disclaimer": "No MetaMask fee swapping into {{destTokenSymbol}}.", + "no_mm_fee_disclaimer": "兑换为 {{destTokenSymbol}} 时免收 MetaMask 费用。", "hardware_wallet_not_supported": "目前暂不支持硬件钱包。请改用热钱包继续操作。", "hardware_wallet_not_supported_solana": "Solana 目前暂不支持硬件钱包。请改用热钱包继续操作。", "price_impact_info_title": "价格影响", @@ -6432,17 +6462,24 @@ "approval_needed": "批准代币用于兑换。", "approval_tooltip_title": "授予精确访问权限", "approval_tooltip_content": "您正在授权动用指定金额({{amount}} {{symbol}})。该合约将无法动用超出此额度的任何资金。", - "minimum_received": "Minimum received", - "minimum_received_tooltip_title": "Minimum received", + "minimum_received": "最低收款", + "minimum_received_tooltip_title": "最低收款", "minimum_received_tooltip_content": "若交易处理期间价格发生波动,根据您设置的滑点容差,此为您将收到的最低金额。该金额来自流动性供应商提供的预估,最终到账金额可能存在差异。", + "market_closed": { + "title": "已休市", + "description": "支撑该代币的市场目前已关闭。但代币仍可随时在链上进行转移。", + "learn_more": "了解详情", + "learn_more_url": "https://status.ondo.finance/market", + "done": "已完成" + }, "submit": "提交", - "default_slippage_description": "Your transaction won't go through if the price changes more than the slippage percent.", + "default_slippage_description": "如果价格变化超过滑移百分比,您的交易将无法成功。", "cancel": "取消", "confirm": "确认", "exceeding_upper_slippage_warning": "高滑移,这可能导致不利兑换", "exceeding_lower_slippage_warning": "低滑移,这可能导致不利兑换", - "exceeding_lower_slippage_error": "Enter a value greater than {{value}}%", - "exceeding_upper_slippage_error": "You cannot enter a value greater than {{value}}%", + "exceeding_lower_slippage_error": "输入一个大于 {{value}}% 的值", + "exceeding_upper_slippage_error": "您输入的数值不能大于 {{value}}%", "custom": "自定义" }, "quote_expired_modal": { @@ -6517,7 +6554,7 @@ "title": "钱包恢复", "login_with_social": "使用社交账户登录", "setup": "设置", - "secret_recovery_phrase": "Secret Recovery Phrase {{num}}", + "secret_recovery_phrase": "私钥助记词 {{num}}", "back_up": "备份", "reveal": "显示", "social_recovery_title": "{{authConnection}} 恢复", @@ -6739,6 +6776,7 @@ "password_bottomsheet": { "title": "输入密码", "description": "输入您的钱包密码以查看卡详情。", + "description_unfreeze": "请输入您的钱包密码以恢复卡消费。", "placeholder": "密码", "confirm": "确认", "cancel": "取消", @@ -7001,6 +7039,7 @@ "enable_card_error": "启用卡失败。请稍后再试。", "view_card_details_error": "卡详情加载失败。请重试。", "biometric_verification_required": "查看卡详情需要身份验证。", + "unfreeze_auth_required": "需要验证才能恢复卡消费。", "warnings": { "close_spending_limit": { "title": "您即将达到消费限额", @@ -7018,7 +7057,7 @@ }, "frozen": { "title": "您的卡已冻结", - "description": "请联系支持团队解冻您的卡" + "description": "您的卡已暂时冻结。您可随时将其解冻。" }, "blocked": { "title": "您的卡已封禁", @@ -7068,7 +7107,14 @@ "travel_description": "预订酒店享高达 70% 折扣", "card_tos_title": "条款和条件", "order_metal_card": "金属卡", - "order_metal_card_description": "立即订购您的实体金属卡" + "order_metal_card_description": "立即订购您的实体金属卡", + "freeze_card": "冻结卡", + "unfreeze_card": "解冻卡", + "freeze_card_description": "暂停卡所有消费", + "unfreeze_card_description": "恢复卡所有消费", + "freeze_error": "更新卡状态失败。请重试。", + "freeze_success": "卡已成功冻结", + "unfreeze_success": "卡已成功解冻" } }, "card_spending_limit": { @@ -7160,31 +7206,31 @@ "resend_cooldown": "{{seconds}} 秒后可重新发送" }, "push_provisioning": { - "add_to_wallet": "Add to {{walletName}}", - "adding_to_wallet": "Adding to {{walletName}}...", - "continue_setup": "Continue {{walletName}} Setup", - "wallet_not_available": "{{walletName}} not available", - "already_in_wallet": "Already in {{walletName}}", - "success_title": "Card added!", - "success_message": "Your MetaMask Card has been added to {{walletName}}.", - "error_title": "Unable to add card", - "error_wallet_not_available": "{{walletName}} is not available on this device. Please ensure you have {{walletName}} set up.", - "error_wallet_not_initialized": "{{walletName}} is not initialized. Please set up your wallet and try again.", + "add_to_wallet": "添加到 {{walletName}}", + "adding_to_wallet": "正在添加到 {{walletName}}……", + "continue_setup": "继续设置 {{walletName}}", + "wallet_not_available": "{{walletName}} 不可用", + "already_in_wallet": "已在 {{walletName}} 中", + "success_title": "卡已添加!", + "success_message": "您的 MetaMask 卡已添加至 {{walletName}}。", + "error_title": "无法添加卡", + "error_wallet_not_available": "该设备上 {{walletName}} 不可用。请确保您已设置 {{walletName}}。", + "error_wallet_not_initialized": "{{walletName}} 未初始化。请设置您的钱包并重试。", "error_card_already_in_wallet": "此卡已添加至 {{walletName}}。", "error_card_pending": "您的卡正在 {{walletName}} 中设置。请过几分钟再来查看。", "error_card_suspended": "您在 {{walletName}} 中的卡已被停用。请联系支持团队寻求帮助。", "error_card_not_eligible": "此卡无法进行移动钱包配置。", - "error_encryption_failed": "Failed to encrypt card data. Please try again.", + "error_encryption_failed": "加密卡数据失败。请重试。", "error_invalid_card_data": "卡数据无效。请核对您的卡详情后重试。", "error_card_not_found": "找不到卡。请重试。", "error_card_provider_not_found": "您所在地区暂无此卡服务商。", "error_card_id_mismatch": "卡验证失败。请重试。", "error_card_not_active": "您的卡尚未激活。请先激活您的卡。", "error_network": "发生了网络错误。请检查您的连接后重试。", - "error_timeout": "The request timed out. Please try again.", - "error_server": "Server error occurred. Please try again later.", - "error_unknown": "An unexpected error occurred. Please try again or contact support.", - "error_platform_not_supported": "This platform does not support mobile wallet provisioning.", + "error_timeout": "请求超时。请重试。", + "error_server": "服务器错误。请稍后再试。", + "error_unknown": "发生意外错误。请重试或联系支持团队。", + "error_platform_not_supported": "此平台不支持移动钱包添加。", "try_again": "请重试", "cancel": "取消" } @@ -7299,7 +7345,7 @@ "main_title": "奖励", "referral_title": "推荐", "tab_overview_title": "概览", - "tab_snapshots_title": "Snapshots", + "tab_snapshots_title": "快照", "tab_activity_title": "活动", "referral_stats_earned_from_referrals": "通过推荐所获奖励", "referral_stats_referrals": "推荐", @@ -7353,7 +7399,7 @@ "verifying_rewards": "在您领取奖励前,我们正在核对所有信息以确保准确无误。" }, "season_status": { - "points_earned": "Points earned" + "points_earned": "已获得积分" }, "onboarding": { "not_supported_region_title": "不支持此地区", @@ -7431,7 +7477,7 @@ "show_less": "收起", "linking_progress": "正在添加账户……({{current}}/{{total}})", "accounts_linked_count": "{{linked}}{{total}} 已加入", - "add_all_accounts": "Add all accounts" + "add_all_accounts": "添加全部账户" }, "referred_by_code": { "title": "推荐码", @@ -7514,7 +7560,7 @@ "claim_label": "领取", "claimed_label": "已领取", "reward_claimed": "已领取奖励", - "time_left": "{{time}} left", + "time_left": "还剩 {{time}}", "expired": "已过期" }, "end_of_season_rewards": { @@ -7528,37 +7574,37 @@ "redeem_failure_title": "兑换失败", "redeem_failure_description": "请稍后再试。", "reward_details": "奖励详情", - "select_account_description": "Select the account where you'd like this reward sent." + "select_account_description": "选择接收此奖励的账户。" }, "animation": { "could_not_load": "无法加载" }, "snapshot": { - "starts_date": "Starts {{date}}", - "ends_date": "Ends {{date}}", - "results_coming_soon": "Results coming soon", - "tokens_on_the_way": "Tokens on the way", - "pill_up_next": "Up next", - "pill_live_now": "Live now", - "pill_calculating": "Calculating", - "pill_results_ready": "Results Ready", - "pill_complete": "Complete" + "starts_date": "开始于 {{date}}", + "ends_date": "结束于 {{date}}", + "results_coming_soon": "结果即将公布", + "tokens_on_the_way": "代币即将到账", + "pill_up_next": "即将到来", + "pill_live_now": "现已上线", + "pill_calculating": "正在计算", + "pill_results_ready": "结果已就绪", + "pill_complete": "完成" }, "snapshots_section": { - "title": "Snapshots", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "title": "快照", + "error_title": "无法加载快照", + "error_description": "无法加载快照。请重试。", "retry_button": "重试" }, "snapshots_tab": { - "active_title": "Active", - "upcoming_title": "Upcoming", - "previous_title": "Previous", - "empty_state": "No snapshots available", - "error_title": "Unable to load snapshots", - "error_description": "We couldn't load the snapshots. Please try again.", + "active_title": "已激活", + "upcoming_title": "即将到来", + "previous_title": "先前", + "empty_state": "暂无快照", + "error_title": "无法加载快照", + "error_description": "无法加载快照。请重试。", "retry_button": "重试", - "refreshing": "Refreshing..." + "refreshing": "正在刷新……" } }, "time": { @@ -7588,9 +7634,9 @@ "bridge_approval": "批准 {{approveSymbol}}", "bridge_approval_loading": "批准", "bridge_send": "从 {{sourceChain}} 桥接 {{sourceSymbol}}", - "bridge_send_loading": "Bridge send", + "bridge_send_loading": "跨链桥发送", "bridge_receive": "在 {{targetChain}} 接收 {{targetSymbol}}", - "bridge_receive_loading": "Bridge receive", + "bridge_receive_loading": "跨链桥接收", "default": "交易", "musd_convert_send": "已从 {{sourceChain}} 发送 {{sourceSymbol}}", "musd_claim": "领取 mUSD", @@ -7607,20 +7653,20 @@ "description": "正在与 {{dappName}} 建立连接……" }, "show_error": { - "title": "Connection error", + "title": "连接错误", "description": "建立连接失败。请重试。" }, "show_rejection": { - "title": "Approval rejected", - "description": "User rejected the request." + "title": "审批已拒绝", + "description": "用户已拒绝该请求。" }, "show_return_to_app": { "title": "成功", "description": "返回应用程序继续操作。" }, "show_not_found": { - "title": "Connection Not Found", - "description": "Please establish a new connection from the app to continue." + "title": "未找到连接", + "description": "请从应用中重新建立连接以继续。" } }, "network_connection_banner": { @@ -7633,16 +7679,16 @@ "updated_to_metamask_default": "Updated to MetaMask default" }, "trending": { - "title": "Explore", - "trending_tokens": "Trending tokens", + "title": "探索", + "trending_tokens": "热门代币", "price_change": "价格变化", "all_networks": "所有网络", - "24h": "24h", + "24h": "24 小时", "time": "时间", "24_hours": "24 小时", "6_hours": "6 小时", - "1_hour": "1 hour", - "5_minutes": "5 minutes", + "1_hour": "1 小时", + "5_minutes": "5 分钟", "networks": "网络", "sort_by": "排序方式", "volume": "交易量", @@ -7650,32 +7696,48 @@ "high_to_low": "从高到低", "low_to_high": "从低到高", "apply": "应用", - "search_placeholder": "Search tokens, sites, URLs", + "search_placeholder": "搜索代币、网站、URL", "cancel": "取消", "perps": "永续合约", "predictions": "预测", - "no_results": "No results found", + "no_results": "未找到结果", "sites": "网站", - "popular_sites": "Popular sites", - "search_sites": "Search sites", - "enable_basic_functionality": "Enable basic functionality", - "basic_functionality_disabled_title": "Explore is not available", - "basic_functionality_disabled_description": "We can't fetch the required metadata when basic functionality is disabled.", + "popular_sites": "热门网站", + "search_sites": "搜索网站", + "enable_basic_functionality": "启用基本功能", + "basic_functionality_disabled_title": "探索功能不可用", + "basic_functionality_disabled_description": "当基础功能被禁用时,我们无法获取所需的元数据。", "empty_error_trending_state": { - "title": "Trending tokens is not available", - "description": "We can't fetch this page right now", + "title": "热门代币暂不可用", + "description": "我们目前无法加载此页面", "try_again": "请重试" }, "empty_search_result_state": { "title": "找不到代币", - "description": "We were not able to find this token" + "description": "我们找不到此代币" } }, "ota_update_modal": { - "title": "Update ready", - "description_ios": "We've made some important fixes. Reload for the latest version of MetaMask.", - "description_android": "We've made some important fixes. Close and reopen MetaMask to apply the update.", + "title": "更新就绪", + "description_ios": "我们已进行一些重要修复。请重新加载以获取最新版 MetaMask。", + "description_android": "我们已进行一些重要修复。请关闭并重新打开 MetaMask 以应用更新。\n\n", "primary_action_reload": "重新加载", "primary_action_acknowledge": "知道了" + }, + "homepage": { + "sections": { + "tokens": "代币", + "perpetuals": "永续合约", + "predictions": "预测", + "defi": "DeFi", + "nfts": "NFT", + "import_nfts": "导入 NFT", + "import_nfts_description": "Easily add your collectibles", + "more_predictions": "More predictions" + }, + "error": { + "unable_to_load": "Unable to load {{section}}", + "retry": "Retry" + } } } From 968afc6fd4987468606b570ae0fa6389c62a69b3 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 24 Feb 2026 11:49:06 +0000 Subject: [PATCH 007/131] [skip ci] Bump version number to 3784 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ed46021c2c0..57d8768a1a0 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.67.0" - versionCode 3779 + versionCode 3784 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 68b7de4599c..e6553b6176d 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3779 + VERSION_NUMBER: 3784 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3779 + FLASK_VERSION_NUMBER: 3784 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index ed65c96dbcf..5ee95d5c0af 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3779; + CURRENT_PROJECT_VERSION = 3784; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3779; + CURRENT_PROJECT_VERSION = 3784; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3779; + CURRENT_PROJECT_VERSION = 3784; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3779; + CURRENT_PROJECT_VERSION = 3784; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3779; + CURRENT_PROJECT_VERSION = 3784; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3779; + CURRENT_PROJECT_VERSION = 3784; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 8f1ce833e44096164e5e507777257f12ca322dc9 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:06:56 +0000 Subject: [PATCH 008/131] chore(runway): cherry-pick fix(perps): connection-aware ensureReady() to fix stale cache on slow connections cp-7.67.0 (#26334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(perps): connection-aware ensureReady() to fix stale cache on slow connections cp-7.67.0 (#26324) ## **Description** Fixes [TAT-2597](https://consensys-mesh.atlassian.net/browse/TAT-2597) and [TAT-2598](https://consensys-mesh.atlassian.net/browse/TAT-2598): After the preload PR merged, slow connections caused StreamChannels to exhaust 150 polling retries (30s) in `ensureReady()` and silently give up, leaving users with stale REST cache and no live WebSocket data — positions not appearing after trade, missing prices. **Root Cause:** `StreamChannel.ensureReady()` used blind polling (`isReady` every 200ms × 150 retries) with no awareness of WebSocket connection state. On slow connections, the connection had not even established yet, so polling burned through all retries before data could arrive. **Fix:** - `PerpsConnectionManager.waitForConnection()` — exposes init/reconnect promises so channels can `await` instead of blind-polling - `StreamChannel.ensureReady()` — detects `isConnecting` state and awaits the connection promise via `awaitConnectionThenConnect()` **Result:** PriceStreamChannel retries dropped from **33 → 0** on device after this fix. ## **Changelog** CHANGELOG entry: Fixed stale cache on slow connections where positions and prices were not updating after a trade ## **Related issues** Fixes: [TAT-2597](https://consensys-mesh.atlassian.net/browse/TAT-2597), [TAT-2598](https://consensys-mesh.atlassian.net/browse/TAT-2598) ## **Manual testing steps** ```gherkin Feature: Perps live data on slow connections Scenario: user opens a trade on a slow connection Given the app is connected to a slow 3G network And the user has navigated to the Perps trading screen When user opens a new position Then the position appears immediately in the positions list And price stream connects without excessive retries Scenario: user recovers from network drop Given the user is viewing live perps positions And the network connection drops momentarily When the network connection is restored Then live WebSocket data resumes without stale cache ``` ## **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 - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. [TAT-2597]: https://consensyssoftware.atlassian.net/browse/TAT-2597?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [TAT-2598]: https://consensyssoftware.atlassian.net/browse/TAT-2598?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [TAT-2597]: https://consensyssoftware.atlassian.net/browse/TAT-2597?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- > [!NOTE] > **Medium Risk** > Changes connection/retry timing and sequencing for Perps WebSocket subscriptions; regressions could delay or prevent live data if the new await/sentinel logic misfires under edge cases (disconnects, rapid resubscribe). > > **Overview** > Perps stream channels now become **connection-aware**: `StreamChannel.ensureReady()` detects `PerpsConnectionManager`'s `isConnecting` state and waits on a new `waitForConnection()` promise, then retries `connect()` (with duplicate-wait suppression and polling fallback on rejection). > > Retry scheduling was tightened by clearing `deferConnect` timers on fire and centralizing the retry delay via `PERPS_CONSTANTS.ConnectRetryDelayMs`; `MarketDataChannel` now uses this constant as well. Tests were expanded to cover the new await/deferral behavior and the new `PerpsConnectionManager.waitForConnection()` contract. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 39e206bc224ebd7d3c4b980d8e77d65c9fd01359. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [43cf1d8](https://github.com/MetaMask/metamask-mobile/commit/43cf1d8ced97ab8a1ad30189f6f6883e5425569f) [TAT-2597]: https://consensyssoftware.atlassian.net/browse/TAT-2597?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [TAT-2598]: https://consensyssoftware.atlassian.net/browse/TAT-2598?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [TAT-2597]: https://consensyssoftware.atlassian.net/browse/TAT-2597?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ Co-authored-by: abretonc7s <107169956+abretonc7s@users.noreply.github.com> --- .../providers/PerpsStreamManager.test.tsx | 163 ++++++++++++++++++ .../UI/Perps/providers/PerpsStreamManager.tsx | 65 ++++++- .../services/PerpsConnectionManager.test.ts | 66 +++++++ .../Perps/services/PerpsConnectionManager.ts | 22 +++ .../perps/constants/perpsConfig.ts | 1 + 5 files changed, 313 insertions(+), 4 deletions(-) diff --git a/app/components/UI/Perps/providers/PerpsStreamManager.test.tsx b/app/components/UI/Perps/providers/PerpsStreamManager.test.tsx index 76c9c3b6ed8..a4b2f3e17e4 100644 --- a/app/components/UI/Perps/providers/PerpsStreamManager.test.tsx +++ b/app/components/UI/Perps/providers/PerpsStreamManager.test.tsx @@ -131,6 +131,11 @@ describe('PerpsStreamManager', () => { isConnecting: false, error: null, }); + mockPerpsConnectionManager.waitForConnection = jest.fn().mockReturnValue( + new Promise((_resolve) => { + /* never resolves */ + }), + ); }); afterEach(() => { @@ -1844,6 +1849,34 @@ describe('PerpsStreamManager', () => { expect(mockLogger.error).toBeDefined(); }); + it('defers connect when isCurrentlyConnecting returns true', () => { + // Arrange — connection is initialized but still connecting + mockPerpsConnectionManager.isCurrentlyConnecting = jest.fn(() => true); + mockPerpsConnectionManager.getConnectionState = jest + .fn() + .mockReturnValue({ isInitialized: true }); + + const mockGetMarketData = jest.fn().mockResolvedValue([]); + mockEngine.context.PerpsController.getMarketDataWithPrices = + mockGetMarketData; + + const streamManager = new PerpsStreamManager(); + const callback = jest.fn(); + + // Act — subscribe triggers connect() → ensureReady passes but isCurrentlyConnecting guard defers + streamManager.marketData.subscribe({ callback, throttleMs: 0 }); + + // Assert — should NOT have called getMarketDataWithPrices yet + expect(mockGetMarketData).not.toHaveBeenCalled(); + + // Now flip the guard and advance timer so deferConnect fires + mockPerpsConnectionManager.isCurrentlyConnecting = jest.fn(() => false); + jest.advanceTimersByTime(250); + + // Now it should have called getMarketDataWithPrices + expect(mockGetMarketData).toHaveBeenCalled(); + }); + it('discards fetched data when provider changes during in-flight fetch', async () => { // Arrange mockPerpsConnectionManager.isCurrentlyConnecting = jest.fn(() => false); @@ -3468,6 +3501,136 @@ describe('PerpsStreamManager', () => { }); }); + describe('awaitConnectionThenConnect', () => { + it('early-exits when sentinel timer is already set (duplicate await prevention)', () => { + const streamManager = new PerpsStreamManager(); + const mockSubscribeToOrderFills = jest.fn().mockReturnValue(jest.fn()); + + // Not initialized but actively connecting → triggers awaitConnectionThenConnect + mockPerpsConnectionManager.getConnectionState = jest + .fn() + .mockReturnValue({ isInitialized: false, isConnecting: true }); + + // waitForConnection returns a never-resolving promise + mockPerpsConnectionManager.waitForConnection = jest.fn().mockReturnValue( + new Promise(() => { + /* never resolves */ + }), + ); + + mockEngine.context.PerpsController = { + ...mockEngine.context.PerpsController, + subscribeToOrderFills: mockSubscribeToOrderFills, + isCurrentlyReinitializing: jest.fn().mockReturnValue(false), + } as unknown as typeof mockEngine.context.PerpsController; + + // First subscribe → ensureReady() → awaitConnectionThenConnect() sets sentinel + streamManager.fills.subscribe({ callback: jest.fn(), throttleMs: 0 }); + + // Second subscribe → ensureReady() → awaitConnectionThenConnect() hits early-exit + streamManager.fills.subscribe({ callback: jest.fn(), throttleMs: 0 }); + + // waitForConnection should only have been called once (second call was skipped) + expect( + mockPerpsConnectionManager.waitForConnection, + ).toHaveBeenCalledTimes(1); + }); + + it('calls connect() when waitForConnection resolves', async () => { + const streamManager = new PerpsStreamManager(); + const mockSubscribeToOrderFills = jest.fn().mockReturnValue(jest.fn()); + + // Not initialized but actively connecting → triggers awaitConnectionThenConnect + mockPerpsConnectionManager.getConnectionState = jest + .fn() + .mockReturnValue({ isInitialized: false, isConnecting: true }); + + // waitForConnection resolves immediately + mockPerpsConnectionManager.waitForConnection = jest + .fn() + .mockResolvedValue(undefined); + + mockEngine.context.PerpsController = { + ...mockEngine.context.PerpsController, + subscribeToOrderFills: mockSubscribeToOrderFills, + isCurrentlyReinitializing: jest.fn().mockReturnValue(false), + } as unknown as typeof mockEngine.context.PerpsController; + + // Subscribe triggers ensureReady → awaitConnectionThenConnect + streamManager.fills.subscribe({ callback: jest.fn(), throttleMs: 0 }); + + // Flush the sentinel setTimeout(noop, 0) + jest.advanceTimersByTime(0); + + // Flush microtasks so the .then() handler runs + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + // After the promise resolves, the .then() handler calls connect() + // which needs isInitialized: true to actually call subscribeToOrderFills + mockPerpsConnectionManager.getConnectionState = jest + .fn() + .mockReturnValue({ isInitialized: true }); + + // The .then() callback already fired and called connect(); but connect() + // re-checks ensureReady(). Since we changed state after, let the deferred + // connect run if one was set. + jest.advanceTimersByTime(0); + await Promise.resolve(); + await Promise.resolve(); + + expect(mockSubscribeToOrderFills).toHaveBeenCalled(); + }); + + it('falls back to deferConnect when waitForConnection rejects', async () => { + const streamManager = new PerpsStreamManager(); + const mockSubscribeToOrderFills = jest.fn().mockReturnValue(jest.fn()); + + // Not initialized but actively connecting + mockPerpsConnectionManager.getConnectionState = jest + .fn() + .mockReturnValue({ isInitialized: false, isConnecting: true }); + + // waitForConnection rejects + mockPerpsConnectionManager.waitForConnection = jest + .fn() + .mockRejectedValue(new Error('connection failed')); + + mockEngine.context.PerpsController = { + ...mockEngine.context.PerpsController, + subscribeToOrderFills: mockSubscribeToOrderFills, + isCurrentlyReinitializing: jest.fn().mockReturnValue(false), + } as unknown as typeof mockEngine.context.PerpsController; + + // Subscribe → awaitConnectionThenConnect + streamManager.fills.subscribe({ callback: jest.fn(), throttleMs: 0 }); + + // Flush sentinel timer + jest.advanceTimersByTime(0); + + // Flush microtasks so .catch() handler runs + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + // The .catch() handler calls deferConnect(200ms) + // Now make connection ready so when deferConnect fires, connect() succeeds + mockPerpsConnectionManager.getConnectionState = jest + .fn() + .mockReturnValue({ isInitialized: true }); + + // Advance past the ConnectRetryDelayMs (200ms) + jest.advanceTimersByTime(250); + + expect(mockSubscribeToOrderFills).toHaveBeenCalled(); + expect(mockDevLogger.log).toHaveBeenCalledWith( + expect.stringContaining('connection failed, falling back to polling'), + ); + }); + }); + describe('FillStreamChannel isInitialized guard', () => { it('defers connect when isInitialized is false', () => { const streamManager = new PerpsStreamManager(); diff --git a/app/components/UI/Perps/providers/PerpsStreamManager.tsx b/app/components/UI/Perps/providers/PerpsStreamManager.tsx index d7343017ed0..1ea67125562 100644 --- a/app/components/UI/Perps/providers/PerpsStreamManager.tsx +++ b/app/components/UI/Perps/providers/PerpsStreamManager.tsx @@ -173,27 +173,84 @@ abstract class StreamChannel { if (this.deferConnectTimer) { clearTimeout(this.deferConnectTimer); } - this.deferConnectTimer = setTimeout(() => this.connect(), delayMs); + this.deferConnectTimer = setTimeout(() => { + this.deferConnectTimer = null; + this.connect(); + }, delayMs); } /** * Common initialization guard for connect(). * Returns true if the channel is ready to connect, false if deferred. * Resets connectRetryCount on success. + * + * When the connection manager is actively connecting, awaits the connection + * promise and retries connect() once resolved, instead of blind 200ms polling. */ protected ensureReady(): boolean { if (Engine.context.PerpsController.isCurrentlyReinitializing()) { this.deferConnect(PERPS_CONSTANTS.ReconnectionCleanupDelayMs); return false; } - if (!PerpsConnectionManager.getConnectionState().isInitialized) { - this.deferConnect(200); + const connState = PerpsConnectionManager.getConnectionState(); + if (!connState.isInitialized) { + // If actively connecting, await the connection promise instead of polling + if (connState.isConnecting) { + DevLogger.log( + `${this.constructor.name}: ensureReady: awaiting active connection`, + ); + this.awaitConnectionThenConnect(); + return false; + } + this.deferConnect(PERPS_CONSTANTS.ConnectRetryDelayMs); return false; } this.connectRetryCount = 0; return true; } + /** + * Await the PerpsConnectionManager connection promise, then retry connect(). + * This replaces blind 200ms polling when we know a connection is in progress. + */ + private awaitConnectionThenConnect(): void { + // Prevent duplicate awaits — only one outstanding wait at a time + if (this.deferConnectTimer) { + return; + } + // Use a sentinel timer value to signal that we're waiting on the promise + // This prevents deferConnect from also scheduling a parallel timer + const noop = () => { + /* sentinel timer */ + }; + const sentinel = setTimeout(noop, 0) as ReturnType; + this.deferConnectTimer = sentinel; + + PerpsConnectionManager.waitForConnection() + .then(() => { + // Only clear if our sentinel is still the active timer; a disconnect() + // followed by a new subscribe() may have replaced it with a real timer. + if (this.deferConnectTimer === sentinel) { + this.deferConnectTimer = null; + } + if (this.subscribers.size > 0) { + this.connect(); + } + }) + .catch(() => { + if (this.deferConnectTimer === sentinel) { + this.deferConnectTimer = null; + } + // Connection failed — fall back to normal defer polling + if (this.subscribers.size > 0) { + DevLogger.log( + `${this.constructor.name}: awaitConnectionThenConnect: connection failed, falling back to polling`, + ); + this.deferConnect(PERPS_CONSTANTS.ConnectRetryDelayMs); + } + }); + } + /** * Reconnect the channel after WebSocket reconnection * Clears dead subscription and re-establishes if there are active subscribers @@ -1261,7 +1318,7 @@ class MarketDataChannel extends StreamChannel { // Check if connection manager is still connecting - retry later if so if (PerpsConnectionManager.isCurrentlyConnecting()) { - this.deferConnect(200); + this.deferConnect(PERPS_CONSTANTS.ConnectRetryDelayMs); return; } diff --git a/app/components/UI/Perps/services/PerpsConnectionManager.test.ts b/app/components/UI/Perps/services/PerpsConnectionManager.test.ts index 78b662790d9..08c9ca0ae74 100644 --- a/app/components/UI/Perps/services/PerpsConnectionManager.test.ts +++ b/app/components/UI/Perps/services/PerpsConnectionManager.test.ts @@ -839,4 +839,70 @@ describe('PerpsConnectionManager', () => { }); }); }); + + describe('waitForConnection', () => { + it('awaits resolving initPromise', async () => { + // Arrange — set initPromise to a resolved promise + const m = PerpsConnectionManager as unknown as { + initPromise: Promise | null; + }; + m.initPromise = Promise.resolve(); + + // Act & Assert — should complete without error + await expect( + PerpsConnectionManager.waitForConnection(), + ).resolves.toBeUndefined(); + + // Cleanup + m.initPromise = null; + }); + + it('swallows initPromise rejection', async () => { + // Arrange — set initPromise to a rejected promise + const m = PerpsConnectionManager as unknown as { + initPromise: Promise | null; + }; + m.initPromise = Promise.reject(new Error('init failed')); + + // Act & Assert — should resolve (not throw) even though initPromise rejects + await expect( + PerpsConnectionManager.waitForConnection(), + ).resolves.toBeUndefined(); + + // Cleanup + m.initPromise = null; + }); + + it('awaits resolving pendingReconnectPromise', async () => { + // Arrange — set pendingReconnectPromise to a resolved promise + const m = PerpsConnectionManager as unknown as { + pendingReconnectPromise: Promise | null; + }; + m.pendingReconnectPromise = Promise.resolve(); + + // Act & Assert — should complete without error + await expect( + PerpsConnectionManager.waitForConnection(), + ).resolves.toBeUndefined(); + + // Cleanup + m.pendingReconnectPromise = null; + }); + + it('swallows pendingReconnectPromise rejection', async () => { + // Arrange — set pendingReconnectPromise to a rejected promise + const m = PerpsConnectionManager as unknown as { + pendingReconnectPromise: Promise | null; + }; + m.pendingReconnectPromise = Promise.reject(new Error('reconnect failed')); + + // Act & Assert — should resolve (not throw) even though promise rejects + await expect( + PerpsConnectionManager.waitForConnection(), + ).resolves.toBeUndefined(); + + // Cleanup + m.pendingReconnectPromise = null; + }); + }); }); diff --git a/app/components/UI/Perps/services/PerpsConnectionManager.ts b/app/components/UI/Perps/services/PerpsConnectionManager.ts index 75cadbfdfd9..dff4bf9695b 100644 --- a/app/components/UI/Perps/services/PerpsConnectionManager.ts +++ b/app/components/UI/Perps/services/PerpsConnectionManager.ts @@ -1006,6 +1006,28 @@ class PerpsConnectionManagerClass { }; } + /** + * Returns a promise that resolves when the current connection attempt completes. + * If no connection is in progress, resolves immediately. + * Used by StreamChannel.ensureReady() to await connection instead of blind polling. + */ + async waitForConnection(): Promise { + if (this.initPromise) { + try { + await this.initPromise; + } catch { + // Connection failed — caller will check getConnectionState() + } + } + if (this.pendingReconnectPromise) { + try { + await this.pendingReconnectPromise; + } catch { + // Reconnection failed — caller will check getConnectionState() + } + } + } + /** * Check if the manager is fully disconnected and ready to connect */ diff --git a/app/controllers/perps/constants/perpsConfig.ts b/app/controllers/perps/constants/perpsConfig.ts index 809e09740dc..c12fe232d0e 100644 --- a/app/controllers/perps/constants/perpsConfig.ts +++ b/app/controllers/perps/constants/perpsConfig.ts @@ -25,6 +25,7 @@ export const PERPS_CONSTANTS = { ConnectionGracePeriodMs: 20_000, // 20 seconds grace period before actual disconnection (same as BackgroundDisconnectDelay for semantic clarity) ConnectionAttemptTimeoutMs: 30_000, // 30 seconds timeout for connection attempts to prevent indefinite hanging WebsocketPingTimeoutMs: 5_000, // 5 seconds timeout for WebSocket health check ping + ConnectRetryDelayMs: 200, // Delay before retrying connect() when connection isn't ready yet ReconnectionCleanupDelayMs: 500, // Platform-agnostic delay to ensure WebSocket is ready ReconnectionDelayAndroidMs: 300, // Android-specific reconnection delay for better reliability on slower devices ReconnectionDelayIosMs: 100, // iOS-specific reconnection delay for optimal performance From 3769a8cbe89274f0d2f3356c78813f4a388c0047 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 24 Feb 2026 12:08:24 +0000 Subject: [PATCH 009/131] [skip ci] Bump version number to 3785 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 57d8768a1a0..ad66552ab90 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.67.0" - versionCode 3784 + versionCode 3785 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index e6553b6176d..30e2814e2e1 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3784 + VERSION_NUMBER: 3785 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3784 + FLASK_VERSION_NUMBER: 3785 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 5ee95d5c0af..298d458956c 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3784; + CURRENT_PROJECT_VERSION = 3785; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3784; + CURRENT_PROJECT_VERSION = 3785; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3784; + CURRENT_PROJECT_VERSION = 3785; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3784; + CURRENT_PROJECT_VERSION = 3785; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3784; + CURRENT_PROJECT_VERSION = 3785; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3784; + CURRENT_PROJECT_VERSION = 3785; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From a31e92d2a674f967ee3f9faea7abc9c37b915500 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:36:58 +0100 Subject: [PATCH 010/131] chore(runway): cherry-pick fix: Fix StorageService Key Encoding cp-7.66.0 (#26337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: Fix StorageService Key Encoding cp-7.66.0 (#26268) ## **Description** ### Problem The `redux-persist-filesystem-storage` library has two problematic behaviors when handling arbitrary keys in `StorageService`: 1. **Slashes (`/`) create subdirectories**: Keys containing slashes are stored in subdirectories, making them unreachable via `getAllKeys`. This breaks the `clear` method for affected keys. 2. **Hyphens (`-`) get corrupted**: The library's internal `fromFileName` function converts hyphens to colons (`:`), meaning keys like `simple-key` are returned as `simple:key` by `getAllKeys`. This causes a permanent mismatch between stored keys and returned keys. ### Solution This PR introduces URI-style encoding for problematic characters in **both namespace and key** portions of storage keys: - `/` → `%2F` (prevents subdirectory creation) - `-` → `%2D` (prevents hyphen-to-colon corruption) - `%` → `%25` (prevents double-encoding issues) The encoding is applied via shared utility functions (`encodeStorageKey`/`decodeStorageKey`) used by both: - `mobileStorageAdapter` in `storage-service-init.ts` - Migration 118 which stores snap source code ### Backward Compatibility **This change is backward compatible and will NOT break existing production keys.** - Existing keys like `storageService:TokenListController:tokensChainsCache:0x1` are **unaffected** because: - The namespace (`TokenListController`) has no special characters → encoding produces identical output - Colons (`:`) are **not encoded** - they pass through unchanged - The key portion (`tokensChainsCache:0x1`) contains no hyphens or slashes - **Strings without `-`, `/`, or `%` characters produce identical output when encoded** - This means all current production keys work exactly as before, while future keys with special characters will be handled correctly ### Examples ``` # No special characters → unchanged (backward compatible) storageService:TokenListController:tokensChainsCache:0x1 → storageService:TokenListController:tokensChainsCache:0x1 # Snap ID with slashes and hyphens → encoded storageService:SnapController:npm:@metamask/bip32-keyring-snap → storageService:SnapController:npm:@metamask%2Fbip32%2Dkeyring%2Dsnap # Namespace with hyphen → encoded storageService:Some-Controller:some-key → storageService:Some%2DController:some%2Dkey # Key with slashes → encoded (prevents subdirectory creation) storageService:TestController:nested/path/key → storageService:TestController:nested%2Fpath%2Fkey ``` ### Files Changed | File | Change | |------|--------| | `app/core/Engine/utils/storage-service-utils.ts` | New utility with `encodeStorageKey`/`decodeStorageKey` functions | | `app/core/Engine/utils/storage-service-utils.test.ts` | 35 unit tests for the encoding utilities | | `app/core/Engine/controllers/storage-service-init.ts` | Apply encoding in `mobileStorageAdapter` methods | | `app/core/Engine/controllers/storage-service-init.test.ts` | 22 new tests for key and namespace encoding behavior | | `app/store/migrations/119.ts` | Encode snap IDs when storing snap source code | ## **Changelog** CHANGELOG entry: null ## **Related issues** Refs: StorageService key handling issues with `redux-persist-filesystem-storage` ## **Manual testing steps** ```gherkin Feature: StorageService key encoding Scenario: Keys with hyphens are stored and retrieved correctly Given the app is running When StorageService stores a key containing hyphens (e.g., "npm:@metamask/bip32-keyring-snap") Then the key is encoded as "npm:@metamask%2Fbip32%2Dkeyring%2Dsnap" on disk And getAllKeys returns the original key "npm:@metamask/bip32-keyring-snap" And getItem with the original key returns the stored data Scenario: Keys with slashes are stored and retrieved correctly Given the app is running When StorageService stores a key containing slashes (e.g., "nested/path/key") Then the key is stored as a single file (not in subdirectories) And getAllKeys returns the original key "nested/path/key" And clear removes the key correctly Scenario: Existing keys with colons remain unchanged Given existing production keys like "storageService:TokenListController:tokensChainsCache:0x1" When the app starts with this fix Then the existing keys are still accessible And no migration is required for existing data ``` ## **Screenshots/Recordings** N/A - No UI changes ### **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 - [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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Changes how persistent storage keys are constructed and enumerated, which could impact read/write/clear behavior for stored controller data if encoding is applied inconsistently or assumptions about existing on-disk keys differ. > > **Overview** > Fixes mobile `StorageService` persistence by **encoding both `namespace` and `key` components** before writing to `redux-persist-filesystem-storage` (and decoding on `getAllKeys`) to avoid `/` creating subdirectories and `-` being corrupted. > > Introduces shared `encodeStorageKey`/`decodeStorageKey` utilities and updates migration `119` to encode snap IDs when persisting snap `sourceCode`; expands/updates unit tests to cover encoding/decoding behavior and the updated migration expectations. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit bb0e8d690abff41c8079ff4dd2e18df84c0f3904. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor [5d11a21](https://github.com/MetaMask/metamask-mobile/commit/5d11a21b465c88b9d698b24f9082fb9c16c5bd84) Co-authored-by: Andre Pimenta Co-authored-by: Cursor --- .../controllers/storage-service-init.test.ts | 309 +++++++++++++++++- .../controllers/storage-service-init.ts | 53 ++- .../utils/storage-service-utils.test.ts | 176 ++++++++++ .../Engine/utils/storage-service-utils.ts | 35 ++ app/store/migrations/119.test.ts | 7 +- app/store/migrations/119.ts | 5 +- 6 files changed, 561 insertions(+), 24 deletions(-) create mode 100644 app/core/Engine/utils/storage-service-utils.test.ts create mode 100644 app/core/Engine/utils/storage-service-utils.ts diff --git a/app/core/Engine/controllers/storage-service-init.test.ts b/app/core/Engine/controllers/storage-service-init.test.ts index 56581f3e717..a2be01a3c5e 100644 --- a/app/core/Engine/controllers/storage-service-init.test.ts +++ b/app/core/Engine/controllers/storage-service-init.test.ts @@ -329,9 +329,6 @@ describe('mobileStorageAdapter', () => { expect(mockFilesystemStorage.removeItem).toHaveBeenCalledWith( `${STORAGE_KEY_PREFIX}TestController:key2`, ); - expect(mockLogger.log).toHaveBeenCalledWith( - 'StorageService: Cleared 2 keys for TestController', - ); }); it('returns early when getAllKeys returns null', async () => { @@ -345,7 +342,7 @@ describe('mobileStorageAdapter', () => { expect(mockFilesystemStorage.removeItem).not.toHaveBeenCalled(); }); - it('removes zero keys and logs count when namespace has no matching entries', async () => { + it('removes zero keys when namespace has no matching entries', async () => { mockFilesystemStorage.getAllKeys.mockResolvedValue([ `${STORAGE_KEY_PREFIX}OtherController:key1`, ]); @@ -355,9 +352,6 @@ describe('mobileStorageAdapter', () => { await adapter.clear('TestController'); expect(mockFilesystemStorage.removeItem).not.toHaveBeenCalled(); - expect(mockLogger.log).toHaveBeenCalledWith( - 'StorageService: Cleared 0 keys for TestController', - ); }); it('throws and logs error when FilesystemStorage fails', async () => { @@ -379,4 +373,305 @@ describe('mobileStorageAdapter', () => { ); }); }); + + describe('key encoding', () => { + describe('setItem', () => { + beforeEach(() => { + mockFilesystemStorage.setItem.mockResolvedValue(undefined); + mockDevice.isIos.mockReturnValue(true); + }); + + it('encodes hyphens in keys as %2D', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('TestController', 'simple-key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:simple%2Dkey`, + JSON.stringify('value'), + true, + ); + }); + + it('encodes slashes in keys as %2F', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('TestController', 'nested/path/key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:nested%2Fpath%2Fkey`, + JSON.stringify('value'), + true, + ); + }); + + it('encodes percent signs in keys as %25', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('TestController', 'percent%key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:percent%25key`, + JSON.stringify('value'), + true, + ); + }); + + it('encodes mixed special characters in keys', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem( + 'TestController', + 'mixed-key/with%special', + 'value', + ); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:mixed%2Dkey%2Fwith%25special`, + JSON.stringify('value'), + true, + ); + }); + + it('does not encode colons in keys', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem( + 'TestController', + 'tokensChainsCache:0x1', + 'value', + ); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:tokensChainsCache:0x1`, + JSON.stringify('value'), + true, + ); + }); + }); + + describe('getItem', () => { + it('encodes hyphens in keys when retrieving', async () => { + mockFilesystemStorage.getItem.mockResolvedValue(JSON.stringify('data')); + const adapter = getStorageAdapter(); + + await adapter.getItem('TestController', 'simple-key'); + + expect(mockFilesystemStorage.getItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:simple%2Dkey`, + ); + }); + + it('encodes slashes in keys when retrieving', async () => { + mockFilesystemStorage.getItem.mockResolvedValue(JSON.stringify('data')); + const adapter = getStorageAdapter(); + + await adapter.getItem('TestController', 'nested/path/key'); + + expect(mockFilesystemStorage.getItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:nested%2Fpath%2Fkey`, + ); + }); + + it('encodes snap IDs with special characters', async () => { + mockFilesystemStorage.getItem.mockResolvedValue( + JSON.stringify({ sourceCode: '...' }), + ); + const adapter = getStorageAdapter(); + + await adapter.getItem( + 'SnapController', + 'npm:@metamask/bip32-keyring-snap', + ); + + expect(mockFilesystemStorage.getItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}SnapController:npm:@metamask%2Fbip32%2Dkeyring%2Dsnap`, + ); + }); + }); + + describe('removeItem', () => { + it('encodes hyphens in keys when removing', async () => { + mockFilesystemStorage.removeItem.mockResolvedValue(undefined); + const adapter = getStorageAdapter(); + + await adapter.removeItem('TestController', 'simple-key'); + + expect(mockFilesystemStorage.removeItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:simple%2Dkey`, + ); + }); + + it('encodes slashes in keys when removing', async () => { + mockFilesystemStorage.removeItem.mockResolvedValue(undefined); + const adapter = getStorageAdapter(); + + await adapter.removeItem('TestController', 'nested/path/key'); + + expect(mockFilesystemStorage.removeItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:nested%2Fpath%2Fkey`, + ); + }); + }); + + describe('getAllKeys', () => { + it('decodes %2D back to hyphens in returned keys', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}TestController:simple%2Dkey`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('TestController'); + + expect(result).toStrictEqual(['simple-key']); + }); + + it('decodes %2F back to slashes in returned keys', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}TestController:nested%2Fpath%2Fkey`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('TestController'); + + expect(result).toStrictEqual(['nested/path/key']); + }); + + it('decodes %25 back to percent signs in returned keys', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}TestController:percent%25key`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('TestController'); + + expect(result).toStrictEqual(['percent%key']); + }); + + it('decodes mixed encoded characters in returned keys', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}TestController:mixed%2Dkey%2Fwith%25special`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('TestController'); + + expect(result).toStrictEqual(['mixed-key/with%special']); + }); + + it('decodes snap IDs with special characters', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}SnapController:npm:@metamask%2Fbip32%2Dkeyring%2Dsnap`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('SnapController'); + + expect(result).toStrictEqual(['npm:@metamask/bip32-keyring-snap']); + }); + + it('returns multiple decoded keys correctly', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}TestController:simple%2Dkey`, + `${STORAGE_KEY_PREFIX}TestController:nested%2Fpath`, + `${STORAGE_KEY_PREFIX}TestController:safe_key`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('TestController'); + + expect(result).toStrictEqual(['simple-key', 'nested/path', 'safe_key']); + }); + }); + + describe('namespace encoding', () => { + beforeEach(() => { + mockFilesystemStorage.setItem.mockResolvedValue(undefined); + mockDevice.isIos.mockReturnValue(true); + }); + + it('encodes hyphens in namespace as %2D', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('Test-Controller', 'key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}Test%2DController:key`, + JSON.stringify('value'), + true, + ); + }); + + it('encodes slashes in namespace as %2F', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('Test/Controller', 'key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}Test%2FController:key`, + JSON.stringify('value'), + true, + ); + }); + + it('encodes both namespace and key with special characters', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('My-Controller', 'nested/path-key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}My%2DController:nested%2Fpath%2Dkey`, + JSON.stringify('value'), + true, + ); + }); + + it('does not change namespaces without special characters', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem( + 'TokenListController', + 'tokensChainsCache:0x1', + 'value', + ); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TokenListController:tokensChainsCache:0x1`, + JSON.stringify('value'), + true, + ); + }); + + it('getAllKeys uses encoded namespace for prefix matching', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}My%2DController:key1`, + `${STORAGE_KEY_PREFIX}My%2DController:key2`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('My-Controller'); + + expect(result).toStrictEqual(['key1', 'key2']); + }); + + it('clear uses encoded namespace for prefix matching', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}My%2DController:key1`, + `${STORAGE_KEY_PREFIX}My%2DController:key2`, + ]); + mockFilesystemStorage.removeItem.mockResolvedValue(undefined); + const adapter = getStorageAdapter(); + + await adapter.clear('My-Controller'); + + expect(mockFilesystemStorage.removeItem).toHaveBeenCalledTimes(2); + expect(mockFilesystemStorage.removeItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}My%2DController:key1`, + ); + expect(mockFilesystemStorage.removeItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}My%2DController:key2`, + ); + }); + }); + }); }); diff --git a/app/core/Engine/controllers/storage-service-init.ts b/app/core/Engine/controllers/storage-service-init.ts index 088fdfe9210..48942fa683e 100644 --- a/app/core/Engine/controllers/storage-service-init.ts +++ b/app/core/Engine/controllers/storage-service-init.ts @@ -11,6 +11,10 @@ import { } from '@metamask/storage-service'; import Device from '../../../util/device'; import Logger from '../../../util/Logger'; +import { + encodeStorageKey, + decodeStorageKey, +} from '../utils/storage-service-utils'; /** * Mobile-specific storage adapter using FilesystemStorage. @@ -30,8 +34,11 @@ const mobileStorageAdapter: StorageAdapter = { */ async getItem(namespace: string, key: string): Promise { try { - // Build full key: storageService:namespace:key - const fullKey = `${STORAGE_KEY_PREFIX}${namespace}:${key}`; + // Build full key: storageService:encodedNamespace:encodedKey + const encodedNamespace = encodeStorageKey(namespace); + const encodedKey = encodeStorageKey(key); + const fullKey = `${STORAGE_KEY_PREFIX}${encodedNamespace}:${encodedKey}`; + const serialized = await FilesystemStorage.getItem(fullKey); // Key not found - return empty object @@ -59,8 +66,10 @@ const mobileStorageAdapter: StorageAdapter = { */ async setItem(namespace: string, key: string, value: Json): Promise { try { - // Build full key: storageService:namespace:key - const fullKey = `${STORAGE_KEY_PREFIX}${namespace}:${key}`; + // Build full key: storageService:encodedNamespace:encodedKey + const encodedNamespace = encodeStorageKey(namespace); + const encodedKey = encodeStorageKey(key); + const fullKey = `${STORAGE_KEY_PREFIX}${encodedNamespace}:${encodedKey}`; await FilesystemStorage.setItem( fullKey, @@ -83,8 +92,11 @@ const mobileStorageAdapter: StorageAdapter = { */ async removeItem(namespace: string, key: string): Promise { try { - // Build full key: storageService:namespace:key - const fullKey = `${STORAGE_KEY_PREFIX}${namespace}:${key}`; + // Build full key: storageService:encodedNamespace:encodedKey + const encodedNamespace = encodeStorageKey(namespace); + const encodedKey = encodeStorageKey(key); + const fullKey = `${STORAGE_KEY_PREFIX}${encodedNamespace}:${encodedKey}`; + await FilesystemStorage.removeItem(fullKey); } catch (error) { Logger.error(error as Error, { @@ -109,11 +121,19 @@ const mobileStorageAdapter: StorageAdapter = { return []; } - const prefix = `${STORAGE_KEY_PREFIX}${namespace}:`; + // Encode namespace to match how keys were stored + const encodedNamespace = encodeStorageKey(namespace); + const prefix = `${STORAGE_KEY_PREFIX}${encodedNamespace}:`; - return allKeys - .filter((key) => key.startsWith(prefix)) - .map((key) => key.slice(prefix.length)); + const filteredKeys = allKeys + .filter((rawKey) => rawKey.startsWith(prefix)) + .map((rawKey) => { + // Extract the encoded key part and decode it + const encodedKeyPart = rawKey.slice(prefix.length); + return decodeStorageKey(encodedKeyPart); + }); + + return filteredKeys; } catch (error) { Logger.error(error as Error, { message: `StorageService: Failed to get keys for ${namespace}`, @@ -135,11 +155,18 @@ const mobileStorageAdapter: StorageAdapter = { return; } - const prefix = `${STORAGE_KEY_PREFIX}${namespace}:`; - const keysToDelete = allKeys.filter((key) => key.startsWith(prefix)); + // Encode namespace to match how keys were stored + const encodedNamespace = encodeStorageKey(namespace); + const prefix = `${STORAGE_KEY_PREFIX}${encodedNamespace}:`; + + const keysToDelete = allKeys.filter((rawKey) => + rawKey.startsWith(prefix), + ); + // For deletion, we pass the raw key as returned by getAllKeys. + // FilesystemStorage.removeItem will apply toFileName to find the file. await Promise.all( - keysToDelete.map((key) => FilesystemStorage.removeItem(key)), + keysToDelete.map((rawKey) => FilesystemStorage.removeItem(rawKey)), ); Logger.log( diff --git a/app/core/Engine/utils/storage-service-utils.test.ts b/app/core/Engine/utils/storage-service-utils.test.ts new file mode 100644 index 00000000000..25a183ab314 --- /dev/null +++ b/app/core/Engine/utils/storage-service-utils.test.ts @@ -0,0 +1,176 @@ +import { encodeStorageKey, decodeStorageKey } from './storage-service-utils'; + +describe('storage-service-utils', () => { + describe('encodeStorageKey', () => { + it('encodes hyphens as %2D', () => { + const result = encodeStorageKey('simple-key'); + + expect(result).toBe('simple%2Dkey'); + }); + + it('encodes slashes as %2F', () => { + const result = encodeStorageKey('nested/path/key'); + + expect(result).toBe('nested%2Fpath%2Fkey'); + }); + + it('encodes percent signs as %25', () => { + const result = encodeStorageKey('percent%key'); + + expect(result).toBe('percent%25key'); + }); + + it('encodes multiple hyphens', () => { + const result = encodeStorageKey('key-with-multiple-hyphens'); + + expect(result).toBe('key%2Dwith%2Dmultiple%2Dhyphens'); + }); + + it('encodes multiple slashes', () => { + const result = encodeStorageKey('a/b/c/d'); + + expect(result).toBe('a%2Fb%2Fc%2Fd'); + }); + + it('encodes mixed special characters', () => { + const result = encodeStorageKey('mixed-key/with%special'); + + expect(result).toBe('mixed%2Dkey%2Fwith%25special'); + }); + + it('encodes snap IDs with npm scope', () => { + const result = encodeStorageKey('npm:@metamask/bip32-keyring-snap'); + + expect(result).toBe('npm:@metamask%2Fbip32%2Dkeyring%2Dsnap'); + }); + + it('does not encode colons', () => { + const result = encodeStorageKey('tokensChainsCache:0x1'); + + expect(result).toBe('tokensChainsCache:0x1'); + }); + + it('does not encode underscores', () => { + const result = encodeStorageKey('safe_key_name'); + + expect(result).toBe('safe_key_name'); + }); + + it('does not encode alphanumeric characters', () => { + const result = encodeStorageKey('SimpleKey123'); + + expect(result).toBe('SimpleKey123'); + }); + + it('returns empty string for empty input', () => { + const result = encodeStorageKey(''); + + expect(result).toBe(''); + }); + + it('encodes percent first to avoid double-encoding', () => { + const result = encodeStorageKey('key%2Dalready'); + + expect(result).toBe('key%252Dalready'); + }); + }); + + describe('decodeStorageKey', () => { + it('decodes %2D back to hyphens', () => { + const result = decodeStorageKey('simple%2Dkey'); + + expect(result).toBe('simple-key'); + }); + + it('decodes %2F back to slashes', () => { + const result = decodeStorageKey('nested%2Fpath%2Fkey'); + + expect(result).toBe('nested/path/key'); + }); + + it('decodes %25 back to percent signs', () => { + const result = decodeStorageKey('percent%25key'); + + expect(result).toBe('percent%key'); + }); + + it('decodes multiple encoded hyphens', () => { + const result = decodeStorageKey('key%2Dwith%2Dmultiple%2Dhyphens'); + + expect(result).toBe('key-with-multiple-hyphens'); + }); + + it('decodes multiple encoded slashes', () => { + const result = decodeStorageKey('a%2Fb%2Fc%2Fd'); + + expect(result).toBe('a/b/c/d'); + }); + + it('decodes mixed encoded characters', () => { + const result = decodeStorageKey('mixed%2Dkey%2Fwith%25special'); + + expect(result).toBe('mixed-key/with%special'); + }); + + it('decodes snap IDs with npm scope', () => { + const result = decodeStorageKey('npm:@metamask%2Fbip32%2Dkeyring%2Dsnap'); + + expect(result).toBe('npm:@metamask/bip32-keyring-snap'); + }); + + it('handles lowercase encoding', () => { + const result = decodeStorageKey('key%2dvalue%2fpath'); + + expect(result).toBe('key-value/path'); + }); + + it('handles uppercase encoding', () => { + const result = decodeStorageKey('key%2Dvalue%2Fpath'); + + expect(result).toBe('key-value/path'); + }); + + it('returns empty string for empty input', () => { + const result = decodeStorageKey(''); + + expect(result).toBe(''); + }); + + it('returns unencoded strings unchanged', () => { + const result = decodeStorageKey('SimpleKey123'); + + expect(result).toBe('SimpleKey123'); + }); + }); + + describe('encode/decode roundtrip', () => { + const testCases = [ + 'simple-key', + 'nested/path/key', + 'mixed-key/with/path', + 'percent%encoded', + 'npm:@metamask/bip32-keyring-snap', + 'tokensChainsCache:0x1', + 'cache:0x1:tokens', + 'safe_key', + 'CamelCaseKey', + 'complex-key/with%special-chars', + '', + ]; + + it.each(testCases)('roundtrips "%s" correctly', (original) => { + const encoded = encodeStorageKey(original); + const decoded = decodeStorageKey(encoded); + + expect(decoded).toBe(original); + }); + + it('handles double encoding prevention', () => { + const original = 'key%2Dalready-encoded'; + const encoded = encodeStorageKey(original); + const decoded = decodeStorageKey(encoded); + + expect(decoded).toBe(original); + }); + }); +}); diff --git a/app/core/Engine/utils/storage-service-utils.ts b/app/core/Engine/utils/storage-service-utils.ts new file mode 100644 index 00000000000..ec8c7e8f8fc --- /dev/null +++ b/app/core/Engine/utils/storage-service-utils.ts @@ -0,0 +1,35 @@ +/** + * Utility functions for StorageService key encoding/decoding. + * + * These functions handle the quirks of redux-persist-filesystem-storage: + * 1. `/` in keys creates subdirectories, making keys unreachable via getAllKeys + * 2. `-` gets converted to `:` by fromFileName, corrupting the key + * + * We encode `-` and `/` but not `:` because we already have keys with colons + * in production (like `tokensChainsCache:0x1`), so encoding `:` would break + * existing data. + * + * We use URI-style encoding (%XX) for these characters because it's a + * well-understood, reversible format. + */ + +/** + * Encode a string to avoid issues with redux-persist-filesystem-storage. + * + * @param key - The string to encode (namespace or key). + * @returns The encoded string safe for filesystem storage. + */ +export const encodeStorageKey = (key: string): string => + key + .replace(/%/g, '%25') // Encode % first to avoid double-encoding + .replace(/\//g, '%2F') // Encode slashes (would create subdirectories) + .replace(/-/g, '%2D'); // Encode hyphens (would be converted to colons) + +/** + * Decode a key that was encoded with encodeStorageKey. + * + * @param encodedKey - The encoded key to decode. + * @returns The original key. + */ +export const decodeStorageKey = (encodedKey: string): string => + encodedKey.replace(/%2D/gi, '-').replace(/%2F/gi, '/').replace(/%25/g, '%'); diff --git a/app/store/migrations/119.test.ts b/app/store/migrations/119.test.ts index 5629127e5b7..c3ea4979706 100644 --- a/app/store/migrations/119.test.ts +++ b/app/store/migrations/119.test.ts @@ -130,23 +130,24 @@ describe(`migration #${migrationVersion}`, () => { const migratedState = await migrate(oldState); + // Snap IDs are encoded: hyphens become %2D expect(FilesystemStorage.setItem).toHaveBeenNthCalledWith( 1, - `${STORAGE_KEY_PREFIX}SnapController:mock-snap-id`, + `${STORAGE_KEY_PREFIX}SnapController:mock%2Dsnap%2Did`, '{"sourceCode":"sourceCode"}', true, ); expect(FilesystemStorage.setItem).toHaveBeenNthCalledWith( 2, - `${STORAGE_KEY_PREFIX}SnapController:foo-snap-id`, + `${STORAGE_KEY_PREFIX}SnapController:foo%2Dsnap%2Did`, '{"sourceCode":"sourceCode2"}', true, ); expect(FilesystemStorage.setItem).toHaveBeenNthCalledWith( 3, - `${STORAGE_KEY_PREFIX}SnapController:bar-snap-id`, + `${STORAGE_KEY_PREFIX}SnapController:bar%2Dsnap%2Did`, '{"sourceCode":"sourceCode3 "}', true, ); diff --git a/app/store/migrations/119.ts b/app/store/migrations/119.ts index 632189e9fc8..9831b886e83 100644 --- a/app/store/migrations/119.ts +++ b/app/store/migrations/119.ts @@ -5,6 +5,7 @@ import { getErrorMessage, hasProperty, isObject } from '@metamask/utils'; import FilesystemStorage from 'redux-persist-filesystem-storage'; import { STORAGE_KEY_PREFIX } from '@metamask/storage-service'; import Device from '../../util/device'; +import { encodeStorageKey } from '../../core/Engine/utils/storage-service-utils'; export const migrationVersion = 119; @@ -89,7 +90,9 @@ async function transformState(state: ValidState) { ).map(async (snap) => { const sourceCode = snap.sourceCode as string; - const fullKey = `${STORAGE_KEY_PREFIX}SnapController:${snap.id}`; + // Encode the snap ID to handle special characters (e.g., slashes and hyphens in npm:@metamask/bip32-keyring-snap) + const encodedSnapId = encodeStorageKey(snap.id as string); + const fullKey = `${STORAGE_KEY_PREFIX}SnapController:${encodedSnapId}`; await FilesystemStorage.setItem( fullKey, From 347dd4c342d394cef585f5775e3e60d142b1cfab Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:28:40 +0000 Subject: [PATCH 011/131] chore(runway): cherry-pick fix: cp-7.66.0 refresh staked balance after account switch (#26349) - fix: cp-7.66.0 refresh staked balance after account switch (#26332) ## **Description** fix refresh staking balance after account switch ## **Changelog** CHANGELOG entry: fix refresh staking balance after account switch ## **Related issues** Fixes: #26323 ## **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/1a720a48-83e6-45e0-84e7-29b2cb3479e7 ### **After** https://github.com/user-attachments/assets/826a347b-e003-4bd6-8816-8bec2b66c423 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Small, localized change to a `useEffect` dependency plus a new test; main risk is accidental extra refresh calls impacting performance or causing redundant network requests. > > **Overview** > Refresh behavior on the `Wallet` screen is updated so `AccountTrackerController.refresh` is re-triggered when `selectedInternalAccount` changes (added to the relevant `useEffect` dependency list), fixing stale balance/staking data after an account switch. > > Tests are updated to cover this regression by asserting `refresh` is called again when `AccountsController.internalAccounts.selectedAccount` changes, and snapshots are updated accordingly. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a15a4d11abd0f2bf384ba1f3fd9b35a176b3cb24. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [c70f38d](https://github.com/MetaMask/metamask-mobile/commit/c70f38de59d42881e3337dce7b891d27b67bb2d0) Co-authored-by: Salim TOUBAL --- .../Wallet/__snapshots__/index.test.tsx.snap | 8 ++-- app/components/Views/Wallet/index.test.tsx | 43 ++++++++++++++++++- app/components/Views/Wallet/index.tsx | 2 +- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap index eadf69d1281..88901f22f28 100644 --- a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap @@ -656,7 +656,7 @@ exports[`Wallet Conditional Rendering should render banner when basic functional { expect(wrapper.toJSON()).toMatchSnapshot(); }); + it('calls AccountTrackerController.refresh when selectedInternalAccount changes', async () => { + const refreshMock = jest.mocked( + Engine.context.AccountTrackerController.refresh, + ); + + //@ts-expect-error we are ignoring the navigation params on purpose + render(Wallet); + await waitFor(() => expect(refreshMock).toHaveBeenCalled()); + refreshMock.mockClear(); + + renderScreen( + // @ts-expect-error we are ignoring the navigation params on purpose + Wallet, + { name: Routes.WALLET_VIEW }, + { + state: { + ...mockInitialState, + engine: { + backgroundState: { + ...mockInitialState.engine.backgroundState, + AccountsController: { + ...mockInitialState.engine.backgroundState.AccountsController, + internalAccounts: { + ...mockInitialState.engine.backgroundState.AccountsController + .internalAccounts, + selectedAccount: 'different-account-id', + }, + }, + }, + }, + }, + }, + ); + + await waitFor(() => expect(refreshMock).toHaveBeenCalled()); + }); + // Simple test to verify mock setup it('should have proper mock setup', () => { expect(typeof jest.fn()).toBe('function'); diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index bdce7cf502b..9c4cf76a90d 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -1063,7 +1063,7 @@ const Wallet = ({ /* eslint-disable-next-line */ // TODO: The need of usage of this chainId as a dependency is not clear, we shouldn't need to refresh the native balances when the chainId changes. Since the pooling is always working in the back. Check with assets team. // TODO: [SOLANA] Check if this logic supports non evm networks before shipping Solana - [navigation, chainId, evmNetworkConfigurations], + [navigation, chainId, evmNetworkConfigurations, selectedInternalAccount], ); const shouldDisplayCardButton = useSelector(selectDisplayCardButton); From 43b3ddb89a5cea90be08c57ed2aa9610fe221aa4 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:11:51 +0000 Subject: [PATCH 012/131] chore(runway): cherry-pick refactor(musd): replace decodeMerklClaimAmount with getClaimPayoutFromReceipt cp-7.66.0 (#26361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refactor(musd): replace decodeMerklClaimAmount with getClaimPayoutFromReceipt cp-7.66.0 (#26342) ## **Description** The mUSD claim transaction amount was displaying the cumulative total reward (from Merkl distributor calldata `amounts[0]`) instead of the actual per-transaction payout. This caused incorrect amounts in the Activity list and transaction detail views — e.g., if a user made multiple claims, each one would show the running total rather than what was actually claimed in that specific transaction. The fix introduces `getClaimPayoutFromReceipt()` which extracts the real payout from the ERC-20 `Transfer` event in the transaction receipt logs (emitted when the Merkl distributor transfers mUSD to the user). This is used as the primary source for confirmed transactions across: - Activity list (`decodeMusdClaimTx`) - Transaction detail hero (`useClaimAmount`) - Confirmation flow (`useMerklClaimAmount`) ## **Changelog** CHANGELOG entry: Fixed mUSD claim transactions showing incorrect cumulative total instead of per-transaction payout amount ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: mUSD claim transaction amount display Scenario: user views a confirmed mUSD claim transaction in Activity Given user has made one or more mUSD claim transactions from this device And at least one claim transaction is confirmed When user navigates to the Activity tab Then the claim transaction shows the correct per-transaction payout amount (not cumulative total) Scenario: user views claim transaction details Given user has a confirmed mUSD claim transaction visible in Activity When user taps on the claim transaction Then the detail view shows the correct claimed amount matching the actual payout Scenario: user views a pending mUSD claim in the Activity list Given user has a pending mUSD claim transaction When user views the transaction in Activity Then the amount shows "Not available" until the transaction confirms And once confirmed, the correct payout amount appears ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Changes how `musdClaim` amounts are derived in Activity/details/confirmation flows, relying on receipt log parsing and BigInt decoding; incorrect log matching or missing receipts could cause amounts to be blank or wrong. > > **Overview** > **Fixes incorrect mUSD claim amounts** by switching from decoding the Merkl claim calldata (cumulative total) to extracting the per-transaction payout from the ERC-20 `Transfer` event in the transaction receipt via new `getClaimPayoutFromReceipt()`. > > Updates `musdClaim` display/amount computation in the Activity list (`decodeMusdClaimTx`), transaction details hero (`useClaimAmount`), and confirmation flow hook (`useMerklClaimAmount`, which now prefers receipt payout when confirmed and falls back to the async unclaimed-amount calculation when pending). Tests are updated to validate receipt-log extraction and the new UI behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c66b845a5eda16cb8d310d22d1ae73107a6799e7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [0ef570f](https://github.com/MetaMask/metamask-mobile/commit/0ef570feb32998e4975d59c84460d987da7f79e4) Co-authored-by: Patryk Łucka <5708018+PatrykLucka@users.noreply.github.com> --- app/components/UI/Earn/utils/musd.test.ts | 133 +++++++++++++++--- app/components/UI/Earn/utils/musd.ts | 79 +++++++++-- app/components/UI/TransactionElement/utils.js | 8 +- .../transaction-details-hero.test.tsx | 31 ++-- .../transaction-details-hero.tsx | 11 +- .../hooks/earn/useMerklClaimAmount.ts | 39 ++++- 6 files changed, 248 insertions(+), 53 deletions(-) diff --git a/app/components/UI/Earn/utils/musd.test.ts b/app/components/UI/Earn/utils/musd.test.ts index bfcc8e4bb47..fa1497b1300 100644 --- a/app/components/UI/Earn/utils/musd.test.ts +++ b/app/components/UI/Earn/utils/musd.test.ts @@ -6,9 +6,12 @@ import { isMusdClaimForCurrentView, convertMusdClaimAmount, decodeMerklClaimParams, - decodeMerklClaimAmount, + getClaimPayoutFromReceipt, } from './musd'; -import { DISTRIBUTOR_CLAIM_ABI } from '../components/MerklRewards/constants'; +import { + DISTRIBUTOR_CLAIM_ABI, + MERKL_DISTRIBUTOR_ADDRESS, +} from '../components/MerklRewards/constants'; import { MUSD_TOKEN_ADDRESS } from '../constants/musd'; const LINEA_CHAIN_ID = '0xe708' as Hex; @@ -258,28 +261,120 @@ describe('musd utils', () => { }); }); - describe('decodeMerklClaimAmount', () => { - const userAddress = '0x1234567890123456789012345678901234567890'; - const tokenAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; - const amount = '999000'; + describe('getClaimPayoutFromReceipt', () => { + const USER = SELECTED_ADDRESS; + const TRANSFER_TOPIC = + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; + + const padAddress = (addr: string) => + `0x${addr.slice(2).toLowerCase().padStart(64, '0')}`; + + const makeTransferLog = ( + tokenAddress: string, + from: string, + to: string, + amount: bigint, + ) => ({ + address: tokenAddress, + topics: [TRANSFER_TOPIC, padAddress(from), padAddress(to)], + data: `0x${amount.toString(16).padStart(64, '0')}`, + }); - it('returns the amount from valid claim data', () => { - const iface = new Interface(DISTRIBUTOR_CLAIM_ABI); - const data = iface.encodeFunctionData('claim', [ - [userAddress], - [tokenAddress], - [amount], - [[]], - ]); - expect(decodeMerklClaimAmount(data)).toBe(amount); + it('extracts the payout from a matching Transfer log', () => { + const payout = 70000000n; // 70 mUSD + const logs = [ + makeTransferLog( + MUSD_TOKEN_ADDRESS, + MERKL_DISTRIBUTOR_ADDRESS, + USER, + payout, + ), + ]; + + expect(getClaimPayoutFromReceipt(logs, USER)).toBe(payout.toString()); }); - it('returns null for undefined data', () => { - expect(decodeMerklClaimAmount(undefined)).toBeNull(); + it('ignores Transfer logs from other senders', () => { + const logs = [ + makeTransferLog(MUSD_TOKEN_ADDRESS, OTHER_ADDRESS, USER, 100n), + ]; + + expect(getClaimPayoutFromReceipt(logs, USER)).toBeNull(); + }); + + it('ignores Transfer logs to other recipients', () => { + const logs = [ + makeTransferLog( + MUSD_TOKEN_ADDRESS, + MERKL_DISTRIBUTOR_ADDRESS, + OTHER_ADDRESS, + 100n, + ), + ]; + + expect(getClaimPayoutFromReceipt(logs, USER)).toBeNull(); }); - it('returns null for invalid data', () => { - expect(decodeMerklClaimAmount('0xbaddata')).toBeNull(); + it('ignores Transfer logs from a different token', () => { + const logs = [ + makeTransferLog( + '0x0000000000000000000000000000000000000099', + MERKL_DISTRIBUTOR_ADDRESS, + USER, + 100n, + ), + ]; + + expect(getClaimPayoutFromReceipt(logs, USER)).toBeNull(); + }); + + it('returns null for undefined logs', () => { + expect(getClaimPayoutFromReceipt(undefined, USER)).toBeNull(); + }); + + it('returns null for empty logs', () => { + expect(getClaimPayoutFromReceipt([], USER)).toBeNull(); + }); + + it('returns null for undefined user address', () => { + const logs = [ + makeTransferLog( + MUSD_TOKEN_ADDRESS, + MERKL_DISTRIBUTOR_ADDRESS, + USER, + 100n, + ), + ]; + + expect(getClaimPayoutFromReceipt(logs, undefined)).toBeNull(); + }); + + it('picks the correct log among multiple logs', () => { + const payout = 42000000n; + const logs = [ + // Some unrelated log + { + address: '0x0000000000000000000000000000000000000001', + topics: ['0xabc'], + data: '0x00', + }, + // The actual mUSD transfer from distributor + makeTransferLog( + MUSD_TOKEN_ADDRESS, + MERKL_DISTRIBUTOR_ADDRESS, + USER, + payout, + ), + // Another unrelated transfer + makeTransferLog( + '0x0000000000000000000000000000000000000099', + MERKL_DISTRIBUTOR_ADDRESS, + USER, + 999n, + ), + ]; + + expect(getClaimPayoutFromReceipt(logs, USER)).toBe(payout.toString()); }); }); }); diff --git a/app/components/UI/Earn/utils/musd.ts b/app/components/UI/Earn/utils/musd.ts index bd4a138371e..37827e3afd7 100644 --- a/app/components/UI/Earn/utils/musd.ts +++ b/app/components/UI/Earn/utils/musd.ts @@ -14,7 +14,10 @@ import { MUSD_TOKEN_ADDRESS, } from '../constants/musd'; import { getClaimedAmountFromContract } from '../components/MerklRewards/merkl-client'; -import { DISTRIBUTOR_CLAIM_ABI } from '../components/MerklRewards/constants'; +import { + DISTRIBUTOR_CLAIM_ABI, + MERKL_DISTRIBUTOR_ADDRESS, +} from '../components/MerklRewards/constants'; /** * Parameters for checking if a transaction is a mUSD claim for the current view. @@ -74,7 +77,7 @@ export function isMusdClaimForCurrentView({ * Parameters for converting mUSD claim amount to user's currency. */ export interface ConvertMusdClaimParams { - /** Raw claim amount from decodeMerklClaimAmount (wei string) */ + /** Raw claim amount in wei string */ claimAmountRaw: string; /** Native-to-user-currency conversion rate (e.g., ETH to EUR) */ conversionRate: BigNumber | number; @@ -233,15 +236,73 @@ export function decodeMerklClaimParams( } } +// ERC-20 Transfer(address,address,uint256) event topic +const ERC20_TRANSFER_TOPIC = + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; + +/** + * Log entry from a transaction receipt. + * The `topics` field is typed as `string` in TransactionController types, + * but at runtime it's `string[]` (raw JSON-RPC response). + */ +interface ReceiptLog { + address?: string; + data?: string; + topics?: string | string[]; +} + /** - * Decode the claim amount from a Merkl claim transaction data. - * Convenience wrapper around decodeMerklClaimParams that returns only the amount. + * Extract the actual mUSD payout from a confirmed claim transaction's receipt logs. * - * @param data - The transaction data hex string - * @returns The first claim amount as a string (raw value, not adjusted for decimals), or null if decoding fails + * The Merkl distributor calls the mUSD token's `transfer`, which emits an + * ERC-20 `Transfer(from=distributor, to=user, amount)` event. The `amount` + * in this event is the real per-transaction payout (not the cumulative total + * stored in calldata). + * + * @param logs - Receipt logs from txReceipt.logs + * @param userAddress - The claiming user's address (to match the Transfer `to` field) + * @returns The payout amount as a raw decimal string, or null if not found */ -export function decodeMerklClaimAmount( - data: string | undefined, +export function getClaimPayoutFromReceipt( + logs: ReceiptLog[] | undefined, + userAddress: string | undefined, ): string | null { - return decodeMerklClaimParams(data)?.totalAmount ?? null; + if (!logs?.length || !userAddress) { + return null; + } + + for (const log of logs) { + const topics = normalizeTopics(log.topics); + if (!topics || topics.length < 3) continue; + + const isTransferEvent = topics[0]?.toLowerCase() === ERC20_TRANSFER_TOPIC; + const isFromDistributor = + addressFromTopic(topics[1]) === MERKL_DISTRIBUTOR_ADDRESS.toLowerCase(); + const isToUser = addressFromTopic(topics[2]) === userAddress.toLowerCase(); + const isMuSDToken = + log.address?.toLowerCase() === MUSD_TOKEN_ADDRESS.toLowerCase(); + + if (isTransferEvent && isFromDistributor && isToUser && isMuSDToken) { + const amount = log.data; + if (!amount) continue; + return BigInt(amount).toString(); + } + } + + return null; +} + +function normalizeTopics( + topics: string | string[] | undefined, +): string[] | null { + if (!topics) return null; + if (Array.isArray(topics)) return topics; + // Shouldn't happen at runtime, but guard against the type definition + return null; +} + +function addressFromTopic(topic: string | undefined): string | undefined { + if (!topic || topic.length < 42) return undefined; + // Topic is a 32-byte hex, address is the last 20 bytes + return `0x${topic.slice(-40)}`.toLowerCase(); } diff --git a/app/components/UI/TransactionElement/utils.js b/app/components/UI/TransactionElement/utils.js index 91ef0de57e8..aca525f6586 100644 --- a/app/components/UI/TransactionElement/utils.js +++ b/app/components/UI/TransactionElement/utils.js @@ -37,7 +37,7 @@ import { hasTransactionType } from '../../Views/confirmations/utils/transaction' import { BigNumber } from 'bignumber.js'; import { convertMusdClaimAmount, - decodeMerklClaimAmount, + getClaimPayoutFromReceipt, } from '../Earn/utils/musd'; const POSITIVE_TRANSFER_TRANSACTION_TYPES = [ @@ -829,7 +829,8 @@ function decodeMusdClaimTx(args) { const { tx: { txParams, - txParams: { from, gas, data }, + txParams: { from, gas }, + txReceipt, hash, }, txChainId, @@ -843,8 +844,7 @@ function decodeMusdClaimTx(args) { const totalGas = calculateTotalGas(txParams); const renderFrom = renderFullAddress(from); - // Decode the claim amount from transaction data - const claimAmountRaw = decodeMerklClaimAmount(data); + const claimAmountRaw = getClaimPayoutFromReceipt(txReceipt?.logs, from); // Calculate display values let renderClaimAmount = strings('transaction.value_not_available'); diff --git a/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.test.tsx b/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.test.tsx index 90b6b2d8642..a1e8beaa100 100644 --- a/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.test.tsx +++ b/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.test.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { Interface } from '@ethersproject/abi'; import renderWithProvider from '../../../../../../util/test/renderWithProvider'; import { useTransactionDetails } from '../../../hooks/activity/useTransactionDetails'; import { @@ -10,7 +9,8 @@ import { TransactionDetailsHero } from './transaction-details-hero'; import { merge } from 'lodash'; import { otherControllersMock } from '../../../__mocks__/controllers/other-controllers-mock'; import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance'; -import { DISTRIBUTOR_CLAIM_ABI } from '../../../../../UI/Earn/components/MerklRewards/constants'; +import { MERKL_DISTRIBUTOR_ADDRESS } from '../../../../../UI/Earn/components/MerklRewards/constants'; +import { MUSD_TOKEN_ADDRESS } from '../../../../../UI/Earn/constants/musd'; jest.mock('../../../hooks/activity/useTransactionDetails'); jest.mock('../../../hooks/tokens/useTokenWithBalance'); @@ -170,23 +170,32 @@ describe('TransactionDetailsHero', () => { it('renders claim amount for musdClaim with valid claim data', () => { const USER_ADDRESS = '0x1234567890123456789012345678901234567890'; - const TOKEN_ADDRESS = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; const claimAmount = '75500000'; // 75.5 mUSD (6 decimals) - const contractInterface = new Interface(DISTRIBUTOR_CLAIM_ABI); - const claimData = contractInterface.encodeFunctionData('claim', [ - [USER_ADDRESS], - [TOKEN_ADDRESS], - [claimAmount], - [[]], - ]); + const ERC20_TRANSFER_TOPIC = + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; + const mockLogs = [ + { + address: MUSD_TOKEN_ADDRESS, + data: '0x' + BigInt(claimAmount).toString(16).padStart(64, '0'), + topics: [ + ERC20_TRANSFER_TOPIC, + '0x000000000000000000000000' + + MERKL_DISTRIBUTOR_ADDRESS.slice(2).toLowerCase(), + '0x000000000000000000000000' + USER_ADDRESS.slice(2).toLowerCase(), + ], + }, + ]; useTransactionDetailsMock.mockReturnValue({ transactionMeta: { ...TRANSACTION_META_MOCK, type: TransactionType.musdClaim, txParams: { - data: claimData, + from: USER_ADDRESS, + }, + txReceipt: { + logs: mockLogs, }, } as unknown as TransactionMeta, }); diff --git a/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.tsx b/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.tsx index d69d8b83ef9..b0b102f9935 100644 --- a/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.tsx +++ b/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.tsx @@ -22,7 +22,7 @@ import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance'; import { BigNumber } from 'bignumber.js'; import { convertMusdClaimAmount, - decodeMerklClaimAmount, + getClaimPayoutFromReceipt, } from '../../../../../UI/Earn/utils/musd'; import { selectConversionRateByChainId, @@ -130,8 +130,13 @@ function useClaimAmount(): { amount: BigNumber | null; isConverted: boolean } { return { amount: null, isConverted: false }; } - const { data } = transactionMeta.txParams ?? {}; - const claimAmountRaw = decodeMerklClaimAmount(data as string); + const { from } = transactionMeta.txParams ?? {}; + const claimAmountRaw = getClaimPayoutFromReceipt( + transactionMeta.txReceipt?.logs as Parameters< + typeof getClaimPayoutFromReceipt + >[0], + from as string, + ); if (!claimAmountRaw) { return { amount: null, isConverted: false }; diff --git a/app/components/Views/confirmations/hooks/earn/useMerklClaimAmount.ts b/app/components/Views/confirmations/hooks/earn/useMerklClaimAmount.ts index 85ba6669569..a0fdb878ffa 100644 --- a/app/components/Views/confirmations/hooks/earn/useMerklClaimAmount.ts +++ b/app/components/Views/confirmations/hooks/earn/useMerklClaimAmount.ts @@ -10,6 +10,7 @@ import { useAsyncResult } from '../../../../hooks/useAsyncResult'; import { convertMusdClaimAmount, ConvertMusdClaimResult, + getClaimPayoutFromReceipt, getUnclaimedAmountForMerklClaimTx, } from '../../../../UI/Earn/utils/musd'; @@ -23,26 +24,44 @@ interface MerklClaimAmountResult { /** * Hook that computes the actual claimable (unclaimed) amount for a Merkl mUSD claim transaction. * - * The transaction calldata contains the cumulative total reward, not the per-claim payout. - * The Merkl Distributor contract computes: payout = totalAmount - alreadyClaimed. - * This hook reads the already-claimed amount from the contract and returns the unclaimed portion. + * For confirmed transactions: extracts the actual payout from the receipt's Transfer event logs. + * For pending transactions: computes payout = totalAmount - alreadyClaimed via contract call. */ const useMerklClaimAmount = ( transaction: TransactionMeta, conversionRate: BigNumber, usdConversionRate: number, ): MerklClaimAmountResult => { - const { chainId, txParams, type: transactionType } = transaction; + const { chainId, txParams, txReceipt, type: transactionType } = transaction; + // For confirmed txs, extract the actual payout from receipt Transfer logs (synchronous) + const receiptPayout = useMemo(() => { + if (transactionType !== TransactionType.musdClaim) return null; + return getClaimPayoutFromReceipt( + txReceipt?.logs as Parameters[0], + txParams?.from as string, + ); + }, [transactionType, txReceipt?.logs, txParams?.from]); + + // For pending txs (no receipt yet): compute payout = totalAmount - alreadyClaimed const { value: claimAmountResult, pending } = useAsyncResult(async () => { if (transactionType !== TransactionType.musdClaim) return null; + if (receiptPayout) return null; return getUnclaimedAmountForMerklClaimTx( txParams?.data as string | undefined, chainId as Hex, ); - }, [transactionType, txParams?.data, chainId]); + }, [transactionType, txParams?.data, chainId, receiptPayout]); const claimAmount = useMemo(() => { + if (receiptPayout) { + return convertMusdClaimAmount({ + claimAmountRaw: receiptPayout, + conversionRate, + usdConversionRate, + }); + } + if (pending || !claimAmountResult) return null; return convertMusdClaimAmount({ @@ -50,9 +69,15 @@ const useMerklClaimAmount = ( conversionRate, usdConversionRate, }); - }, [pending, claimAmountResult, conversionRate, usdConversionRate]); + }, [ + receiptPayout, + pending, + claimAmountResult, + conversionRate, + usdConversionRate, + ]); - return { pending, claimAmount }; + return { pending: !receiptPayout && pending, claimAmount }; }; export default useMerklClaimAmount; From e7e91093a9c81b49d0c570c36557efbd37153e84 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:57:50 +0100 Subject: [PATCH 013/131] chore(runway): cherry-pick chore: allow list audit finding GHSA-378v-28hj-76wf cp-7.66.0 (#26427) - chore: allow list audit finding GHSA-378v-28hj-76wf cp-7.66.0 (#26386) add 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 ## **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** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Configuration-only change to suppress a specific audit warning; no runtime logic changes, but it reduces audit signal for this known issue. > > **Overview** > Updates Yarn config to ignore the `bn.js` npm audit advisory `GHSA-378v-28hj-76wf` (ID `1113402`) in `npmAuditIgnoreAdvisories`, to unblock CI until a fix is available. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6560f97da7476a9d8e49ee19c92c9572374178aa. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [ebce642](https://github.com/MetaMask/metamask-mobile/commit/ebce642cc3a9add456b742bc0f3ae86fc1b391a1) Co-authored-by: sethkfman <10342624+sethkfman@users.noreply.github.com> --- .yarnrc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.yarnrc.yml b/.yarnrc.yml index 4328c89b949..4124447b739 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -13,6 +13,7 @@ plugins: npmAuditIgnoreAdvisories: - 1109627 # TODO: Upgrade @react-native-community/cli to 17.0.1+ when ready. Suppressing for now to unblock CI. - 1112455 # lodash prototype pollution in _.unset and _.omit. No fix available yet (latest is 4.17.21, affected <=4.17.22). Suppressing for now to unblock CI. https://github.com/advisories/GHSA-xxjr-mmjv-4gpg + - 1113402 # 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 yarnPath: .yarn/releases/yarn-4.10.3.cjs From f694aa8ae83628f4b98fdbd15e1f431777ee4ce9 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:41:21 +0100 Subject: [PATCH 014/131] chore(runway): cherry-pick chore(predict): cp-7.66.0 remove code related to super bowl banner on carousel (#26375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chore(predict): cp-7.66.0 remove code related to super bowl banner on carousel (#26363) ## **Description** The Carousel previously had special handling for the Predict Superbowl banner: it could replace the entire carousel with a single `PredictMarketSportCardWrapper` when a Superbowl slide with a `marketId` was present, and hid that slide from the normal carousel. This change removes that integration. **What changed:** - **Carousel (`index.tsx`):** Removed Predict/Superbowl imports, the `predictSuperbowlSlide` and `predictSuperbowlMarketId` memos, the early return that rendered `PredictMarketSportCardWrapper`, the filter that excluded `PREDICT_SUPERBOWL_VARIABLE_NAME` from visible slides, and the `handleSportCardDismiss` / `handlePredictSuperbowlLoad` callbacks (including "Banner Display" tracking for the Superbowl card). - **Tests (`index.test.tsx`):** Removed the `PredictMarketSportCardWrapper` mock, the `PREDICT_SUPERBOWL_VARIABLE_NAME` import, and the entire "Carousel Predict Superbowl Integration" describe block and its five tests (render with/without marketId, props, tracking). The Carousel no longer has any Superbowl-specific behavior; any such slides from Contentful would now be treated as normal carousel slides. ## **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. ## **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** > Removes a time-bound/feature-specific code path and its tests without touching core navigation or data handling; main risk is unintended behavior if Contentful still serves Superbowl slides. > > **Overview** > Removes the Carousel’s special-case Predict Superbowl behavior so it no longer replaces the carousel with a `PredictMarketSportCardWrapper` when a Superbowl slide is present. > > This deletes Superbowl-specific slide detection/metadata handling, explicit filtering of the Superbowl slide from `visibleSlides`, and related dismiss/load callbacks + “Banner Display” tracking. The test suite is updated by dropping the Predict Superbowl mock and all associated integration tests. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4bde7239443eeaf7e33109fd6266f69f68596841. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [6eb88ac](https://github.com/MetaMask/metamask-mobile/commit/6eb88aceb7658030a2acf804be365c55929734df) Co-authored-by: Caainã Jeronimo --- app/components/UI/Carousel/index.test.tsx | 134 ---------------------- app/components/UI/Carousel/index.tsx | 58 ---------- 2 files changed, 192 deletions(-) diff --git a/app/components/UI/Carousel/index.test.tsx b/app/components/UI/Carousel/index.test.tsx index 624ef385857..b4dc6202023 100644 --- a/app/components/UI/Carousel/index.test.tsx +++ b/app/components/UI/Carousel/index.test.tsx @@ -23,7 +23,6 @@ import Routes from '../../../constants/navigation/Routes'; import { WalletClientType } from '../../../core/SnapKeyring/MultichainWalletSnapClient'; import { SolScope } from '@metamask/keyring-api'; import { setContentPreviewToken } from '../../../actions/notification/helpers'; -import { PREDICT_SUPERBOWL_VARIABLE_NAME } from '../Predict/constants/carousel'; const makeMockState = () => ({ @@ -97,32 +96,6 @@ jest.mock('./fetchCarouselSlidesFromContentful', () => ({ fetchCarouselSlidesFromContentful: jest.fn(), })); -jest.mock('../Predict/components/PredictMarketSportCard', () => { - const { useEffect } = jest.requireActual('react'); - const { View, Text } = jest.requireActual('react-native'); - return { - PredictMarketSportCardWrapper: function MockPredictMarketSportCardWrapper({ - marketId, - testID, - onLoad, - }: { - marketId: string; - testID?: string; - onLoad?: () => void; - }) { - useEffect(() => { - onLoad?.(); - }, [onLoad]); - - return ( - - {marketId} - - ); - }, - }; -}); - const mockDispatch = jest.fn(); const mockFetchCarouselSlides = jest.mocked(fetchCarouselSlidesFromContentful); @@ -514,110 +487,3 @@ describe('useFetchCarouselSlides()', () => { expect(mockFetchCarouselSlides).not.toHaveBeenCalled(); }); }); - -describe('Carousel Predict Superbowl Integration', () => { - it('renders PredictMarketSportCardWrapper for predict-superbowl slides', async () => { - const predictSlide = createMockSlide({ - id: 'predict-superbowl-slide', - variableName: PREDICT_SUPERBOWL_VARIABLE_NAME, - metadata: { marketId: 'test-market-123' }, - }); - mockFetchCarouselSlides.mockResolvedValue({ - prioritySlides: [], - regularSlides: [predictSlide], - }); - - const { findByTestId } = render(); - - const marketIdElement = await findByTestId('predict-sport-card-market-id'); - expect(marketIdElement).toHaveTextContent('test-market-123'); - }); - - it('does not render PredictMarketSportCardWrapper when metadata is missing marketId', async () => { - const regularSlide = createMockSlide({ id: 'regular-slide' }); - const predictSlide = createMockSlide({ - id: 'predict-superbowl-slide', - variableName: PREDICT_SUPERBOWL_VARIABLE_NAME, - metadata: undefined, - }); - mockFetchCarouselSlides.mockResolvedValue({ - prioritySlides: [], - regularSlides: [predictSlide, regularSlide], - }); - - const { findByTestId, queryByTestId } = render(); - - await findByTestId('carousel-slide-regular-slide'); - - expect(queryByTestId('predict-sport-card-wrapper')).toBeNull(); - }); - - it('does not render PredictMarketSportCardWrapper when marketId is empty', async () => { - const regularSlide = createMockSlide({ id: 'regular-slide' }); - const predictSlide = createMockSlide({ - id: 'predict-superbowl-slide', - variableName: PREDICT_SUPERBOWL_VARIABLE_NAME, - metadata: { marketId: '' }, - }); - mockFetchCarouselSlides.mockResolvedValue({ - prioritySlides: [], - regularSlides: [predictSlide, regularSlide], - }); - - const { findByTestId, queryByTestId } = render(); - - await findByTestId('carousel-slide-regular-slide'); - - expect(queryByTestId('predict-sport-card-wrapper')).toBeNull(); - }); - - it('passes correct props to PredictMarketSportCardWrapper', async () => { - const predictSlide = createMockSlide({ - id: 'predict-superbowl-slide', - variableName: PREDICT_SUPERBOWL_VARIABLE_NAME, - metadata: { marketId: 'market-abc-123' }, - testID: 'custom-test-id', - }); - mockFetchCarouselSlides.mockResolvedValue({ - prioritySlides: [predictSlide], - regularSlides: [], - }); - - const { findByTestId } = render(); - - const wrapper = await findByTestId('custom-test-id'); - expect(wrapper).toBeOnTheScreen(); - - const marketId = await findByTestId('predict-sport-card-market-id'); - expect(marketId).toHaveTextContent('market-abc-123'); - }); - - it('fires Banner Display tracking event for predict-superbowl slide', async () => { - mockTrackEvent.mockClear(); - mockCreateEventBuilder.mockClear(); - const predictSlide = createMockSlide({ - id: 'predict-superbowl-slide', - variableName: PREDICT_SUPERBOWL_VARIABLE_NAME, - metadata: { marketId: 'test-market-123' }, - }); - mockFetchCarouselSlides.mockResolvedValue({ - prioritySlides: [], - regularSlides: [predictSlide], - }); - - const { findByTestId } = render(); - - await findByTestId('predict-sport-card-market-id'); - - await waitFor(() => { - expect(mockCreateEventBuilder).toHaveBeenCalledWith({ - category: 'Banner Display', - properties: { - name: PREDICT_SUPERBOWL_VARIABLE_NAME, - }, - }); - }); - - expect(mockTrackEvent).toHaveBeenCalled(); - }); -}); diff --git a/app/components/UI/Carousel/index.tsx b/app/components/UI/Carousel/index.tsx index 92cbdb7a299..af76f168c6f 100644 --- a/app/components/UI/Carousel/index.tsx +++ b/app/components/UI/Carousel/index.tsx @@ -45,10 +45,6 @@ import { subscribeToContentPreviewToken } from '../../../actions/notification/he import SharedDeeplinkManager from '../../../core/DeeplinkManager/DeeplinkManager'; import { isInternalDeepLink } from '../../../core/DeeplinkManager/util/deeplinks'; import AppConstants from '../../../core/AppConstants'; -import { PredictMarketSportCardWrapper } from '../Predict/components/PredictMarketSportCard'; -import { PredictEventValues } from '../Predict/constants/eventNames'; -import { PREDICT_SUPERBOWL_VARIABLE_NAME } from '../Predict/constants/carousel'; -import { PredictCarouselMetadata } from '../Predict/types'; const MAX_CAROUSEL_SLIDES = 8; @@ -276,24 +272,6 @@ const CarouselComponent: FC = ({ style, onEmptyState }) => { dismissedBanners, ]); - const predictSuperbowlSlide = useMemo( - () => - slidesConfig.find( - (slide) => - slide.variableName === PREDICT_SUPERBOWL_VARIABLE_NAME && - !dismissedBanners.includes(slide.id), - ), - [slidesConfig, dismissedBanners], - ); - - const predictSuperbowlMarketId = useMemo(() => { - if (!predictSuperbowlSlide) return null; - const metadata = predictSuperbowlSlide.metadata as - | PredictCarouselMetadata - | undefined; - return metadata?.marketId ?? null; - }, [predictSuperbowlSlide]); - const visibleSlides = useMemo(() => { const filtered = slidesConfig.filter((slide: CarouselSlide) => { const active = isActive(slide); @@ -308,11 +286,6 @@ const CarouselComponent: FC = ({ style, onEmptyState }) => { } ///: END:ONLY_INCLUDE_IF - // We dont want to show the predict superbowl slide in the carousel - if (slide.variableName === PREDICT_SUPERBOWL_VARIABLE_NAME) { - return false; - } - return !dismissedBanners.includes(slide.id); }); @@ -564,11 +537,6 @@ const CarouselComponent: FC = ({ style, onEmptyState }) => { } }, [transitionToEmpty, onEmptyState]); - const handleSportCardDismiss = useCallback(() => { - if (!predictSuperbowlSlide) return; - dispatch(dismissBanner(predictSuperbowlSlide.id)); - }, [predictSuperbowlSlide, dispatch]); - const renderCard = useCallback( (slide: CarouselSlide, isCurrentCard: boolean) => { const isEmptyCard = slide.variableName === 'empty'; @@ -649,32 +617,6 @@ const CarouselComponent: FC = ({ style, onEmptyState }) => { } }, [currentSlide, trackEvent, createEventBuilder]); - const handlePredictSuperbowlLoad = useCallback(() => { - if (predictSuperbowlSlide) { - trackEvent( - createEventBuilder({ - category: 'Banner Display', - properties: { - name: - predictSuperbowlSlide.variableName ?? predictSuperbowlSlide.id, - }, - }).build(), - ); - } - }, [predictSuperbowlSlide, trackEvent, createEventBuilder]); - - if (predictSuperbowlMarketId) { - return ( - - ); - } - if ( !isCarouselVisible || (visibleSlides.length === 0 && !isAnimating.current) From a88e9a25251aade0b748c8e3d19aa275865c2d2b Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:12:23 +0100 Subject: [PATCH 015/131] chore(runway): cherry-pick feat: cp-7.66.0 MUSD-357 added musd_conversion and musd_claim transaction types to transaction-controller metrics_properties (#26433) - feat: cp-7.66.0 MUSD-357 added musd_conversion and musd_claim transaction types to transaction-controller metrics_properties (#26383) ## **Description** Added musd_conversion and musd_claim transaction types to transaction-controller metrics_properties ## **Changelog** CHANGELOG entry: added musd_conversion and musd_claim transaction types to transaction-controller metrics_properties ## **Related issues** Fixes: [MUSD-357: Add musdConversion and musdClaim Transaction Types to "Transaction *" Events](https://consensyssoftware.atlassian.net/browse/MUSD-357) ## **Manual testing steps** ```gherkin Feature: mUSD transaction analytics classification Scenario: user submits mUSD conversion or claim transaction Given user has an mUSD conversion or mUSD claim transaction in the wallet flow When the wallet emits general transaction lifecycle events Then analytics include a specific transaction_type value And transaction_type is "musd_conversion" for conversion transactions And transaction_type is "musd_claim" for claim transactions ``` ## **Screenshots/Recordings** ### **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 - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Small, additive change limited to analytics `transaction_type` labeling plus test coverage; low risk aside from potential downstream dashboard/event-name expectations. > > **Overview** > Adds analytics classification for nested mUSD transactions by mapping `TransactionType.musdConversion` and `TransactionType.musdClaim` to `transaction_type` values `musd_conversion` and `musd_claim` in `getTransactionTypeValue`. > > Extends the existing parameterized test to cover these new nested transaction type mappings. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3aa4ee511be14996431962a3832ac1d76a21483a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [435a464](https://github.com/MetaMask/metamask-mobile/commit/435a464c0ae5556fca3569afb62a1ae549e3f7be) Co-authored-by: Matthew Grainger <46547583+Matt561@users.noreply.github.com> --- .../metrics_properties/base.test.ts | 2 ++ .../transaction-controller/metrics_properties/base.ts | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/app/core/Engine/controllers/transaction-controller/metrics_properties/base.test.ts b/app/core/Engine/controllers/transaction-controller/metrics_properties/base.test.ts index 116ace0682d..e00d27c44e3 100644 --- a/app/core/Engine/controllers/transaction-controller/metrics_properties/base.test.ts +++ b/app/core/Engine/controllers/transaction-controller/metrics_properties/base.test.ts @@ -101,6 +101,8 @@ describe('getTransactionTypeValue', () => { ['predict_claim', TransactionType.predictClaim], ['predict_deposit', TransactionType.predictDeposit], ['predict_withdraw', TransactionType.predictWithdraw], + ['musd_conversion', TransactionType.musdConversion], + ['musd_claim', TransactionType.musdClaim], ])('returns %s if nested transaction type is %s', (expected, nestedType) => { const mockTransactionMeta = { type: TransactionType.simpleSend, diff --git a/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts b/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts index ee90992dc6a..0295e1441b8 100644 --- a/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts +++ b/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts @@ -70,6 +70,14 @@ export function getTransactionTypeValue( return 'predict_claim'; } + if (hasTransactionType(transactionMeta, [TransactionType.musdConversion])) { + return 'musd_conversion'; + } + + if (hasTransactionType(transactionMeta, [TransactionType.musdClaim])) { + return 'musd_claim'; + } + switch (transactionType) { case TransactionType.bridgeApproval: return 'bridge_approval'; From f3e85e0bb814d0dce38b825561d5adf1bab53de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:20:56 +0000 Subject: [PATCH 016/131] Revert "fix: MUL-1331 modify android manifest file for correct BLE location permission. (#23759)" This reverts commit ddd333ce68f2bd948326ba63f226e79a442c231e. --- android/app/src/main/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 845d99f6471..e9e91ece731 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,12 +12,12 @@ - + - + From 6765ad5b24d72eeaead09cbbfc5981819cca3cbb Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Fri, 20 Feb 2026 18:56:00 +0000 Subject: [PATCH 017/131] feat: Fix for pull to refresh gesture bug --- .../BrowserTab/GestureWebViewWrapper.test.tsx | 229 +++++++++++++++++- .../BrowserTab/GestureWebViewWrapper.tsx | 68 ++++-- app/components/Views/BrowserTab/constants.ts | 8 + 3 files changed, 283 insertions(+), 22 deletions(-) diff --git a/app/components/Views/BrowserTab/GestureWebViewWrapper.test.tsx b/app/components/Views/BrowserTab/GestureWebViewWrapper.test.tsx index 347a51e67b3..3e9461445ac 100644 --- a/app/components/Views/BrowserTab/GestureWebViewWrapper.test.tsx +++ b/app/components/Views/BrowserTab/GestureWebViewWrapper.test.tsx @@ -18,6 +18,7 @@ import { PULL_THRESHOLD, SCROLL_TOP_THRESHOLD, PULL_ACTIVATION_ZONE, + PULL_MOVE_ACTIVATION, } from './constants'; import { GestureWebViewWrapper, @@ -28,6 +29,8 @@ import { type GestureCallback = (...args: unknown[]) => void; const capturedCallbacks: { onTouchesDown?: GestureCallback; + onTouchesMove?: GestureCallback; + onTouchesUp?: GestureCallback; onUpdate?: GestureCallback; onEnd?: GestureCallback; onFinalize?: GestureCallback; @@ -38,6 +41,8 @@ const capturedCallbacks: { interface MockPanGestureType { manualActivation: jest.Mock; onTouchesDown: jest.Mock; + onTouchesMove: jest.Mock; + onTouchesUp: jest.Mock; onUpdate: jest.Mock; onEnd: jest.Mock; onFinalize: jest.Mock; @@ -54,6 +59,20 @@ const mockPanGesture: MockPanGestureType = { capturedCallbacks.onTouchesDown = callback; return this; }), + onTouchesMove: jest.fn(function ( + this: MockPanGestureType, + callback: GestureCallback, + ) { + capturedCallbacks.onTouchesMove = callback; + return this; + }), + onTouchesUp: jest.fn(function ( + this: MockPanGestureType, + callback: GestureCallback, + ) { + capturedCallbacks.onTouchesUp = callback; + return this; + }), onUpdate: jest.fn(function ( this: MockPanGestureType, callback: GestureCallback, @@ -82,6 +101,8 @@ jest.mock('react-native-gesture-handler', () => ({ Pan: jest.fn(() => mockPanGesture), Native: jest.fn(() => ({})), Race: jest.fn((...gestures) => gestures[0]), + Exclusive: jest.fn((...gestures) => gestures[0]), + Simultaneous: jest.fn((...gestures) => gestures[0]), }, GestureDetector: ({ children }: { children: React.ReactNode }) => children, })); @@ -259,12 +280,12 @@ describe('GestureWebViewWrapper', () => { expect(Gesture.Native).toHaveBeenCalled(); }); - it('combines gestures with Race', () => { + it('combines gestures with Simultaneous', () => { const { Gesture } = jest.requireMock('react-native-gesture-handler'); renderComponent(); - expect(Gesture.Race).toHaveBeenCalled(); + expect(Gesture.Simultaneous).toHaveBeenCalled(); }); it('sets up onTouchesDown handler', () => { @@ -273,6 +294,18 @@ describe('GestureWebViewWrapper', () => { expect(mockPanGesture.onTouchesDown).toHaveBeenCalled(); }); + it('sets up onTouchesMove handler', () => { + renderComponent(); + + expect(mockPanGesture.onTouchesMove).toHaveBeenCalled(); + }); + + it('sets up onTouchesUp handler', () => { + renderComponent(); + + expect(mockPanGesture.onTouchesUp).toHaveBeenCalled(); + }); + it('sets up onUpdate handler', () => { renderComponent(); @@ -377,6 +410,8 @@ describe('GestureWebViewWrapper', () => { beforeEach(() => { // Clear captured callbacks before each test capturedCallbacks.onTouchesDown = undefined; + capturedCallbacks.onTouchesMove = undefined; + capturedCallbacks.onTouchesUp = undefined; capturedCallbacks.onUpdate = undefined; capturedCallbacks.onEnd = undefined; capturedCallbacks.onFinalize = undefined; @@ -507,8 +542,8 @@ describe('GestureWebViewWrapper', () => { }); }); - describe('pull-to-refresh touch', () => { - it('calls stateManager.activate for top zone touch when at scroll top', () => { + describe('pull-to-refresh touch (deferred activation)', () => { + it('defers activation for top zone touch when at scroll top (pending_refresh)', () => { const scrollY = mockSharedValue(0); const isRefreshing = mockSharedValue(false); renderComponent({ scrollY, isRefreshing }); @@ -517,9 +552,109 @@ describe('GestureWebViewWrapper', () => { capturedCallbacks.onTouchesDown?.(event, stateManager); + expect(stateManager.activate).not.toHaveBeenCalled(); + expect(stateManager.fail).not.toHaveBeenCalled(); + }); + + it('activates after sufficient downward movement in onTouchesMove', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + expect(stateManager.activate).not.toHaveBeenCalled(); + + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + expect(stateManager.activate).toHaveBeenCalled(); }); + it('fails on tap (finger lifts without significant movement)', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + expect(stateManager.activate).not.toHaveBeenCalled(); + expect(stateManager.fail).not.toHaveBeenCalled(); + + capturedCallbacks.onTouchesUp?.({}, stateManager); + + expect(stateManager.fail).toHaveBeenCalled(); + expect(stateManager.activate).not.toHaveBeenCalled(); + }); + + it('fails on horizontal movement (not a pull)', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + + const moveEvent = { + allTouches: [{ x: 200 + PULL_MOVE_ACTIVATION + 5, y: 25 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + + expect(stateManager.fail).toHaveBeenCalled(); + expect(stateManager.activate).not.toHaveBeenCalled(); + }); + + it('fails on upward movement (not a pull-down)', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + + const moveEvent = { + allTouches: [{ x: 200, y: 25 - PULL_MOVE_ACTIVATION - 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + + expect(stateManager.fail).toHaveBeenCalled(); + expect(stateManager.activate).not.toHaveBeenCalled(); + }); + + it('keeps waiting when movement is below threshold', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + + const moveEvent = { + allTouches: [{ x: 202, y: 28 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + + expect(stateManager.activate).not.toHaveBeenCalled(); + expect(stateManager.fail).not.toHaveBeenCalled(); + }); + it('calls stateManager.fail for top zone touch when already refreshing', () => { const scrollY = mockSharedValue(0); const isRefreshing = mockSharedValue(true); @@ -555,6 +690,42 @@ describe('GestureWebViewWrapper', () => { expect(stateManager.fail).toHaveBeenCalled(); }); + + it('fails in onTouchesMove when no touches in event', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + + const moveEvent = { allTouches: [] }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + + expect(stateManager.fail).toHaveBeenCalled(); + }); + + it('onTouchesMove returns early when gestureType is not pending_refresh', () => { + renderComponent({ backEnabled: true }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(10, 200), + stateManager, + ); + expect(stateManager.activate).toHaveBeenCalled(); + + stateManager.activate.mockClear(); + stateManager.fail.mockClear(); + const moveEvent = { allTouches: [{ x: 10, y: 220 }] }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + + expect(stateManager.activate).not.toHaveBeenCalled(); + expect(stateManager.fail).not.toHaveBeenCalled(); + }); }); describe('onUpdate behavior', () => { @@ -674,18 +845,25 @@ describe('GestureWebViewWrapper', () => { capturedCallbacks.onFinalize?.(); }); - it('completes refresh gesture flow: touchDown -> update -> end', () => { + it('completes refresh gesture flow: touchDown -> touchesMove -> update -> end', () => { const scrollY = mockSharedValue(0); const isRefreshing = mockSharedValue(false); const onReload = jest.fn(); renderComponent({ scrollY, isRefreshing, onReload }); const stateManager = createStateManager(); - // Touch down in pull zone + // Touch down in pull zone (deferred) capturedCallbacks.onTouchesDown?.( createTouchEvent(200, 25), stateManager, ); + expect(stateManager.activate).not.toHaveBeenCalled(); + + // Move down to activate + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); expect(stateManager.activate).toHaveBeenCalled(); // Update with positive Y translation (pulling down) @@ -744,6 +922,13 @@ describe('GestureWebViewWrapper', () => { createTouchEvent(200, 25), stateManager, ); + + // Move down to activate first + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + capturedCallbacks.onUpdate?.(createUpdateEvent(0, 30)); // Small pull capturedCallbacks.onEnd?.(createEndEvent(0, 30)); // Below threshold }); @@ -799,6 +984,11 @@ describe('GestureWebViewWrapper', () => { createTouchEvent(200, 25), stateManager, ); + // Activate via onTouchesMove first + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); capturedCallbacks.onUpdate?.(createUpdateEvent(0, -10)); // Upward movement }); @@ -812,6 +1002,11 @@ describe('GestureWebViewWrapper', () => { createTouchEvent(200, 25), stateManager, ); + // Activate via onTouchesMove first + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); capturedCallbacks.onUpdate?.(createUpdateEvent(0, 150)); // Large pull (1.5x threshold) }); }); @@ -873,17 +1068,24 @@ describe('GestureWebViewWrapper', () => { }); describe('handleRefresh callback', () => { - it('executes refresh flow without errors', () => { + it('executes refresh flow without errors (with deferred activation)', () => { const scrollY = mockSharedValue(0); const isRefreshing = mockSharedValue(false); renderComponent({ scrollY, isRefreshing }); const stateManager = createStateManager(); - // Touch down in pull zone + // Touch down in pull zone (deferred) capturedCallbacks.onTouchesDown?.( createTouchEvent(200, 25), stateManager, ); + expect(stateManager.activate).not.toHaveBeenCalled(); + + // Move down to activate + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); expect(stateManager.activate).toHaveBeenCalled(); // Pull down enough to trigger refresh (threshold is 80px) @@ -1080,6 +1282,17 @@ describe('GestureWebViewWrapper constants', () => { expect(PULL_ACTIVATION_ZONE).toBe(50); }); }); + + describe('PULL_MOVE_ACTIVATION', () => { + it('returns positive pixel value for pull movement threshold', () => { + expect(PULL_MOVE_ACTIVATION).toBeGreaterThan(0); + expect(typeof PULL_MOVE_ACTIVATION).toBe('number'); + }); + + it('uses 10 pixels as pull movement activation threshold', () => { + expect(PULL_MOVE_ACTIVATION).toBe(10); + }); + }); }); describe('GestureWebViewWrapper gesture zone calculations', () => { diff --git a/app/components/Views/BrowserTab/GestureWebViewWrapper.tsx b/app/components/Views/BrowserTab/GestureWebViewWrapper.tsx index 27c39e1b47d..2902c525504 100644 --- a/app/components/Views/BrowserTab/GestureWebViewWrapper.tsx +++ b/app/components/Views/BrowserTab/GestureWebViewWrapper.tsx @@ -18,6 +18,7 @@ import { PULL_THRESHOLD, SCROLL_TOP_THRESHOLD, PULL_ACTIVATION_ZONE, + PULL_MOVE_ACTIVATION, } from './constants'; const styles = StyleSheet.create({ @@ -65,8 +66,10 @@ export interface GestureWebViewWrapperProps { * - Right edge swipe: Navigate forward * - Pull-to-refresh: Reload page (when scrolled to top) * - * Uses Gesture.Race with manualActivation(true) to coordinate gestures with WebView's - * native touch handling. + * Uses Gesture.Simultaneous with manualActivation(true) to coordinate gestures with + * WebView's native touch handling. Simultaneous allows both our Pan and the Native + * gesture to coexist so that taps pass through while pull-to-refresh uses deferred + * activation. */ export const GestureWebViewWrapper: React.FC = ({ isTabActive, @@ -90,12 +93,14 @@ export const GestureWebViewWrapper: React.FC = ({ const pullProgress = useSharedValue(0); const isPulling = useSharedValue(false); const pullHapticTriggered = useSharedValue(false); + const initialTouchY = useSharedValue(0); + const initialTouchX = useSharedValue(0); const swipeStartTime = useRef(0); const swipeProgress = useSharedValue(0); const swipeDirection = useSharedValue<'back' | 'forward' | null>(null); - const gestureType = useSharedValue<'back' | 'forward' | 'refresh' | null>( - null, - ); + const gestureType = useSharedValue< + 'back' | 'forward' | 'refresh' | 'pending_refresh' | null + >(null); // Navigation state as shared values - prevents stale reads in worklets // These are synced from props to ensure real-time access in UI thread @@ -244,19 +249,53 @@ export const GestureWebViewWrapper: React.FC = ({ stateManager.activate(); runOnJS(triggerHapticFeedback)(ImpactFeedbackStyle.Light); } else if (canPullToRefresh) { + gestureType.value = 'pending_refresh'; + initialTouchY.value = y; + initialTouchX.value = x; + } else { + stateManager.fail(); + } + }) + .onTouchesMove((event, stateManager) => { + 'worklet'; + if (gestureType.value !== 'pending_refresh') return; + + const touch = event.allTouches[0]; + if (!touch) { + gestureType.value = null; + stateManager.fail(); + return; + } + + const deltaY = touch.y - initialTouchY.value; + const deltaX = Math.abs(touch.x - initialTouchX.value); + + if (deltaY > PULL_MOVE_ACTIVATION && deltaY > deltaX) { gestureType.value = 'refresh'; isPulling.value = true; pullProgress.value = 0; pullHapticTriggered.value = false; stateManager.activate(); - } else { + } else if ( + deltaX > PULL_MOVE_ACTIVATION || + deltaY < -PULL_MOVE_ACTIVATION + ) { + gestureType.value = null; + stateManager.fail(); + } + }) + .onTouchesUp((_event, stateManager) => { + 'worklet'; + if (gestureType.value === 'pending_refresh') { + gestureType.value = null; stateManager.fail(); } }) .onUpdate((event) => { 'worklet'; const currentGestureType = gestureType.value; - if (!currentGestureType) return; + if (!currentGestureType || currentGestureType === 'pending_refresh') + return; const swipeDistance = screenWidth * SWIPE_THRESHOLD; @@ -297,8 +336,7 @@ export const GestureWebViewWrapper: React.FC = ({ .onEnd((event) => { 'worklet'; const currentGestureType = gestureType.value; - if (!currentGestureType) { - // Reset animations directly on UI thread + if (!currentGestureType || currentGestureType === 'pending_refresh') { swipeProgress.value = withTiming(0, { duration: 200 }); swipeDirection.value = null; pullProgress.value = withTiming(0, { duration: 200 }); @@ -311,14 +349,12 @@ export const GestureWebViewWrapper: React.FC = ({ if (currentGestureType === 'back') { if (event.translationX >= swipeDistance) { - // Only runOnJS for callbacks that need JS thread (navigation, analytics) runOnJS(handleSwipeNavigation)( 'back', event.translationX, duration, ); } - // Reset swipe animation directly on UI thread swipeProgress.value = withTiming(0, { duration: 200 }); swipeDirection.value = null; } else if (currentGestureType === 'forward') { @@ -336,7 +372,6 @@ export const GestureWebViewWrapper: React.FC = ({ pullDistanceRef.current = event.translationY; runOnJS(handleRefresh)(); } - // Reset pull animation directly on UI thread pullProgress.value = withTiming(0, { duration: 200 }); isPulling.value = false; } @@ -367,10 +402,15 @@ export const GestureWebViewWrapper: React.FC = ({ ); /** - * Combined gesture - uses Race so our gesture takes priority when activated + * Combined gesture - Simultaneous allows both our Pan and the WebView's Native + * gesture to coexist. This is critical for deferred pull-to-refresh activation: + * while Pan is in BEGAN (deciding if touch is a tap vs pull), Native independently + * handles the touch so taps pass through to the WebView. When Pan activates for + * a real pull, both gestures are active but since we're at the top of the page, + * the WebView has nothing to scroll. */ const combinedWebViewGesture = useMemo( - () => Gesture.Race(fullyUnifiedGesture, webViewNativeGesture), + () => Gesture.Simultaneous(fullyUnifiedGesture, webViewNativeGesture), [webViewNativeGesture, fullyUnifiedGesture], ); diff --git a/app/components/Views/BrowserTab/constants.ts b/app/components/Views/BrowserTab/constants.ts index 4159141f42b..f5e63cc0edc 100644 --- a/app/components/Views/BrowserTab/constants.ts +++ b/app/components/Views/BrowserTab/constants.ts @@ -49,3 +49,11 @@ export const SCROLL_TOP_THRESHOLD = 5; * WebView interactions (tapping links, buttons, etc.). */ export const PULL_ACTIVATION_ZONE = 50; + +/** + * Minimum downward movement in pixels before activating pull-to-refresh. + * This prevents taps on buttons near the top of the page from being + * swallowed by the gesture handler. The gesture stays in a "pending" + * state until the finger moves down by at least this amount. + */ +export const PULL_MOVE_ACTIVATION = 10; From e372c9c22c45073982bc237ccfcbd11a48182815 Mon Sep 17 00:00:00 2001 From: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:33:54 -0330 Subject: [PATCH 018/131] release: release/7.66.0-Changelog (#26044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates the change log for 7.66.0. (Hotfix - no test plan generated.) Co-authored-by: metamaskbot Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> --- CHANGELOG.md | 260 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 259 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f7f648bd64..2643b348f2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,262 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.66.0] + +### Added + +- Adds a page for changing preferred ramp provider (#25860) +- Add asset overview deeplinks (#25447) +- Restored the previously selected "Pay with" token when returning to the Perps order view within 5 minutes. (#25938) +- Fixed predict transaction toast notifications not appearing when navigating away from the Predict tab (#25863) +- Added new Accounts Menu screen to organize settings navigation with Settings, Manage, and Resources sections (#25611) +- Adds Bridge and Swap feature to `MegaETH` (#25906) +- Adds chiliz.png as network logo and enables it in metamask mobile (#25437) +- Always display learn more about perps link (#25958) +- Created new token list item v2 (#25824) +- Added custom claim transaction request screen for mUSD bonus claims with improved UX flow (#25837) +- Added an "Ending soon" tab to prediction markets feed showing markets sorted by end date (#25868) +- Removed legacy homepage script injection and related RPC methods (#25620) +- Add google/web search inside browser search bar (#25897) +- Homogenize spacing on Explore page for perps items (#25894) +- Added 1st interaction alert to warn users when interacting with an address for the first time. (#25575) +- Added icons to the bridge token selector network pills (#25851) +- Create feature flag for the new unified assets state (#25891) +- Adds Bridge and Swap feature to HyperEVM (#25769) +- Added lightweight position display and one-click Long/Short trading on token details page for perps-enabled assets (#25685) +- Improved browser tab switching performance by keeping tabs mounted (#25702) +- Validation errors from non-EVM transaction snaps will now be displayed to users during send flow. (#25648) +- Added detailed transaction display for mUSD reward claims showing claimed amount, network fee, and received total (#25452) +- Adds functionality for selecting a payment method (#25681) +- Base setup for in-app provisioning (#25669) +- Update the look of the "Earn %" CTA displayed for ETH and Tron staking products to tag style (#25722) +- Added educational bottom sheet explaining that mUSD bonuses are claimed on Linea, and auto-scroll to the resulting token (#25516) + after successful claim +- Added Perps “Pay with” option (Perps balance or other tokens) and info tooltip on the order view. (#25626) +- Updates the "Earn a 3% bonus" text in the mUSD conversion CTA to be clickable. (#25676) +- Added new token details button layout behind a feature flag (#25574) +- Added payment method deeplink support for ramps (#25003) +- Bring back destination asset sync to new swaps asset picker (#25644) +- Add sanitized origin to sentinel metadata (#25612) +- Added a warning message when gas sponsorship is unavailable due to reserve balance requirements. (#25320) +- Added new design of the perps empty state (#25581) +- Added A/B test for homepage featured section (carousel vs list) with variant-specific analytics; replaced empty predictions (#25237) + state with featured markets; hide balance card when no positions exist; + removed dead code +- Added Buy/Sell sticky action bar to Token Details page with smart token selection (#25499) +- Updated the Browser Tabs View with a new top navigation bar, 2-column grid layout, and improved navigation behavior (#25470) +- Allow user to opt-in all accounts at once to Rewards (#24450) +- Improved Perps home screen load time by making price prewarming non-blocking (#25501) +- Add rewards season 2 season status banner (#25522) +- Remove legacy swaps liveness service in favor of new stx hooks (#25506) +- Added points estimate history tracking to state logs for Customer Support diagnostics (#25389) +- When one-click trade transaction creation fails, users now see an error toast ("Could not open position") and the failure is (#25429) + tracked in analytics. +- New retryWithDelay utility - A generic, well-tested retry utility (#24920) + Updated getAuthTokens - Now automatically retries up to + 3 times on transient failures with logging +- Update slippage UI, adding option for users to set a custom slippage (#25405) +- Added deeplinking to the NFT screen (#25426) +- Updated browser URL bar buttons - back button now shows chevron icon and hides when typing, cancel button always shows text (#25418) + instead of X icon +- Added omni-search to browser URL bar - search tokens, perps, and predictions directly from the browser (#25358) +- Fixed malicious alert modal to require checkbox acknowledgment before enabling the Acknowledge button, and added a Close (#24055) + button for easier dismissal +- Replaced transaction details modal with bottom sheet for improved UX consistency (#25400) + +### Fixed + +- Remove deeplink interstitial on dApp deeplinks (#25963) +- Multiple fixes on import token flow (#25962) +- Fixed decimal precision calculation for Tron's staked balance (#25430) +- Fixed intermittent "Failed to fetch market data" errors on Perps by switching market data fetches from WebSocket to HTTP (#26014) + transport +- Fixed `x-us-env` header being incorrectly set to `false` for US Card users when geolocation requests fail (#25971) +- Fix #24546 with human readable message (#25555) +- Removed "Add funds to start trading perps" banner from Perps market details and allow opening trades (Long/Short) when perps (#25960) + balance is zero. +- Fixed long token names pushing balance off screen in Send flow and MM Pay token picker (#25338) +- Fix #25693 styling issue in for ledger devices (#25758) +- Fixed navigation error and token buyability checks when purchasing crypto with cash using unified buy V2 (#25617) +- Fixed Predictions tab not hiding monetary values when privacy mode is enabled (#25887) +- Fixed Perps deposit+order flow so the pending deposit toast auto-dismisses after a few seconds and the "deposit taking longer" (#25939) + message appears after 30 seconds. +- Fixed header height to scale properly with larger accessibility font sizes (#25855) +- Activity header symbol fallback (#25821) +- Fixed the Perps order pay row not appearing until margin was loaded. (#25836) +- When passoword oudated, it navigate to oauthRehydrate screen when reopen app (#25687) +- Fixed notification and transaction display for EIP-7702 transactions without nonces (#25646) +- Adds event for when token details page is opened. (#25780) +- Added error screens when wallet creation fails, allowing users to retry or contact support instead of being redirected (#25564) + to login. +- Remove toggle switch from login screen (#25424) +- Fixed minor button layout issues (#25771) +- Fixed long account names overflowing in the Deposit Buy screen by enabling proper text truncation (#25715) +- Remove subtitle in token details (#25726) +- Fixed flow for "Cash buy X" button on the new token details layout (#25719) +- Pass assetID to the on ramp buy screen. (#25709) +- Fixes padding in add chain approval bottom sheet (#25671) +- Refactored (#25613) +- N/a (#25642) +- Fix rewards end of season scroll issue (#25639) +- Exclude gas fees from swap quotes insufficientBal calculation (#25637) +- Fixed Perps activity tab sometimes showing empty when accessed from perps home or market detail screens (#25695) +- N/a (#25635) +- Fixed perps tutorial animation alignment by removing empty space in carousel (#25664) +- Prevent mUSD conversion initiation from creating duplicate transactions (#25604) +- Fixed inaccurate fill percentages for historical Perps orders and improved price precision for low-priced assets (#24278) +- Fixes incorrect stop lost banner price (#25556) +- Fixed missing localization for "Change" text on the Buy screen (#25641) +- Do not render keyboard when quote reloads after slippage change (#25633) +- Fixed hardware wallet scan screen layout with centered reader, blurred edges, and improved text positioning (#25290) +- Fixed transaction list not automatically scrolling to show latest transactions after send/swap operations (#25467) +- Fixed order book header price not updating in real-time (#25577) +- Disable swap max button on native assets when stx is disabled (#25023) +- Fixed perps market list search to reset category filters when closing search and enabled sort direction toggle for all (#25465) + sort options +- Fixes an issue preventing insufficient funds error when pressing max balance after inputting non-max balance in swaps (#25513) +- Change Rewards season summary icon colors (#25458) +- Strengthen explore portfolio site condition (#25433) +- Fixed a bug where in the Swaps recipient account picker, if the user clicked on the search input bar, the keyboard would (#25393) + push the search input off screen. + +## [7.65.0] + +### Added + +- Added WebSocket connection health toast notification for Perps trading to show real-time connection status with manual retry (#25022) + option +- Update the look of the "Earn %" CTA displayed for ETH and Tron staking products to tag style (#25722) +- Added educational bottom sheet explaining that mUSD bonuses are claimed on Linea, and auto-scroll to the resulting token (#25516) + after successful claim +- Added Perps “Pay with” option (Perps balance or other tokens) and info tooltip on the order view. (#25626) +- Updates the "Earn a 3% bonus" text in the mUSD conversion CTA to be clickable. (#25676) +- Added new token details button layout behind a feature flag (#25574) +- Added payment method deeplink support for ramps (#25003) +- Bring back destination asset sync to new swaps asset picker (#25644) +- Add sanitized origin to sentinel metadata (#25612) +- Added a warning message when gas sponsorship is unavailable due to reserve balance requirements. (#25320) +- Added new design of the perps empty state (#25581) +- Added A/B test for homepage featured section (carousel vs list) with variant-specific analytics; replaced empty predictions (#25237) + state with featured markets; hide balance card when no positions exist; + removed dead code +- Added Buy/Sell sticky action bar to Token Details page with smart token selection (#25499) +- Updated the Browser Tabs View with a new top navigation bar, 2-column grid layout, and improved navigation behavior (#25470) +- Allow user to opt-in all accounts at once to Rewards (#24450) +- Improved Perps home screen load time by making price prewarming non-blocking (#25501) +- Add rewards season 2 season status banner (#25522) +- Remove legacy swaps liveness service in favor of new stx hooks (#25506) +- Added points estimate history tracking to state logs for Customer Support diagnostics (#25389) +- When one-click trade transaction creation fails, users now see an error toast ("Could not open position") and the failure is (#25429) + tracked in analytics. +- New retryWithDelay utility - A generic, well-tested retry utility (#24920) + Updated getAuthTokens - Now automatically retries up to + 3 times on transient failures with logging +- Update slippage UI, adding option for users to set a custom slippage (#25405) +- Added deeplinking to the NFT screen (#25426) +- Updated browser URL bar buttons - back button now shows chevron icon and hides when typing, cancel button always shows text (#25418) + instead of X icon +- Added omni-search to browser URL bar - search tokens, perps, and predictions directly from the browser (#25358) +- Fixed malicious alert modal to require checkbox acknowledgment before enabling the Acknowledge button, and added a Close (#24055) + button for easier dismissal +- Replaced transaction details modal with bottom sheet for improved UX consistency (#25400) +- Added one-click trading for Perps, allowing users to deposit funds and execute trades seamlessly within the order view (#24964) +- Update slippage UI, adding option for users to set a custom slippage (#25124) +- Updated stablecoin lending cta to be right-aligned and not render the percentage (#25351) +- Add same-chain mUSD conversion enforcement (#25238) +- Added Metal Card checkout flow allowing virtual card holders to upgrade to a physical Metal Card with Daimo Pay integration (#25172) +- Added support for queueing non-EVM confirmations (#25319) +- Added trending markets display in Perps tab for users without open positions to improve trading discovery (#25302) +- Support filter by event types in the Activity Tab (#24910) +- Allow user to set a referral code in Rewards Settings after opt-in (#25085) +- Change password screen ui fixes (#25301) +- Continue button placement changes in create pasword screen (#25264) +- Added close button to token selection modal in Earn feature (#25006) +- Added `earn-musd` deeplink handler for direct navigation to mUSD conversion education flow (#25285) +- Add client in metadata for smartTransaction and relayTransaction transaction submission (#25331) +- Integrates per chain file save for tokenListController. (#24019) +- Improved mUSD bonus claiming flow to redirect to homepage after claiming (#25274) +- Add Bitcoin and Tron account support for rewards (#24890) +- Added "terms apply" clickable link to mUSD conversion education screen and navbar tooltip (#25284) +- Added one-click "Switch to Infura" button for custom networks experiencing connectivity issues (#25054) +- Added ability to claim Merkl rewards from mainnet mUSD asset overview (rewards still claimed on Linea) (#25259) +- Changed asset picker to pin selected token to top of list (#25226) +- Added swipe navigation gestures (swipe left/right edges to navigate browser history) and pull-to-refresh functionality (pull (#24851) + down from top to reload page) to the In-App Browser +- Added MUSD Conversion Transaction Details screen showing source and destination token amounts (#24551) +- Fixed Merkl rewards claimable amount not updating immediately after claiming by reading from blockchain and implementing (#24935) + optimistic UI updates +- Brought back MetaMask fee row for mUSD conversion transactions (#25132) +- Handle shield deep link (#23663) +- Fixed claimable reward display rounding to show "< 0.01" instead of "< 0.00001" for very small amounts (#25174) +- Enable support for EIP-5792 methods over WalletConnect (#25114) +- Import SRP screen UX improvements (#24693) +- Added new swaps asset picker (#22712) + +### Fixed + +- Fixes padding in add chain approval bottom sheet (#25671) +- Refactored (#25613) +- N/a (#25642) +- Fix rewards end of season scroll issue (#25639) +- Exclude gas fees from swap quotes insufficientBal calculation (#25637) +- Fixed Perps activity tab sometimes showing empty when accessed from perps home or market detail screens (#25695) +- N/a (#25635) +- Fixed perps tutorial animation alignment by removing empty space in carousel (#25664) +- Prevent mUSD conversion initiation from creating duplicate transactions (#25604) +- Fixed inaccurate fill percentages for historical Perps orders and improved price precision for low-priced assets (#24278) +- Fixes incorrect stop lost banner price (#25556) +- Fixed missing localization for "Change" text on the Buy screen (#25641) +- Do not render keyboard when quote reloads after slippage change (#25633) +- Fixed hardware wallet scan screen layout with centered reader, blurred edges, and improved text positioning (#25290) +- Fixed transaction list not automatically scrolling to show latest transactions after send/swap operations (#25467) +- Fixed order book header price not updating in real-time (#25577) +- Disable swap max button on native assets when stx is disabled (#25023) +- Fixed perps market list search to reset category filters when closing search and enabled sort direction toggle for all (#25465) + sort options +- Fixes an issue preventing insufficient funds error when pressing max balance after inputting non-max balance in swaps (#25513) +- Change Rewards season summary icon colors (#25458) +- Strengthen explore portfolio site condition (#25433) +- Fixed a bug where in the Swaps recipient account picker, if the user clicked on the search input bar, the keyboard would (#25393) + push the search input off screen. +- Fixed a bug where the currently selected swap asset would be pinned to the top of the asset picker list even when it didn't (#25395) + match the search query +- Enables the “Got it” button in an alert (#25368) +- Fix multiple bugs with stop loss being set via stop loss banner (#25234) +- Password field error state on Create Password screen. (#25254) +- Adjusted padding and border radius for Swaps network pills (#25342) +- Swaps Non EVM tokens with zero balance now show 2 decimal places just like the EVM ones (#25289) +- Format input amount when validating balance (#25333) +- N/a (#25299) +- Fixed postal code input in Deposit flow to allow entering codes with punctuation, spaces, and letters (#25323) +- Disabled the "switch tokens" button when destination token in on a disabled network (#25311) +- Fixed a bug where the asset picker would pin the currently selected asset to the top of networks that didn't match the (#25308) + network of the selected token +- Fixes missing stock badge on asset overview opened from trending token search view (#25288) +- Changes the mUSD conversion asset overview CTA copy (#25294) +- Made liquidation price estimate in margin adjustment form to accurately reflect Hyperliquid's maintenance margin rules (#25243) +- Android Safe Area View Explore Layout Issues (#25142) +- Removed chevron from Swaps recipient address picker (#25207) +- Fixes iOS yellow AutoFill suggestion box appearing above text fields during Card onboarding (#25210) +- Show token symbol on Send screen for tokens with zero balance (#25201) +- Remove isEvm guard from Perps wallet actions button (#25239) +- Fix layout flicker on network fee row. (#25161) +- New error type: GoogleLoginOneTapFailure (code 10016) for generic One Tap failures (#24936) + Browser fallback: One Tap failures now trigger + browser-based OAuth on Android +- Fixed PnL dollar value formatting in Predict sell preview to show 2 decimal places (#25228) +- Updated mUSD conversion screen navbar (#25135) +- Fixed chainId assertions in `eth_sendTransaction` and `eth_signTypedData_v4` requests over the Multichain API (#25131) +- Updated Deposit page selectors to have consistent styling without borders (#25128) +- Updated Deposit page header to use back button instead of close button (#25126) +- Removed background from payment method icons in deposit flow (#25122) +- Set OPTIN_META_METRICS_UI_SEEN flag when user login with social login (#24979) + unset OPTIN_META_METRICS_UI_SEEN flag when user create + srp wallet +- Fixed a bug in the network name for the token detail page (#25106) +- Fixed Perps WebSocket race conditions and error handling during reconnection/initialization states (#25029) +- Changed swaps network filtering logic to only filter source networks (#25092) + ## [7.64.1] ### Fixed @@ -10359,7 +10615,9 @@ 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.64.1...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.66.0...HEAD +[7.66.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.65.0...v7.66.0 +[7.65.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.1...v7.65.0 [7.64.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.0...v7.64.1 [7.64.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.63.1...v7.64.0 [7.63.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.63.0...v7.63.1 From c0f721f3df0388683d08cf05b7f184ff7bb28d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:05:18 +0000 Subject: [PATCH 019/131] chore: merge stable into release 7.67.0 branch (#26496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** sync release/7.67.0 with stable ## **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. ## **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. --------- Co-authored-by: runway-github[bot] <73448015+runway-github[bot]@users.noreply.github.com> Co-authored-by: Caainã Jeronimo Co-authored-by: Matthew Grainger <46547583+Matt561@users.noreply.github.com> Co-authored-by: Prithpal Sooriya Co-authored-by: Michal Szorad Co-authored-by: Cursor Co-authored-by: metamaskbot Co-authored-by: Juanmi <95381763+juanmigdr@users.noreply.github.com> Co-authored-by: Bruno Nascimento Co-authored-by: Ale Machado Co-authored-by: Bryan Fullam Co-authored-by: Aslau Mario-Daniel Co-authored-by: sethkfman <10342624+sethkfman@users.noreply.github.com> Co-authored-by: Mark Stacey Co-authored-by: Cal-L Co-authored-by: Bruno Nascimento Co-authored-by: Alejandro Garcia Anglada Co-authored-by: OGPoyraz Co-authored-by: Matthew Walsh Co-authored-by: Florin Dzeladini Co-authored-by: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com> Co-authored-by: Nick Gambino <35090461+gambinish@users.noreply.github.com> Co-authored-by: Vince Howard Co-authored-by: sahar-fehri Co-authored-by: George Weiler Co-authored-by: George Marshall Co-authored-by: George Marshall Co-authored-by: sophieqgu <37032128+sophieqgu@users.noreply.github.com> Co-authored-by: Wei Sun Co-authored-by: ieow <4881057+ieow@users.noreply.github.com> Co-authored-by: himanshu Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Andre Pimenta Co-authored-by: Salim TOUBAL Co-authored-by: Patryk Łucka <5708018+PatrykLucka@users.noreply.github.com> Co-authored-by: chloeYue <105063779+chloeYue@users.noreply.github.com> --- .yarnrc.yml | 1 + CHANGELOG.md | 260 ++++++++++++++- android/app/src/main/AndroidManifest.xml | 4 +- app/components/UI/Carousel/index.test.tsx | 134 -------- app/components/UI/Carousel/index.tsx | 58 ---- app/components/UI/Earn/utils/musd.test.ts | 133 ++++++-- app/components/UI/Earn/utils/musd.ts | 79 ++++- app/components/UI/TransactionElement/utils.js | 8 +- .../BrowserTab/GestureWebViewWrapper.test.tsx | 229 ++++++++++++- .../BrowserTab/GestureWebViewWrapper.tsx | 68 +++- app/components/Views/BrowserTab/constants.ts | 8 + .../Wallet/__snapshots__/index.test.tsx.snap | 8 +- app/components/Views/Wallet/index.test.tsx | 43 ++- app/components/Views/Wallet/index.tsx | 2 +- .../transaction-details-hero.test.tsx | 31 +- .../transaction-details-hero.tsx | 11 +- .../hooks/earn/useMerklClaimAmount.ts | 39 ++- .../controllers/storage-service-init.test.ts | 309 +++++++++++++++++- .../controllers/storage-service-init.ts | 53 ++- .../metrics_properties/base.test.ts | 2 + .../metrics_properties/base.ts | 8 + .../utils/storage-service-utils.test.ts | 176 ++++++++++ .../Engine/utils/storage-service-utils.ts | 35 ++ app/store/migrations/119.test.ts | 7 +- app/store/migrations/119.ts | 5 +- 25 files changed, 1411 insertions(+), 300 deletions(-) create mode 100644 app/core/Engine/utils/storage-service-utils.test.ts create mode 100644 app/core/Engine/utils/storage-service-utils.ts diff --git a/.yarnrc.yml b/.yarnrc.yml index 4328c89b949..4124447b739 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -13,6 +13,7 @@ plugins: npmAuditIgnoreAdvisories: - 1109627 # TODO: Upgrade @react-native-community/cli to 17.0.1+ when ready. Suppressing for now to unblock CI. - 1112455 # lodash prototype pollution in _.unset and _.omit. No fix available yet (latest is 4.17.21, affected <=4.17.22). Suppressing for now to unblock CI. https://github.com/advisories/GHSA-xxjr-mmjv-4gpg + - 1113402 # 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 yarnPath: .yarn/releases/yarn-4.10.3.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f7f648bd64..2643b348f2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,262 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.66.0] + +### Added + +- Adds a page for changing preferred ramp provider (#25860) +- Add asset overview deeplinks (#25447) +- Restored the previously selected "Pay with" token when returning to the Perps order view within 5 minutes. (#25938) +- Fixed predict transaction toast notifications not appearing when navigating away from the Predict tab (#25863) +- Added new Accounts Menu screen to organize settings navigation with Settings, Manage, and Resources sections (#25611) +- Adds Bridge and Swap feature to `MegaETH` (#25906) +- Adds chiliz.png as network logo and enables it in metamask mobile (#25437) +- Always display learn more about perps link (#25958) +- Created new token list item v2 (#25824) +- Added custom claim transaction request screen for mUSD bonus claims with improved UX flow (#25837) +- Added an "Ending soon" tab to prediction markets feed showing markets sorted by end date (#25868) +- Removed legacy homepage script injection and related RPC methods (#25620) +- Add google/web search inside browser search bar (#25897) +- Homogenize spacing on Explore page for perps items (#25894) +- Added 1st interaction alert to warn users when interacting with an address for the first time. (#25575) +- Added icons to the bridge token selector network pills (#25851) +- Create feature flag for the new unified assets state (#25891) +- Adds Bridge and Swap feature to HyperEVM (#25769) +- Added lightweight position display and one-click Long/Short trading on token details page for perps-enabled assets (#25685) +- Improved browser tab switching performance by keeping tabs mounted (#25702) +- Validation errors from non-EVM transaction snaps will now be displayed to users during send flow. (#25648) +- Added detailed transaction display for mUSD reward claims showing claimed amount, network fee, and received total (#25452) +- Adds functionality for selecting a payment method (#25681) +- Base setup for in-app provisioning (#25669) +- Update the look of the "Earn %" CTA displayed for ETH and Tron staking products to tag style (#25722) +- Added educational bottom sheet explaining that mUSD bonuses are claimed on Linea, and auto-scroll to the resulting token (#25516) + after successful claim +- Added Perps “Pay with” option (Perps balance or other tokens) and info tooltip on the order view. (#25626) +- Updates the "Earn a 3% bonus" text in the mUSD conversion CTA to be clickable. (#25676) +- Added new token details button layout behind a feature flag (#25574) +- Added payment method deeplink support for ramps (#25003) +- Bring back destination asset sync to new swaps asset picker (#25644) +- Add sanitized origin to sentinel metadata (#25612) +- Added a warning message when gas sponsorship is unavailable due to reserve balance requirements. (#25320) +- Added new design of the perps empty state (#25581) +- Added A/B test for homepage featured section (carousel vs list) with variant-specific analytics; replaced empty predictions (#25237) + state with featured markets; hide balance card when no positions exist; + removed dead code +- Added Buy/Sell sticky action bar to Token Details page with smart token selection (#25499) +- Updated the Browser Tabs View with a new top navigation bar, 2-column grid layout, and improved navigation behavior (#25470) +- Allow user to opt-in all accounts at once to Rewards (#24450) +- Improved Perps home screen load time by making price prewarming non-blocking (#25501) +- Add rewards season 2 season status banner (#25522) +- Remove legacy swaps liveness service in favor of new stx hooks (#25506) +- Added points estimate history tracking to state logs for Customer Support diagnostics (#25389) +- When one-click trade transaction creation fails, users now see an error toast ("Could not open position") and the failure is (#25429) + tracked in analytics. +- New retryWithDelay utility - A generic, well-tested retry utility (#24920) + Updated getAuthTokens - Now automatically retries up to + 3 times on transient failures with logging +- Update slippage UI, adding option for users to set a custom slippage (#25405) +- Added deeplinking to the NFT screen (#25426) +- Updated browser URL bar buttons - back button now shows chevron icon and hides when typing, cancel button always shows text (#25418) + instead of X icon +- Added omni-search to browser URL bar - search tokens, perps, and predictions directly from the browser (#25358) +- Fixed malicious alert modal to require checkbox acknowledgment before enabling the Acknowledge button, and added a Close (#24055) + button for easier dismissal +- Replaced transaction details modal with bottom sheet for improved UX consistency (#25400) + +### Fixed + +- Remove deeplink interstitial on dApp deeplinks (#25963) +- Multiple fixes on import token flow (#25962) +- Fixed decimal precision calculation for Tron's staked balance (#25430) +- Fixed intermittent "Failed to fetch market data" errors on Perps by switching market data fetches from WebSocket to HTTP (#26014) + transport +- Fixed `x-us-env` header being incorrectly set to `false` for US Card users when geolocation requests fail (#25971) +- Fix #24546 with human readable message (#25555) +- Removed "Add funds to start trading perps" banner from Perps market details and allow opening trades (Long/Short) when perps (#25960) + balance is zero. +- Fixed long token names pushing balance off screen in Send flow and MM Pay token picker (#25338) +- Fix #25693 styling issue in for ledger devices (#25758) +- Fixed navigation error and token buyability checks when purchasing crypto with cash using unified buy V2 (#25617) +- Fixed Predictions tab not hiding monetary values when privacy mode is enabled (#25887) +- Fixed Perps deposit+order flow so the pending deposit toast auto-dismisses after a few seconds and the "deposit taking longer" (#25939) + message appears after 30 seconds. +- Fixed header height to scale properly with larger accessibility font sizes (#25855) +- Activity header symbol fallback (#25821) +- Fixed the Perps order pay row not appearing until margin was loaded. (#25836) +- When passoword oudated, it navigate to oauthRehydrate screen when reopen app (#25687) +- Fixed notification and transaction display for EIP-7702 transactions without nonces (#25646) +- Adds event for when token details page is opened. (#25780) +- Added error screens when wallet creation fails, allowing users to retry or contact support instead of being redirected (#25564) + to login. +- Remove toggle switch from login screen (#25424) +- Fixed minor button layout issues (#25771) +- Fixed long account names overflowing in the Deposit Buy screen by enabling proper text truncation (#25715) +- Remove subtitle in token details (#25726) +- Fixed flow for "Cash buy X" button on the new token details layout (#25719) +- Pass assetID to the on ramp buy screen. (#25709) +- Fixes padding in add chain approval bottom sheet (#25671) +- Refactored (#25613) +- N/a (#25642) +- Fix rewards end of season scroll issue (#25639) +- Exclude gas fees from swap quotes insufficientBal calculation (#25637) +- Fixed Perps activity tab sometimes showing empty when accessed from perps home or market detail screens (#25695) +- N/a (#25635) +- Fixed perps tutorial animation alignment by removing empty space in carousel (#25664) +- Prevent mUSD conversion initiation from creating duplicate transactions (#25604) +- Fixed inaccurate fill percentages for historical Perps orders and improved price precision for low-priced assets (#24278) +- Fixes incorrect stop lost banner price (#25556) +- Fixed missing localization for "Change" text on the Buy screen (#25641) +- Do not render keyboard when quote reloads after slippage change (#25633) +- Fixed hardware wallet scan screen layout with centered reader, blurred edges, and improved text positioning (#25290) +- Fixed transaction list not automatically scrolling to show latest transactions after send/swap operations (#25467) +- Fixed order book header price not updating in real-time (#25577) +- Disable swap max button on native assets when stx is disabled (#25023) +- Fixed perps market list search to reset category filters when closing search and enabled sort direction toggle for all (#25465) + sort options +- Fixes an issue preventing insufficient funds error when pressing max balance after inputting non-max balance in swaps (#25513) +- Change Rewards season summary icon colors (#25458) +- Strengthen explore portfolio site condition (#25433) +- Fixed a bug where in the Swaps recipient account picker, if the user clicked on the search input bar, the keyboard would (#25393) + push the search input off screen. + +## [7.65.0] + +### Added + +- Added WebSocket connection health toast notification for Perps trading to show real-time connection status with manual retry (#25022) + option +- Update the look of the "Earn %" CTA displayed for ETH and Tron staking products to tag style (#25722) +- Added educational bottom sheet explaining that mUSD bonuses are claimed on Linea, and auto-scroll to the resulting token (#25516) + after successful claim +- Added Perps “Pay with” option (Perps balance or other tokens) and info tooltip on the order view. (#25626) +- Updates the "Earn a 3% bonus" text in the mUSD conversion CTA to be clickable. (#25676) +- Added new token details button layout behind a feature flag (#25574) +- Added payment method deeplink support for ramps (#25003) +- Bring back destination asset sync to new swaps asset picker (#25644) +- Add sanitized origin to sentinel metadata (#25612) +- Added a warning message when gas sponsorship is unavailable due to reserve balance requirements. (#25320) +- Added new design of the perps empty state (#25581) +- Added A/B test for homepage featured section (carousel vs list) with variant-specific analytics; replaced empty predictions (#25237) + state with featured markets; hide balance card when no positions exist; + removed dead code +- Added Buy/Sell sticky action bar to Token Details page with smart token selection (#25499) +- Updated the Browser Tabs View with a new top navigation bar, 2-column grid layout, and improved navigation behavior (#25470) +- Allow user to opt-in all accounts at once to Rewards (#24450) +- Improved Perps home screen load time by making price prewarming non-blocking (#25501) +- Add rewards season 2 season status banner (#25522) +- Remove legacy swaps liveness service in favor of new stx hooks (#25506) +- Added points estimate history tracking to state logs for Customer Support diagnostics (#25389) +- When one-click trade transaction creation fails, users now see an error toast ("Could not open position") and the failure is (#25429) + tracked in analytics. +- New retryWithDelay utility - A generic, well-tested retry utility (#24920) + Updated getAuthTokens - Now automatically retries up to + 3 times on transient failures with logging +- Update slippage UI, adding option for users to set a custom slippage (#25405) +- Added deeplinking to the NFT screen (#25426) +- Updated browser URL bar buttons - back button now shows chevron icon and hides when typing, cancel button always shows text (#25418) + instead of X icon +- Added omni-search to browser URL bar - search tokens, perps, and predictions directly from the browser (#25358) +- Fixed malicious alert modal to require checkbox acknowledgment before enabling the Acknowledge button, and added a Close (#24055) + button for easier dismissal +- Replaced transaction details modal with bottom sheet for improved UX consistency (#25400) +- Added one-click trading for Perps, allowing users to deposit funds and execute trades seamlessly within the order view (#24964) +- Update slippage UI, adding option for users to set a custom slippage (#25124) +- Updated stablecoin lending cta to be right-aligned and not render the percentage (#25351) +- Add same-chain mUSD conversion enforcement (#25238) +- Added Metal Card checkout flow allowing virtual card holders to upgrade to a physical Metal Card with Daimo Pay integration (#25172) +- Added support for queueing non-EVM confirmations (#25319) +- Added trending markets display in Perps tab for users without open positions to improve trading discovery (#25302) +- Support filter by event types in the Activity Tab (#24910) +- Allow user to set a referral code in Rewards Settings after opt-in (#25085) +- Change password screen ui fixes (#25301) +- Continue button placement changes in create pasword screen (#25264) +- Added close button to token selection modal in Earn feature (#25006) +- Added `earn-musd` deeplink handler for direct navigation to mUSD conversion education flow (#25285) +- Add client in metadata for smartTransaction and relayTransaction transaction submission (#25331) +- Integrates per chain file save for tokenListController. (#24019) +- Improved mUSD bonus claiming flow to redirect to homepage after claiming (#25274) +- Add Bitcoin and Tron account support for rewards (#24890) +- Added "terms apply" clickable link to mUSD conversion education screen and navbar tooltip (#25284) +- Added one-click "Switch to Infura" button for custom networks experiencing connectivity issues (#25054) +- Added ability to claim Merkl rewards from mainnet mUSD asset overview (rewards still claimed on Linea) (#25259) +- Changed asset picker to pin selected token to top of list (#25226) +- Added swipe navigation gestures (swipe left/right edges to navigate browser history) and pull-to-refresh functionality (pull (#24851) + down from top to reload page) to the In-App Browser +- Added MUSD Conversion Transaction Details screen showing source and destination token amounts (#24551) +- Fixed Merkl rewards claimable amount not updating immediately after claiming by reading from blockchain and implementing (#24935) + optimistic UI updates +- Brought back MetaMask fee row for mUSD conversion transactions (#25132) +- Handle shield deep link (#23663) +- Fixed claimable reward display rounding to show "< 0.01" instead of "< 0.00001" for very small amounts (#25174) +- Enable support for EIP-5792 methods over WalletConnect (#25114) +- Import SRP screen UX improvements (#24693) +- Added new swaps asset picker (#22712) + +### Fixed + +- Fixes padding in add chain approval bottom sheet (#25671) +- Refactored (#25613) +- N/a (#25642) +- Fix rewards end of season scroll issue (#25639) +- Exclude gas fees from swap quotes insufficientBal calculation (#25637) +- Fixed Perps activity tab sometimes showing empty when accessed from perps home or market detail screens (#25695) +- N/a (#25635) +- Fixed perps tutorial animation alignment by removing empty space in carousel (#25664) +- Prevent mUSD conversion initiation from creating duplicate transactions (#25604) +- Fixed inaccurate fill percentages for historical Perps orders and improved price precision for low-priced assets (#24278) +- Fixes incorrect stop lost banner price (#25556) +- Fixed missing localization for "Change" text on the Buy screen (#25641) +- Do not render keyboard when quote reloads after slippage change (#25633) +- Fixed hardware wallet scan screen layout with centered reader, blurred edges, and improved text positioning (#25290) +- Fixed transaction list not automatically scrolling to show latest transactions after send/swap operations (#25467) +- Fixed order book header price not updating in real-time (#25577) +- Disable swap max button on native assets when stx is disabled (#25023) +- Fixed perps market list search to reset category filters when closing search and enabled sort direction toggle for all (#25465) + sort options +- Fixes an issue preventing insufficient funds error when pressing max balance after inputting non-max balance in swaps (#25513) +- Change Rewards season summary icon colors (#25458) +- Strengthen explore portfolio site condition (#25433) +- Fixed a bug where in the Swaps recipient account picker, if the user clicked on the search input bar, the keyboard would (#25393) + push the search input off screen. +- Fixed a bug where the currently selected swap asset would be pinned to the top of the asset picker list even when it didn't (#25395) + match the search query +- Enables the “Got it” button in an alert (#25368) +- Fix multiple bugs with stop loss being set via stop loss banner (#25234) +- Password field error state on Create Password screen. (#25254) +- Adjusted padding and border radius for Swaps network pills (#25342) +- Swaps Non EVM tokens with zero balance now show 2 decimal places just like the EVM ones (#25289) +- Format input amount when validating balance (#25333) +- N/a (#25299) +- Fixed postal code input in Deposit flow to allow entering codes with punctuation, spaces, and letters (#25323) +- Disabled the "switch tokens" button when destination token in on a disabled network (#25311) +- Fixed a bug where the asset picker would pin the currently selected asset to the top of networks that didn't match the (#25308) + network of the selected token +- Fixes missing stock badge on asset overview opened from trending token search view (#25288) +- Changes the mUSD conversion asset overview CTA copy (#25294) +- Made liquidation price estimate in margin adjustment form to accurately reflect Hyperliquid's maintenance margin rules (#25243) +- Android Safe Area View Explore Layout Issues (#25142) +- Removed chevron from Swaps recipient address picker (#25207) +- Fixes iOS yellow AutoFill suggestion box appearing above text fields during Card onboarding (#25210) +- Show token symbol on Send screen for tokens with zero balance (#25201) +- Remove isEvm guard from Perps wallet actions button (#25239) +- Fix layout flicker on network fee row. (#25161) +- New error type: GoogleLoginOneTapFailure (code 10016) for generic One Tap failures (#24936) + Browser fallback: One Tap failures now trigger + browser-based OAuth on Android +- Fixed PnL dollar value formatting in Predict sell preview to show 2 decimal places (#25228) +- Updated mUSD conversion screen navbar (#25135) +- Fixed chainId assertions in `eth_sendTransaction` and `eth_signTypedData_v4` requests over the Multichain API (#25131) +- Updated Deposit page selectors to have consistent styling without borders (#25128) +- Updated Deposit page header to use back button instead of close button (#25126) +- Removed background from payment method icons in deposit flow (#25122) +- Set OPTIN_META_METRICS_UI_SEEN flag when user login with social login (#24979) + unset OPTIN_META_METRICS_UI_SEEN flag when user create + srp wallet +- Fixed a bug in the network name for the token detail page (#25106) +- Fixed Perps WebSocket race conditions and error handling during reconnection/initialization states (#25029) +- Changed swaps network filtering logic to only filter source networks (#25092) + ## [7.64.1] ### Fixed @@ -10359,7 +10615,9 @@ 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.64.1...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.66.0...HEAD +[7.66.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.65.0...v7.66.0 +[7.65.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.1...v7.65.0 [7.64.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.0...v7.64.1 [7.64.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.63.1...v7.64.0 [7.63.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.63.0...v7.63.1 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 845d99f6471..e9e91ece731 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,12 +12,12 @@ - + - + diff --git a/app/components/UI/Carousel/index.test.tsx b/app/components/UI/Carousel/index.test.tsx index 624ef385857..b4dc6202023 100644 --- a/app/components/UI/Carousel/index.test.tsx +++ b/app/components/UI/Carousel/index.test.tsx @@ -23,7 +23,6 @@ import Routes from '../../../constants/navigation/Routes'; import { WalletClientType } from '../../../core/SnapKeyring/MultichainWalletSnapClient'; import { SolScope } from '@metamask/keyring-api'; import { setContentPreviewToken } from '../../../actions/notification/helpers'; -import { PREDICT_SUPERBOWL_VARIABLE_NAME } from '../Predict/constants/carousel'; const makeMockState = () => ({ @@ -97,32 +96,6 @@ jest.mock('./fetchCarouselSlidesFromContentful', () => ({ fetchCarouselSlidesFromContentful: jest.fn(), })); -jest.mock('../Predict/components/PredictMarketSportCard', () => { - const { useEffect } = jest.requireActual('react'); - const { View, Text } = jest.requireActual('react-native'); - return { - PredictMarketSportCardWrapper: function MockPredictMarketSportCardWrapper({ - marketId, - testID, - onLoad, - }: { - marketId: string; - testID?: string; - onLoad?: () => void; - }) { - useEffect(() => { - onLoad?.(); - }, [onLoad]); - - return ( - - {marketId} - - ); - }, - }; -}); - const mockDispatch = jest.fn(); const mockFetchCarouselSlides = jest.mocked(fetchCarouselSlidesFromContentful); @@ -514,110 +487,3 @@ describe('useFetchCarouselSlides()', () => { expect(mockFetchCarouselSlides).not.toHaveBeenCalled(); }); }); - -describe('Carousel Predict Superbowl Integration', () => { - it('renders PredictMarketSportCardWrapper for predict-superbowl slides', async () => { - const predictSlide = createMockSlide({ - id: 'predict-superbowl-slide', - variableName: PREDICT_SUPERBOWL_VARIABLE_NAME, - metadata: { marketId: 'test-market-123' }, - }); - mockFetchCarouselSlides.mockResolvedValue({ - prioritySlides: [], - regularSlides: [predictSlide], - }); - - const { findByTestId } = render(); - - const marketIdElement = await findByTestId('predict-sport-card-market-id'); - expect(marketIdElement).toHaveTextContent('test-market-123'); - }); - - it('does not render PredictMarketSportCardWrapper when metadata is missing marketId', async () => { - const regularSlide = createMockSlide({ id: 'regular-slide' }); - const predictSlide = createMockSlide({ - id: 'predict-superbowl-slide', - variableName: PREDICT_SUPERBOWL_VARIABLE_NAME, - metadata: undefined, - }); - mockFetchCarouselSlides.mockResolvedValue({ - prioritySlides: [], - regularSlides: [predictSlide, regularSlide], - }); - - const { findByTestId, queryByTestId } = render(); - - await findByTestId('carousel-slide-regular-slide'); - - expect(queryByTestId('predict-sport-card-wrapper')).toBeNull(); - }); - - it('does not render PredictMarketSportCardWrapper when marketId is empty', async () => { - const regularSlide = createMockSlide({ id: 'regular-slide' }); - const predictSlide = createMockSlide({ - id: 'predict-superbowl-slide', - variableName: PREDICT_SUPERBOWL_VARIABLE_NAME, - metadata: { marketId: '' }, - }); - mockFetchCarouselSlides.mockResolvedValue({ - prioritySlides: [], - regularSlides: [predictSlide, regularSlide], - }); - - const { findByTestId, queryByTestId } = render(); - - await findByTestId('carousel-slide-regular-slide'); - - expect(queryByTestId('predict-sport-card-wrapper')).toBeNull(); - }); - - it('passes correct props to PredictMarketSportCardWrapper', async () => { - const predictSlide = createMockSlide({ - id: 'predict-superbowl-slide', - variableName: PREDICT_SUPERBOWL_VARIABLE_NAME, - metadata: { marketId: 'market-abc-123' }, - testID: 'custom-test-id', - }); - mockFetchCarouselSlides.mockResolvedValue({ - prioritySlides: [predictSlide], - regularSlides: [], - }); - - const { findByTestId } = render(); - - const wrapper = await findByTestId('custom-test-id'); - expect(wrapper).toBeOnTheScreen(); - - const marketId = await findByTestId('predict-sport-card-market-id'); - expect(marketId).toHaveTextContent('market-abc-123'); - }); - - it('fires Banner Display tracking event for predict-superbowl slide', async () => { - mockTrackEvent.mockClear(); - mockCreateEventBuilder.mockClear(); - const predictSlide = createMockSlide({ - id: 'predict-superbowl-slide', - variableName: PREDICT_SUPERBOWL_VARIABLE_NAME, - metadata: { marketId: 'test-market-123' }, - }); - mockFetchCarouselSlides.mockResolvedValue({ - prioritySlides: [], - regularSlides: [predictSlide], - }); - - const { findByTestId } = render(); - - await findByTestId('predict-sport-card-market-id'); - - await waitFor(() => { - expect(mockCreateEventBuilder).toHaveBeenCalledWith({ - category: 'Banner Display', - properties: { - name: PREDICT_SUPERBOWL_VARIABLE_NAME, - }, - }); - }); - - expect(mockTrackEvent).toHaveBeenCalled(); - }); -}); diff --git a/app/components/UI/Carousel/index.tsx b/app/components/UI/Carousel/index.tsx index 92cbdb7a299..af76f168c6f 100644 --- a/app/components/UI/Carousel/index.tsx +++ b/app/components/UI/Carousel/index.tsx @@ -45,10 +45,6 @@ import { subscribeToContentPreviewToken } from '../../../actions/notification/he import SharedDeeplinkManager from '../../../core/DeeplinkManager/DeeplinkManager'; import { isInternalDeepLink } from '../../../core/DeeplinkManager/util/deeplinks'; import AppConstants from '../../../core/AppConstants'; -import { PredictMarketSportCardWrapper } from '../Predict/components/PredictMarketSportCard'; -import { PredictEventValues } from '../Predict/constants/eventNames'; -import { PREDICT_SUPERBOWL_VARIABLE_NAME } from '../Predict/constants/carousel'; -import { PredictCarouselMetadata } from '../Predict/types'; const MAX_CAROUSEL_SLIDES = 8; @@ -276,24 +272,6 @@ const CarouselComponent: FC = ({ style, onEmptyState }) => { dismissedBanners, ]); - const predictSuperbowlSlide = useMemo( - () => - slidesConfig.find( - (slide) => - slide.variableName === PREDICT_SUPERBOWL_VARIABLE_NAME && - !dismissedBanners.includes(slide.id), - ), - [slidesConfig, dismissedBanners], - ); - - const predictSuperbowlMarketId = useMemo(() => { - if (!predictSuperbowlSlide) return null; - const metadata = predictSuperbowlSlide.metadata as - | PredictCarouselMetadata - | undefined; - return metadata?.marketId ?? null; - }, [predictSuperbowlSlide]); - const visibleSlides = useMemo(() => { const filtered = slidesConfig.filter((slide: CarouselSlide) => { const active = isActive(slide); @@ -308,11 +286,6 @@ const CarouselComponent: FC = ({ style, onEmptyState }) => { } ///: END:ONLY_INCLUDE_IF - // We dont want to show the predict superbowl slide in the carousel - if (slide.variableName === PREDICT_SUPERBOWL_VARIABLE_NAME) { - return false; - } - return !dismissedBanners.includes(slide.id); }); @@ -564,11 +537,6 @@ const CarouselComponent: FC = ({ style, onEmptyState }) => { } }, [transitionToEmpty, onEmptyState]); - const handleSportCardDismiss = useCallback(() => { - if (!predictSuperbowlSlide) return; - dispatch(dismissBanner(predictSuperbowlSlide.id)); - }, [predictSuperbowlSlide, dispatch]); - const renderCard = useCallback( (slide: CarouselSlide, isCurrentCard: boolean) => { const isEmptyCard = slide.variableName === 'empty'; @@ -649,32 +617,6 @@ const CarouselComponent: FC = ({ style, onEmptyState }) => { } }, [currentSlide, trackEvent, createEventBuilder]); - const handlePredictSuperbowlLoad = useCallback(() => { - if (predictSuperbowlSlide) { - trackEvent( - createEventBuilder({ - category: 'Banner Display', - properties: { - name: - predictSuperbowlSlide.variableName ?? predictSuperbowlSlide.id, - }, - }).build(), - ); - } - }, [predictSuperbowlSlide, trackEvent, createEventBuilder]); - - if (predictSuperbowlMarketId) { - return ( - - ); - } - if ( !isCarouselVisible || (visibleSlides.length === 0 && !isAnimating.current) diff --git a/app/components/UI/Earn/utils/musd.test.ts b/app/components/UI/Earn/utils/musd.test.ts index bfcc8e4bb47..fa1497b1300 100644 --- a/app/components/UI/Earn/utils/musd.test.ts +++ b/app/components/UI/Earn/utils/musd.test.ts @@ -6,9 +6,12 @@ import { isMusdClaimForCurrentView, convertMusdClaimAmount, decodeMerklClaimParams, - decodeMerklClaimAmount, + getClaimPayoutFromReceipt, } from './musd'; -import { DISTRIBUTOR_CLAIM_ABI } from '../components/MerklRewards/constants'; +import { + DISTRIBUTOR_CLAIM_ABI, + MERKL_DISTRIBUTOR_ADDRESS, +} from '../components/MerklRewards/constants'; import { MUSD_TOKEN_ADDRESS } from '../constants/musd'; const LINEA_CHAIN_ID = '0xe708' as Hex; @@ -258,28 +261,120 @@ describe('musd utils', () => { }); }); - describe('decodeMerklClaimAmount', () => { - const userAddress = '0x1234567890123456789012345678901234567890'; - const tokenAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; - const amount = '999000'; + describe('getClaimPayoutFromReceipt', () => { + const USER = SELECTED_ADDRESS; + const TRANSFER_TOPIC = + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; + + const padAddress = (addr: string) => + `0x${addr.slice(2).toLowerCase().padStart(64, '0')}`; + + const makeTransferLog = ( + tokenAddress: string, + from: string, + to: string, + amount: bigint, + ) => ({ + address: tokenAddress, + topics: [TRANSFER_TOPIC, padAddress(from), padAddress(to)], + data: `0x${amount.toString(16).padStart(64, '0')}`, + }); - it('returns the amount from valid claim data', () => { - const iface = new Interface(DISTRIBUTOR_CLAIM_ABI); - const data = iface.encodeFunctionData('claim', [ - [userAddress], - [tokenAddress], - [amount], - [[]], - ]); - expect(decodeMerklClaimAmount(data)).toBe(amount); + it('extracts the payout from a matching Transfer log', () => { + const payout = 70000000n; // 70 mUSD + const logs = [ + makeTransferLog( + MUSD_TOKEN_ADDRESS, + MERKL_DISTRIBUTOR_ADDRESS, + USER, + payout, + ), + ]; + + expect(getClaimPayoutFromReceipt(logs, USER)).toBe(payout.toString()); }); - it('returns null for undefined data', () => { - expect(decodeMerklClaimAmount(undefined)).toBeNull(); + it('ignores Transfer logs from other senders', () => { + const logs = [ + makeTransferLog(MUSD_TOKEN_ADDRESS, OTHER_ADDRESS, USER, 100n), + ]; + + expect(getClaimPayoutFromReceipt(logs, USER)).toBeNull(); + }); + + it('ignores Transfer logs to other recipients', () => { + const logs = [ + makeTransferLog( + MUSD_TOKEN_ADDRESS, + MERKL_DISTRIBUTOR_ADDRESS, + OTHER_ADDRESS, + 100n, + ), + ]; + + expect(getClaimPayoutFromReceipt(logs, USER)).toBeNull(); }); - it('returns null for invalid data', () => { - expect(decodeMerklClaimAmount('0xbaddata')).toBeNull(); + it('ignores Transfer logs from a different token', () => { + const logs = [ + makeTransferLog( + '0x0000000000000000000000000000000000000099', + MERKL_DISTRIBUTOR_ADDRESS, + USER, + 100n, + ), + ]; + + expect(getClaimPayoutFromReceipt(logs, USER)).toBeNull(); + }); + + it('returns null for undefined logs', () => { + expect(getClaimPayoutFromReceipt(undefined, USER)).toBeNull(); + }); + + it('returns null for empty logs', () => { + expect(getClaimPayoutFromReceipt([], USER)).toBeNull(); + }); + + it('returns null for undefined user address', () => { + const logs = [ + makeTransferLog( + MUSD_TOKEN_ADDRESS, + MERKL_DISTRIBUTOR_ADDRESS, + USER, + 100n, + ), + ]; + + expect(getClaimPayoutFromReceipt(logs, undefined)).toBeNull(); + }); + + it('picks the correct log among multiple logs', () => { + const payout = 42000000n; + const logs = [ + // Some unrelated log + { + address: '0x0000000000000000000000000000000000000001', + topics: ['0xabc'], + data: '0x00', + }, + // The actual mUSD transfer from distributor + makeTransferLog( + MUSD_TOKEN_ADDRESS, + MERKL_DISTRIBUTOR_ADDRESS, + USER, + payout, + ), + // Another unrelated transfer + makeTransferLog( + '0x0000000000000000000000000000000000000099', + MERKL_DISTRIBUTOR_ADDRESS, + USER, + 999n, + ), + ]; + + expect(getClaimPayoutFromReceipt(logs, USER)).toBe(payout.toString()); }); }); }); diff --git a/app/components/UI/Earn/utils/musd.ts b/app/components/UI/Earn/utils/musd.ts index bd4a138371e..37827e3afd7 100644 --- a/app/components/UI/Earn/utils/musd.ts +++ b/app/components/UI/Earn/utils/musd.ts @@ -14,7 +14,10 @@ import { MUSD_TOKEN_ADDRESS, } from '../constants/musd'; import { getClaimedAmountFromContract } from '../components/MerklRewards/merkl-client'; -import { DISTRIBUTOR_CLAIM_ABI } from '../components/MerklRewards/constants'; +import { + DISTRIBUTOR_CLAIM_ABI, + MERKL_DISTRIBUTOR_ADDRESS, +} from '../components/MerklRewards/constants'; /** * Parameters for checking if a transaction is a mUSD claim for the current view. @@ -74,7 +77,7 @@ export function isMusdClaimForCurrentView({ * Parameters for converting mUSD claim amount to user's currency. */ export interface ConvertMusdClaimParams { - /** Raw claim amount from decodeMerklClaimAmount (wei string) */ + /** Raw claim amount in wei string */ claimAmountRaw: string; /** Native-to-user-currency conversion rate (e.g., ETH to EUR) */ conversionRate: BigNumber | number; @@ -233,15 +236,73 @@ export function decodeMerklClaimParams( } } +// ERC-20 Transfer(address,address,uint256) event topic +const ERC20_TRANSFER_TOPIC = + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; + +/** + * Log entry from a transaction receipt. + * The `topics` field is typed as `string` in TransactionController types, + * but at runtime it's `string[]` (raw JSON-RPC response). + */ +interface ReceiptLog { + address?: string; + data?: string; + topics?: string | string[]; +} + /** - * Decode the claim amount from a Merkl claim transaction data. - * Convenience wrapper around decodeMerklClaimParams that returns only the amount. + * Extract the actual mUSD payout from a confirmed claim transaction's receipt logs. * - * @param data - The transaction data hex string - * @returns The first claim amount as a string (raw value, not adjusted for decimals), or null if decoding fails + * The Merkl distributor calls the mUSD token's `transfer`, which emits an + * ERC-20 `Transfer(from=distributor, to=user, amount)` event. The `amount` + * in this event is the real per-transaction payout (not the cumulative total + * stored in calldata). + * + * @param logs - Receipt logs from txReceipt.logs + * @param userAddress - The claiming user's address (to match the Transfer `to` field) + * @returns The payout amount as a raw decimal string, or null if not found */ -export function decodeMerklClaimAmount( - data: string | undefined, +export function getClaimPayoutFromReceipt( + logs: ReceiptLog[] | undefined, + userAddress: string | undefined, ): string | null { - return decodeMerklClaimParams(data)?.totalAmount ?? null; + if (!logs?.length || !userAddress) { + return null; + } + + for (const log of logs) { + const topics = normalizeTopics(log.topics); + if (!topics || topics.length < 3) continue; + + const isTransferEvent = topics[0]?.toLowerCase() === ERC20_TRANSFER_TOPIC; + const isFromDistributor = + addressFromTopic(topics[1]) === MERKL_DISTRIBUTOR_ADDRESS.toLowerCase(); + const isToUser = addressFromTopic(topics[2]) === userAddress.toLowerCase(); + const isMuSDToken = + log.address?.toLowerCase() === MUSD_TOKEN_ADDRESS.toLowerCase(); + + if (isTransferEvent && isFromDistributor && isToUser && isMuSDToken) { + const amount = log.data; + if (!amount) continue; + return BigInt(amount).toString(); + } + } + + return null; +} + +function normalizeTopics( + topics: string | string[] | undefined, +): string[] | null { + if (!topics) return null; + if (Array.isArray(topics)) return topics; + // Shouldn't happen at runtime, but guard against the type definition + return null; +} + +function addressFromTopic(topic: string | undefined): string | undefined { + if (!topic || topic.length < 42) return undefined; + // Topic is a 32-byte hex, address is the last 20 bytes + return `0x${topic.slice(-40)}`.toLowerCase(); } diff --git a/app/components/UI/TransactionElement/utils.js b/app/components/UI/TransactionElement/utils.js index 91ef0de57e8..aca525f6586 100644 --- a/app/components/UI/TransactionElement/utils.js +++ b/app/components/UI/TransactionElement/utils.js @@ -37,7 +37,7 @@ import { hasTransactionType } from '../../Views/confirmations/utils/transaction' import { BigNumber } from 'bignumber.js'; import { convertMusdClaimAmount, - decodeMerklClaimAmount, + getClaimPayoutFromReceipt, } from '../Earn/utils/musd'; const POSITIVE_TRANSFER_TRANSACTION_TYPES = [ @@ -829,7 +829,8 @@ function decodeMusdClaimTx(args) { const { tx: { txParams, - txParams: { from, gas, data }, + txParams: { from, gas }, + txReceipt, hash, }, txChainId, @@ -843,8 +844,7 @@ function decodeMusdClaimTx(args) { const totalGas = calculateTotalGas(txParams); const renderFrom = renderFullAddress(from); - // Decode the claim amount from transaction data - const claimAmountRaw = decodeMerklClaimAmount(data); + const claimAmountRaw = getClaimPayoutFromReceipt(txReceipt?.logs, from); // Calculate display values let renderClaimAmount = strings('transaction.value_not_available'); diff --git a/app/components/Views/BrowserTab/GestureWebViewWrapper.test.tsx b/app/components/Views/BrowserTab/GestureWebViewWrapper.test.tsx index 347a51e67b3..3e9461445ac 100644 --- a/app/components/Views/BrowserTab/GestureWebViewWrapper.test.tsx +++ b/app/components/Views/BrowserTab/GestureWebViewWrapper.test.tsx @@ -18,6 +18,7 @@ import { PULL_THRESHOLD, SCROLL_TOP_THRESHOLD, PULL_ACTIVATION_ZONE, + PULL_MOVE_ACTIVATION, } from './constants'; import { GestureWebViewWrapper, @@ -28,6 +29,8 @@ import { type GestureCallback = (...args: unknown[]) => void; const capturedCallbacks: { onTouchesDown?: GestureCallback; + onTouchesMove?: GestureCallback; + onTouchesUp?: GestureCallback; onUpdate?: GestureCallback; onEnd?: GestureCallback; onFinalize?: GestureCallback; @@ -38,6 +41,8 @@ const capturedCallbacks: { interface MockPanGestureType { manualActivation: jest.Mock; onTouchesDown: jest.Mock; + onTouchesMove: jest.Mock; + onTouchesUp: jest.Mock; onUpdate: jest.Mock; onEnd: jest.Mock; onFinalize: jest.Mock; @@ -54,6 +59,20 @@ const mockPanGesture: MockPanGestureType = { capturedCallbacks.onTouchesDown = callback; return this; }), + onTouchesMove: jest.fn(function ( + this: MockPanGestureType, + callback: GestureCallback, + ) { + capturedCallbacks.onTouchesMove = callback; + return this; + }), + onTouchesUp: jest.fn(function ( + this: MockPanGestureType, + callback: GestureCallback, + ) { + capturedCallbacks.onTouchesUp = callback; + return this; + }), onUpdate: jest.fn(function ( this: MockPanGestureType, callback: GestureCallback, @@ -82,6 +101,8 @@ jest.mock('react-native-gesture-handler', () => ({ Pan: jest.fn(() => mockPanGesture), Native: jest.fn(() => ({})), Race: jest.fn((...gestures) => gestures[0]), + Exclusive: jest.fn((...gestures) => gestures[0]), + Simultaneous: jest.fn((...gestures) => gestures[0]), }, GestureDetector: ({ children }: { children: React.ReactNode }) => children, })); @@ -259,12 +280,12 @@ describe('GestureWebViewWrapper', () => { expect(Gesture.Native).toHaveBeenCalled(); }); - it('combines gestures with Race', () => { + it('combines gestures with Simultaneous', () => { const { Gesture } = jest.requireMock('react-native-gesture-handler'); renderComponent(); - expect(Gesture.Race).toHaveBeenCalled(); + expect(Gesture.Simultaneous).toHaveBeenCalled(); }); it('sets up onTouchesDown handler', () => { @@ -273,6 +294,18 @@ describe('GestureWebViewWrapper', () => { expect(mockPanGesture.onTouchesDown).toHaveBeenCalled(); }); + it('sets up onTouchesMove handler', () => { + renderComponent(); + + expect(mockPanGesture.onTouchesMove).toHaveBeenCalled(); + }); + + it('sets up onTouchesUp handler', () => { + renderComponent(); + + expect(mockPanGesture.onTouchesUp).toHaveBeenCalled(); + }); + it('sets up onUpdate handler', () => { renderComponent(); @@ -377,6 +410,8 @@ describe('GestureWebViewWrapper', () => { beforeEach(() => { // Clear captured callbacks before each test capturedCallbacks.onTouchesDown = undefined; + capturedCallbacks.onTouchesMove = undefined; + capturedCallbacks.onTouchesUp = undefined; capturedCallbacks.onUpdate = undefined; capturedCallbacks.onEnd = undefined; capturedCallbacks.onFinalize = undefined; @@ -507,8 +542,8 @@ describe('GestureWebViewWrapper', () => { }); }); - describe('pull-to-refresh touch', () => { - it('calls stateManager.activate for top zone touch when at scroll top', () => { + describe('pull-to-refresh touch (deferred activation)', () => { + it('defers activation for top zone touch when at scroll top (pending_refresh)', () => { const scrollY = mockSharedValue(0); const isRefreshing = mockSharedValue(false); renderComponent({ scrollY, isRefreshing }); @@ -517,9 +552,109 @@ describe('GestureWebViewWrapper', () => { capturedCallbacks.onTouchesDown?.(event, stateManager); + expect(stateManager.activate).not.toHaveBeenCalled(); + expect(stateManager.fail).not.toHaveBeenCalled(); + }); + + it('activates after sufficient downward movement in onTouchesMove', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + expect(stateManager.activate).not.toHaveBeenCalled(); + + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + expect(stateManager.activate).toHaveBeenCalled(); }); + it('fails on tap (finger lifts without significant movement)', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + expect(stateManager.activate).not.toHaveBeenCalled(); + expect(stateManager.fail).not.toHaveBeenCalled(); + + capturedCallbacks.onTouchesUp?.({}, stateManager); + + expect(stateManager.fail).toHaveBeenCalled(); + expect(stateManager.activate).not.toHaveBeenCalled(); + }); + + it('fails on horizontal movement (not a pull)', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + + const moveEvent = { + allTouches: [{ x: 200 + PULL_MOVE_ACTIVATION + 5, y: 25 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + + expect(stateManager.fail).toHaveBeenCalled(); + expect(stateManager.activate).not.toHaveBeenCalled(); + }); + + it('fails on upward movement (not a pull-down)', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + + const moveEvent = { + allTouches: [{ x: 200, y: 25 - PULL_MOVE_ACTIVATION - 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + + expect(stateManager.fail).toHaveBeenCalled(); + expect(stateManager.activate).not.toHaveBeenCalled(); + }); + + it('keeps waiting when movement is below threshold', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + + const moveEvent = { + allTouches: [{ x: 202, y: 28 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + + expect(stateManager.activate).not.toHaveBeenCalled(); + expect(stateManager.fail).not.toHaveBeenCalled(); + }); + it('calls stateManager.fail for top zone touch when already refreshing', () => { const scrollY = mockSharedValue(0); const isRefreshing = mockSharedValue(true); @@ -555,6 +690,42 @@ describe('GestureWebViewWrapper', () => { expect(stateManager.fail).toHaveBeenCalled(); }); + + it('fails in onTouchesMove when no touches in event', () => { + const scrollY = mockSharedValue(0); + const isRefreshing = mockSharedValue(false); + renderComponent({ scrollY, isRefreshing }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(200, 25), + stateManager, + ); + + const moveEvent = { allTouches: [] }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + + expect(stateManager.fail).toHaveBeenCalled(); + }); + + it('onTouchesMove returns early when gestureType is not pending_refresh', () => { + renderComponent({ backEnabled: true }); + const stateManager = createStateManager(); + + capturedCallbacks.onTouchesDown?.( + createTouchEvent(10, 200), + stateManager, + ); + expect(stateManager.activate).toHaveBeenCalled(); + + stateManager.activate.mockClear(); + stateManager.fail.mockClear(); + const moveEvent = { allTouches: [{ x: 10, y: 220 }] }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + + expect(stateManager.activate).not.toHaveBeenCalled(); + expect(stateManager.fail).not.toHaveBeenCalled(); + }); }); describe('onUpdate behavior', () => { @@ -674,18 +845,25 @@ describe('GestureWebViewWrapper', () => { capturedCallbacks.onFinalize?.(); }); - it('completes refresh gesture flow: touchDown -> update -> end', () => { + it('completes refresh gesture flow: touchDown -> touchesMove -> update -> end', () => { const scrollY = mockSharedValue(0); const isRefreshing = mockSharedValue(false); const onReload = jest.fn(); renderComponent({ scrollY, isRefreshing, onReload }); const stateManager = createStateManager(); - // Touch down in pull zone + // Touch down in pull zone (deferred) capturedCallbacks.onTouchesDown?.( createTouchEvent(200, 25), stateManager, ); + expect(stateManager.activate).not.toHaveBeenCalled(); + + // Move down to activate + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); expect(stateManager.activate).toHaveBeenCalled(); // Update with positive Y translation (pulling down) @@ -744,6 +922,13 @@ describe('GestureWebViewWrapper', () => { createTouchEvent(200, 25), stateManager, ); + + // Move down to activate first + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); + capturedCallbacks.onUpdate?.(createUpdateEvent(0, 30)); // Small pull capturedCallbacks.onEnd?.(createEndEvent(0, 30)); // Below threshold }); @@ -799,6 +984,11 @@ describe('GestureWebViewWrapper', () => { createTouchEvent(200, 25), stateManager, ); + // Activate via onTouchesMove first + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); capturedCallbacks.onUpdate?.(createUpdateEvent(0, -10)); // Upward movement }); @@ -812,6 +1002,11 @@ describe('GestureWebViewWrapper', () => { createTouchEvent(200, 25), stateManager, ); + // Activate via onTouchesMove first + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); capturedCallbacks.onUpdate?.(createUpdateEvent(0, 150)); // Large pull (1.5x threshold) }); }); @@ -873,17 +1068,24 @@ describe('GestureWebViewWrapper', () => { }); describe('handleRefresh callback', () => { - it('executes refresh flow without errors', () => { + it('executes refresh flow without errors (with deferred activation)', () => { const scrollY = mockSharedValue(0); const isRefreshing = mockSharedValue(false); renderComponent({ scrollY, isRefreshing }); const stateManager = createStateManager(); - // Touch down in pull zone + // Touch down in pull zone (deferred) capturedCallbacks.onTouchesDown?.( createTouchEvent(200, 25), stateManager, ); + expect(stateManager.activate).not.toHaveBeenCalled(); + + // Move down to activate + const moveEvent = { + allTouches: [{ x: 200, y: 25 + PULL_MOVE_ACTIVATION + 5 }], + }; + capturedCallbacks.onTouchesMove?.(moveEvent, stateManager); expect(stateManager.activate).toHaveBeenCalled(); // Pull down enough to trigger refresh (threshold is 80px) @@ -1080,6 +1282,17 @@ describe('GestureWebViewWrapper constants', () => { expect(PULL_ACTIVATION_ZONE).toBe(50); }); }); + + describe('PULL_MOVE_ACTIVATION', () => { + it('returns positive pixel value for pull movement threshold', () => { + expect(PULL_MOVE_ACTIVATION).toBeGreaterThan(0); + expect(typeof PULL_MOVE_ACTIVATION).toBe('number'); + }); + + it('uses 10 pixels as pull movement activation threshold', () => { + expect(PULL_MOVE_ACTIVATION).toBe(10); + }); + }); }); describe('GestureWebViewWrapper gesture zone calculations', () => { diff --git a/app/components/Views/BrowserTab/GestureWebViewWrapper.tsx b/app/components/Views/BrowserTab/GestureWebViewWrapper.tsx index 27c39e1b47d..2902c525504 100644 --- a/app/components/Views/BrowserTab/GestureWebViewWrapper.tsx +++ b/app/components/Views/BrowserTab/GestureWebViewWrapper.tsx @@ -18,6 +18,7 @@ import { PULL_THRESHOLD, SCROLL_TOP_THRESHOLD, PULL_ACTIVATION_ZONE, + PULL_MOVE_ACTIVATION, } from './constants'; const styles = StyleSheet.create({ @@ -65,8 +66,10 @@ export interface GestureWebViewWrapperProps { * - Right edge swipe: Navigate forward * - Pull-to-refresh: Reload page (when scrolled to top) * - * Uses Gesture.Race with manualActivation(true) to coordinate gestures with WebView's - * native touch handling. + * Uses Gesture.Simultaneous with manualActivation(true) to coordinate gestures with + * WebView's native touch handling. Simultaneous allows both our Pan and the Native + * gesture to coexist so that taps pass through while pull-to-refresh uses deferred + * activation. */ export const GestureWebViewWrapper: React.FC = ({ isTabActive, @@ -90,12 +93,14 @@ export const GestureWebViewWrapper: React.FC = ({ const pullProgress = useSharedValue(0); const isPulling = useSharedValue(false); const pullHapticTriggered = useSharedValue(false); + const initialTouchY = useSharedValue(0); + const initialTouchX = useSharedValue(0); const swipeStartTime = useRef(0); const swipeProgress = useSharedValue(0); const swipeDirection = useSharedValue<'back' | 'forward' | null>(null); - const gestureType = useSharedValue<'back' | 'forward' | 'refresh' | null>( - null, - ); + const gestureType = useSharedValue< + 'back' | 'forward' | 'refresh' | 'pending_refresh' | null + >(null); // Navigation state as shared values - prevents stale reads in worklets // These are synced from props to ensure real-time access in UI thread @@ -244,19 +249,53 @@ export const GestureWebViewWrapper: React.FC = ({ stateManager.activate(); runOnJS(triggerHapticFeedback)(ImpactFeedbackStyle.Light); } else if (canPullToRefresh) { + gestureType.value = 'pending_refresh'; + initialTouchY.value = y; + initialTouchX.value = x; + } else { + stateManager.fail(); + } + }) + .onTouchesMove((event, stateManager) => { + 'worklet'; + if (gestureType.value !== 'pending_refresh') return; + + const touch = event.allTouches[0]; + if (!touch) { + gestureType.value = null; + stateManager.fail(); + return; + } + + const deltaY = touch.y - initialTouchY.value; + const deltaX = Math.abs(touch.x - initialTouchX.value); + + if (deltaY > PULL_MOVE_ACTIVATION && deltaY > deltaX) { gestureType.value = 'refresh'; isPulling.value = true; pullProgress.value = 0; pullHapticTriggered.value = false; stateManager.activate(); - } else { + } else if ( + deltaX > PULL_MOVE_ACTIVATION || + deltaY < -PULL_MOVE_ACTIVATION + ) { + gestureType.value = null; + stateManager.fail(); + } + }) + .onTouchesUp((_event, stateManager) => { + 'worklet'; + if (gestureType.value === 'pending_refresh') { + gestureType.value = null; stateManager.fail(); } }) .onUpdate((event) => { 'worklet'; const currentGestureType = gestureType.value; - if (!currentGestureType) return; + if (!currentGestureType || currentGestureType === 'pending_refresh') + return; const swipeDistance = screenWidth * SWIPE_THRESHOLD; @@ -297,8 +336,7 @@ export const GestureWebViewWrapper: React.FC = ({ .onEnd((event) => { 'worklet'; const currentGestureType = gestureType.value; - if (!currentGestureType) { - // Reset animations directly on UI thread + if (!currentGestureType || currentGestureType === 'pending_refresh') { swipeProgress.value = withTiming(0, { duration: 200 }); swipeDirection.value = null; pullProgress.value = withTiming(0, { duration: 200 }); @@ -311,14 +349,12 @@ export const GestureWebViewWrapper: React.FC = ({ if (currentGestureType === 'back') { if (event.translationX >= swipeDistance) { - // Only runOnJS for callbacks that need JS thread (navigation, analytics) runOnJS(handleSwipeNavigation)( 'back', event.translationX, duration, ); } - // Reset swipe animation directly on UI thread swipeProgress.value = withTiming(0, { duration: 200 }); swipeDirection.value = null; } else if (currentGestureType === 'forward') { @@ -336,7 +372,6 @@ export const GestureWebViewWrapper: React.FC = ({ pullDistanceRef.current = event.translationY; runOnJS(handleRefresh)(); } - // Reset pull animation directly on UI thread pullProgress.value = withTiming(0, { duration: 200 }); isPulling.value = false; } @@ -367,10 +402,15 @@ export const GestureWebViewWrapper: React.FC = ({ ); /** - * Combined gesture - uses Race so our gesture takes priority when activated + * Combined gesture - Simultaneous allows both our Pan and the WebView's Native + * gesture to coexist. This is critical for deferred pull-to-refresh activation: + * while Pan is in BEGAN (deciding if touch is a tap vs pull), Native independently + * handles the touch so taps pass through to the WebView. When Pan activates for + * a real pull, both gestures are active but since we're at the top of the page, + * the WebView has nothing to scroll. */ const combinedWebViewGesture = useMemo( - () => Gesture.Race(fullyUnifiedGesture, webViewNativeGesture), + () => Gesture.Simultaneous(fullyUnifiedGesture, webViewNativeGesture), [webViewNativeGesture, fullyUnifiedGesture], ); diff --git a/app/components/Views/BrowserTab/constants.ts b/app/components/Views/BrowserTab/constants.ts index 4159141f42b..f5e63cc0edc 100644 --- a/app/components/Views/BrowserTab/constants.ts +++ b/app/components/Views/BrowserTab/constants.ts @@ -49,3 +49,11 @@ export const SCROLL_TOP_THRESHOLD = 5; * WebView interactions (tapping links, buttons, etc.). */ export const PULL_ACTIVATION_ZONE = 50; + +/** + * Minimum downward movement in pixels before activating pull-to-refresh. + * This prevents taps on buttons near the top of the page from being + * swallowed by the gesture handler. The gesture stays in a "pending" + * state until the finger moves down by at least this amount. + */ +export const PULL_MOVE_ACTIVATION = 10; diff --git a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap index eadf69d1281..88901f22f28 100644 --- a/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/Wallet/__snapshots__/index.test.tsx.snap @@ -656,7 +656,7 @@ exports[`Wallet Conditional Rendering should render banner when basic functional { expect(wrapper.toJSON()).toMatchSnapshot(); }); + it('calls AccountTrackerController.refresh when selectedInternalAccount changes', async () => { + const refreshMock = jest.mocked( + Engine.context.AccountTrackerController.refresh, + ); + + //@ts-expect-error we are ignoring the navigation params on purpose + render(Wallet); + await waitFor(() => expect(refreshMock).toHaveBeenCalled()); + refreshMock.mockClear(); + + renderScreen( + // @ts-expect-error we are ignoring the navigation params on purpose + Wallet, + { name: Routes.WALLET_VIEW }, + { + state: { + ...mockInitialState, + engine: { + backgroundState: { + ...mockInitialState.engine.backgroundState, + AccountsController: { + ...mockInitialState.engine.backgroundState.AccountsController, + internalAccounts: { + ...mockInitialState.engine.backgroundState.AccountsController + .internalAccounts, + selectedAccount: 'different-account-id', + }, + }, + }, + }, + }, + }, + ); + + await waitFor(() => expect(refreshMock).toHaveBeenCalled()); + }); + // Simple test to verify mock setup it('should have proper mock setup', () => { expect(typeof jest.fn()).toBe('function'); diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index bdce7cf502b..9c4cf76a90d 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -1063,7 +1063,7 @@ const Wallet = ({ /* eslint-disable-next-line */ // TODO: The need of usage of this chainId as a dependency is not clear, we shouldn't need to refresh the native balances when the chainId changes. Since the pooling is always working in the back. Check with assets team. // TODO: [SOLANA] Check if this logic supports non evm networks before shipping Solana - [navigation, chainId, evmNetworkConfigurations], + [navigation, chainId, evmNetworkConfigurations, selectedInternalAccount], ); const shouldDisplayCardButton = useSelector(selectDisplayCardButton); diff --git a/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.test.tsx b/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.test.tsx index 90b6b2d8642..a1e8beaa100 100644 --- a/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.test.tsx +++ b/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.test.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { Interface } from '@ethersproject/abi'; import renderWithProvider from '../../../../../../util/test/renderWithProvider'; import { useTransactionDetails } from '../../../hooks/activity/useTransactionDetails'; import { @@ -10,7 +9,8 @@ import { TransactionDetailsHero } from './transaction-details-hero'; import { merge } from 'lodash'; import { otherControllersMock } from '../../../__mocks__/controllers/other-controllers-mock'; import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance'; -import { DISTRIBUTOR_CLAIM_ABI } from '../../../../../UI/Earn/components/MerklRewards/constants'; +import { MERKL_DISTRIBUTOR_ADDRESS } from '../../../../../UI/Earn/components/MerklRewards/constants'; +import { MUSD_TOKEN_ADDRESS } from '../../../../../UI/Earn/constants/musd'; jest.mock('../../../hooks/activity/useTransactionDetails'); jest.mock('../../../hooks/tokens/useTokenWithBalance'); @@ -170,23 +170,32 @@ describe('TransactionDetailsHero', () => { it('renders claim amount for musdClaim with valid claim data', () => { const USER_ADDRESS = '0x1234567890123456789012345678901234567890'; - const TOKEN_ADDRESS = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; const claimAmount = '75500000'; // 75.5 mUSD (6 decimals) - const contractInterface = new Interface(DISTRIBUTOR_CLAIM_ABI); - const claimData = contractInterface.encodeFunctionData('claim', [ - [USER_ADDRESS], - [TOKEN_ADDRESS], - [claimAmount], - [[]], - ]); + const ERC20_TRANSFER_TOPIC = + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; + const mockLogs = [ + { + address: MUSD_TOKEN_ADDRESS, + data: '0x' + BigInt(claimAmount).toString(16).padStart(64, '0'), + topics: [ + ERC20_TRANSFER_TOPIC, + '0x000000000000000000000000' + + MERKL_DISTRIBUTOR_ADDRESS.slice(2).toLowerCase(), + '0x000000000000000000000000' + USER_ADDRESS.slice(2).toLowerCase(), + ], + }, + ]; useTransactionDetailsMock.mockReturnValue({ transactionMeta: { ...TRANSACTION_META_MOCK, type: TransactionType.musdClaim, txParams: { - data: claimData, + from: USER_ADDRESS, + }, + txReceipt: { + logs: mockLogs, }, } as unknown as TransactionMeta, }); diff --git a/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.tsx b/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.tsx index d69d8b83ef9..b0b102f9935 100644 --- a/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.tsx +++ b/app/components/Views/confirmations/components/activity/transaction-details-hero/transaction-details-hero.tsx @@ -22,7 +22,7 @@ import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance'; import { BigNumber } from 'bignumber.js'; import { convertMusdClaimAmount, - decodeMerklClaimAmount, + getClaimPayoutFromReceipt, } from '../../../../../UI/Earn/utils/musd'; import { selectConversionRateByChainId, @@ -130,8 +130,13 @@ function useClaimAmount(): { amount: BigNumber | null; isConverted: boolean } { return { amount: null, isConverted: false }; } - const { data } = transactionMeta.txParams ?? {}; - const claimAmountRaw = decodeMerklClaimAmount(data as string); + const { from } = transactionMeta.txParams ?? {}; + const claimAmountRaw = getClaimPayoutFromReceipt( + transactionMeta.txReceipt?.logs as Parameters< + typeof getClaimPayoutFromReceipt + >[0], + from as string, + ); if (!claimAmountRaw) { return { amount: null, isConverted: false }; diff --git a/app/components/Views/confirmations/hooks/earn/useMerklClaimAmount.ts b/app/components/Views/confirmations/hooks/earn/useMerklClaimAmount.ts index 85ba6669569..a0fdb878ffa 100644 --- a/app/components/Views/confirmations/hooks/earn/useMerklClaimAmount.ts +++ b/app/components/Views/confirmations/hooks/earn/useMerklClaimAmount.ts @@ -10,6 +10,7 @@ import { useAsyncResult } from '../../../../hooks/useAsyncResult'; import { convertMusdClaimAmount, ConvertMusdClaimResult, + getClaimPayoutFromReceipt, getUnclaimedAmountForMerklClaimTx, } from '../../../../UI/Earn/utils/musd'; @@ -23,26 +24,44 @@ interface MerklClaimAmountResult { /** * Hook that computes the actual claimable (unclaimed) amount for a Merkl mUSD claim transaction. * - * The transaction calldata contains the cumulative total reward, not the per-claim payout. - * The Merkl Distributor contract computes: payout = totalAmount - alreadyClaimed. - * This hook reads the already-claimed amount from the contract and returns the unclaimed portion. + * For confirmed transactions: extracts the actual payout from the receipt's Transfer event logs. + * For pending transactions: computes payout = totalAmount - alreadyClaimed via contract call. */ const useMerklClaimAmount = ( transaction: TransactionMeta, conversionRate: BigNumber, usdConversionRate: number, ): MerklClaimAmountResult => { - const { chainId, txParams, type: transactionType } = transaction; + const { chainId, txParams, txReceipt, type: transactionType } = transaction; + // For confirmed txs, extract the actual payout from receipt Transfer logs (synchronous) + const receiptPayout = useMemo(() => { + if (transactionType !== TransactionType.musdClaim) return null; + return getClaimPayoutFromReceipt( + txReceipt?.logs as Parameters[0], + txParams?.from as string, + ); + }, [transactionType, txReceipt?.logs, txParams?.from]); + + // For pending txs (no receipt yet): compute payout = totalAmount - alreadyClaimed const { value: claimAmountResult, pending } = useAsyncResult(async () => { if (transactionType !== TransactionType.musdClaim) return null; + if (receiptPayout) return null; return getUnclaimedAmountForMerklClaimTx( txParams?.data as string | undefined, chainId as Hex, ); - }, [transactionType, txParams?.data, chainId]); + }, [transactionType, txParams?.data, chainId, receiptPayout]); const claimAmount = useMemo(() => { + if (receiptPayout) { + return convertMusdClaimAmount({ + claimAmountRaw: receiptPayout, + conversionRate, + usdConversionRate, + }); + } + if (pending || !claimAmountResult) return null; return convertMusdClaimAmount({ @@ -50,9 +69,15 @@ const useMerklClaimAmount = ( conversionRate, usdConversionRate, }); - }, [pending, claimAmountResult, conversionRate, usdConversionRate]); + }, [ + receiptPayout, + pending, + claimAmountResult, + conversionRate, + usdConversionRate, + ]); - return { pending, claimAmount }; + return { pending: !receiptPayout && pending, claimAmount }; }; export default useMerklClaimAmount; diff --git a/app/core/Engine/controllers/storage-service-init.test.ts b/app/core/Engine/controllers/storage-service-init.test.ts index 56581f3e717..a2be01a3c5e 100644 --- a/app/core/Engine/controllers/storage-service-init.test.ts +++ b/app/core/Engine/controllers/storage-service-init.test.ts @@ -329,9 +329,6 @@ describe('mobileStorageAdapter', () => { expect(mockFilesystemStorage.removeItem).toHaveBeenCalledWith( `${STORAGE_KEY_PREFIX}TestController:key2`, ); - expect(mockLogger.log).toHaveBeenCalledWith( - 'StorageService: Cleared 2 keys for TestController', - ); }); it('returns early when getAllKeys returns null', async () => { @@ -345,7 +342,7 @@ describe('mobileStorageAdapter', () => { expect(mockFilesystemStorage.removeItem).not.toHaveBeenCalled(); }); - it('removes zero keys and logs count when namespace has no matching entries', async () => { + it('removes zero keys when namespace has no matching entries', async () => { mockFilesystemStorage.getAllKeys.mockResolvedValue([ `${STORAGE_KEY_PREFIX}OtherController:key1`, ]); @@ -355,9 +352,6 @@ describe('mobileStorageAdapter', () => { await adapter.clear('TestController'); expect(mockFilesystemStorage.removeItem).not.toHaveBeenCalled(); - expect(mockLogger.log).toHaveBeenCalledWith( - 'StorageService: Cleared 0 keys for TestController', - ); }); it('throws and logs error when FilesystemStorage fails', async () => { @@ -379,4 +373,305 @@ describe('mobileStorageAdapter', () => { ); }); }); + + describe('key encoding', () => { + describe('setItem', () => { + beforeEach(() => { + mockFilesystemStorage.setItem.mockResolvedValue(undefined); + mockDevice.isIos.mockReturnValue(true); + }); + + it('encodes hyphens in keys as %2D', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('TestController', 'simple-key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:simple%2Dkey`, + JSON.stringify('value'), + true, + ); + }); + + it('encodes slashes in keys as %2F', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('TestController', 'nested/path/key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:nested%2Fpath%2Fkey`, + JSON.stringify('value'), + true, + ); + }); + + it('encodes percent signs in keys as %25', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('TestController', 'percent%key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:percent%25key`, + JSON.stringify('value'), + true, + ); + }); + + it('encodes mixed special characters in keys', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem( + 'TestController', + 'mixed-key/with%special', + 'value', + ); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:mixed%2Dkey%2Fwith%25special`, + JSON.stringify('value'), + true, + ); + }); + + it('does not encode colons in keys', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem( + 'TestController', + 'tokensChainsCache:0x1', + 'value', + ); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:tokensChainsCache:0x1`, + JSON.stringify('value'), + true, + ); + }); + }); + + describe('getItem', () => { + it('encodes hyphens in keys when retrieving', async () => { + mockFilesystemStorage.getItem.mockResolvedValue(JSON.stringify('data')); + const adapter = getStorageAdapter(); + + await adapter.getItem('TestController', 'simple-key'); + + expect(mockFilesystemStorage.getItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:simple%2Dkey`, + ); + }); + + it('encodes slashes in keys when retrieving', async () => { + mockFilesystemStorage.getItem.mockResolvedValue(JSON.stringify('data')); + const adapter = getStorageAdapter(); + + await adapter.getItem('TestController', 'nested/path/key'); + + expect(mockFilesystemStorage.getItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:nested%2Fpath%2Fkey`, + ); + }); + + it('encodes snap IDs with special characters', async () => { + mockFilesystemStorage.getItem.mockResolvedValue( + JSON.stringify({ sourceCode: '...' }), + ); + const adapter = getStorageAdapter(); + + await adapter.getItem( + 'SnapController', + 'npm:@metamask/bip32-keyring-snap', + ); + + expect(mockFilesystemStorage.getItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}SnapController:npm:@metamask%2Fbip32%2Dkeyring%2Dsnap`, + ); + }); + }); + + describe('removeItem', () => { + it('encodes hyphens in keys when removing', async () => { + mockFilesystemStorage.removeItem.mockResolvedValue(undefined); + const adapter = getStorageAdapter(); + + await adapter.removeItem('TestController', 'simple-key'); + + expect(mockFilesystemStorage.removeItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:simple%2Dkey`, + ); + }); + + it('encodes slashes in keys when removing', async () => { + mockFilesystemStorage.removeItem.mockResolvedValue(undefined); + const adapter = getStorageAdapter(); + + await adapter.removeItem('TestController', 'nested/path/key'); + + expect(mockFilesystemStorage.removeItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TestController:nested%2Fpath%2Fkey`, + ); + }); + }); + + describe('getAllKeys', () => { + it('decodes %2D back to hyphens in returned keys', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}TestController:simple%2Dkey`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('TestController'); + + expect(result).toStrictEqual(['simple-key']); + }); + + it('decodes %2F back to slashes in returned keys', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}TestController:nested%2Fpath%2Fkey`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('TestController'); + + expect(result).toStrictEqual(['nested/path/key']); + }); + + it('decodes %25 back to percent signs in returned keys', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}TestController:percent%25key`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('TestController'); + + expect(result).toStrictEqual(['percent%key']); + }); + + it('decodes mixed encoded characters in returned keys', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}TestController:mixed%2Dkey%2Fwith%25special`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('TestController'); + + expect(result).toStrictEqual(['mixed-key/with%special']); + }); + + it('decodes snap IDs with special characters', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}SnapController:npm:@metamask%2Fbip32%2Dkeyring%2Dsnap`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('SnapController'); + + expect(result).toStrictEqual(['npm:@metamask/bip32-keyring-snap']); + }); + + it('returns multiple decoded keys correctly', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}TestController:simple%2Dkey`, + `${STORAGE_KEY_PREFIX}TestController:nested%2Fpath`, + `${STORAGE_KEY_PREFIX}TestController:safe_key`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('TestController'); + + expect(result).toStrictEqual(['simple-key', 'nested/path', 'safe_key']); + }); + }); + + describe('namespace encoding', () => { + beforeEach(() => { + mockFilesystemStorage.setItem.mockResolvedValue(undefined); + mockDevice.isIos.mockReturnValue(true); + }); + + it('encodes hyphens in namespace as %2D', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('Test-Controller', 'key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}Test%2DController:key`, + JSON.stringify('value'), + true, + ); + }); + + it('encodes slashes in namespace as %2F', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('Test/Controller', 'key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}Test%2FController:key`, + JSON.stringify('value'), + true, + ); + }); + + it('encodes both namespace and key with special characters', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem('My-Controller', 'nested/path-key', 'value'); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}My%2DController:nested%2Fpath%2Dkey`, + JSON.stringify('value'), + true, + ); + }); + + it('does not change namespaces without special characters', async () => { + const adapter = getStorageAdapter(); + + await adapter.setItem( + 'TokenListController', + 'tokensChainsCache:0x1', + 'value', + ); + + expect(mockFilesystemStorage.setItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}TokenListController:tokensChainsCache:0x1`, + JSON.stringify('value'), + true, + ); + }); + + it('getAllKeys uses encoded namespace for prefix matching', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}My%2DController:key1`, + `${STORAGE_KEY_PREFIX}My%2DController:key2`, + ]); + const adapter = getStorageAdapter(); + + const result = await adapter.getAllKeys('My-Controller'); + + expect(result).toStrictEqual(['key1', 'key2']); + }); + + it('clear uses encoded namespace for prefix matching', async () => { + mockFilesystemStorage.getAllKeys.mockResolvedValue([ + `${STORAGE_KEY_PREFIX}My%2DController:key1`, + `${STORAGE_KEY_PREFIX}My%2DController:key2`, + ]); + mockFilesystemStorage.removeItem.mockResolvedValue(undefined); + const adapter = getStorageAdapter(); + + await adapter.clear('My-Controller'); + + expect(mockFilesystemStorage.removeItem).toHaveBeenCalledTimes(2); + expect(mockFilesystemStorage.removeItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}My%2DController:key1`, + ); + expect(mockFilesystemStorage.removeItem).toHaveBeenCalledWith( + `${STORAGE_KEY_PREFIX}My%2DController:key2`, + ); + }); + }); + }); }); diff --git a/app/core/Engine/controllers/storage-service-init.ts b/app/core/Engine/controllers/storage-service-init.ts index 088fdfe9210..48942fa683e 100644 --- a/app/core/Engine/controllers/storage-service-init.ts +++ b/app/core/Engine/controllers/storage-service-init.ts @@ -11,6 +11,10 @@ import { } from '@metamask/storage-service'; import Device from '../../../util/device'; import Logger from '../../../util/Logger'; +import { + encodeStorageKey, + decodeStorageKey, +} from '../utils/storage-service-utils'; /** * Mobile-specific storage adapter using FilesystemStorage. @@ -30,8 +34,11 @@ const mobileStorageAdapter: StorageAdapter = { */ async getItem(namespace: string, key: string): Promise { try { - // Build full key: storageService:namespace:key - const fullKey = `${STORAGE_KEY_PREFIX}${namespace}:${key}`; + // Build full key: storageService:encodedNamespace:encodedKey + const encodedNamespace = encodeStorageKey(namespace); + const encodedKey = encodeStorageKey(key); + const fullKey = `${STORAGE_KEY_PREFIX}${encodedNamespace}:${encodedKey}`; + const serialized = await FilesystemStorage.getItem(fullKey); // Key not found - return empty object @@ -59,8 +66,10 @@ const mobileStorageAdapter: StorageAdapter = { */ async setItem(namespace: string, key: string, value: Json): Promise { try { - // Build full key: storageService:namespace:key - const fullKey = `${STORAGE_KEY_PREFIX}${namespace}:${key}`; + // Build full key: storageService:encodedNamespace:encodedKey + const encodedNamespace = encodeStorageKey(namespace); + const encodedKey = encodeStorageKey(key); + const fullKey = `${STORAGE_KEY_PREFIX}${encodedNamespace}:${encodedKey}`; await FilesystemStorage.setItem( fullKey, @@ -83,8 +92,11 @@ const mobileStorageAdapter: StorageAdapter = { */ async removeItem(namespace: string, key: string): Promise { try { - // Build full key: storageService:namespace:key - const fullKey = `${STORAGE_KEY_PREFIX}${namespace}:${key}`; + // Build full key: storageService:encodedNamespace:encodedKey + const encodedNamespace = encodeStorageKey(namespace); + const encodedKey = encodeStorageKey(key); + const fullKey = `${STORAGE_KEY_PREFIX}${encodedNamespace}:${encodedKey}`; + await FilesystemStorage.removeItem(fullKey); } catch (error) { Logger.error(error as Error, { @@ -109,11 +121,19 @@ const mobileStorageAdapter: StorageAdapter = { return []; } - const prefix = `${STORAGE_KEY_PREFIX}${namespace}:`; + // Encode namespace to match how keys were stored + const encodedNamespace = encodeStorageKey(namespace); + const prefix = `${STORAGE_KEY_PREFIX}${encodedNamespace}:`; - return allKeys - .filter((key) => key.startsWith(prefix)) - .map((key) => key.slice(prefix.length)); + const filteredKeys = allKeys + .filter((rawKey) => rawKey.startsWith(prefix)) + .map((rawKey) => { + // Extract the encoded key part and decode it + const encodedKeyPart = rawKey.slice(prefix.length); + return decodeStorageKey(encodedKeyPart); + }); + + return filteredKeys; } catch (error) { Logger.error(error as Error, { message: `StorageService: Failed to get keys for ${namespace}`, @@ -135,11 +155,18 @@ const mobileStorageAdapter: StorageAdapter = { return; } - const prefix = `${STORAGE_KEY_PREFIX}${namespace}:`; - const keysToDelete = allKeys.filter((key) => key.startsWith(prefix)); + // Encode namespace to match how keys were stored + const encodedNamespace = encodeStorageKey(namespace); + const prefix = `${STORAGE_KEY_PREFIX}${encodedNamespace}:`; + + const keysToDelete = allKeys.filter((rawKey) => + rawKey.startsWith(prefix), + ); + // For deletion, we pass the raw key as returned by getAllKeys. + // FilesystemStorage.removeItem will apply toFileName to find the file. await Promise.all( - keysToDelete.map((key) => FilesystemStorage.removeItem(key)), + keysToDelete.map((rawKey) => FilesystemStorage.removeItem(rawKey)), ); Logger.log( diff --git a/app/core/Engine/controllers/transaction-controller/metrics_properties/base.test.ts b/app/core/Engine/controllers/transaction-controller/metrics_properties/base.test.ts index 116ace0682d..e00d27c44e3 100644 --- a/app/core/Engine/controllers/transaction-controller/metrics_properties/base.test.ts +++ b/app/core/Engine/controllers/transaction-controller/metrics_properties/base.test.ts @@ -101,6 +101,8 @@ describe('getTransactionTypeValue', () => { ['predict_claim', TransactionType.predictClaim], ['predict_deposit', TransactionType.predictDeposit], ['predict_withdraw', TransactionType.predictWithdraw], + ['musd_conversion', TransactionType.musdConversion], + ['musd_claim', TransactionType.musdClaim], ])('returns %s if nested transaction type is %s', (expected, nestedType) => { const mockTransactionMeta = { type: TransactionType.simpleSend, diff --git a/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts b/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts index ee90992dc6a..0295e1441b8 100644 --- a/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts +++ b/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts @@ -70,6 +70,14 @@ export function getTransactionTypeValue( return 'predict_claim'; } + if (hasTransactionType(transactionMeta, [TransactionType.musdConversion])) { + return 'musd_conversion'; + } + + if (hasTransactionType(transactionMeta, [TransactionType.musdClaim])) { + return 'musd_claim'; + } + switch (transactionType) { case TransactionType.bridgeApproval: return 'bridge_approval'; diff --git a/app/core/Engine/utils/storage-service-utils.test.ts b/app/core/Engine/utils/storage-service-utils.test.ts new file mode 100644 index 00000000000..25a183ab314 --- /dev/null +++ b/app/core/Engine/utils/storage-service-utils.test.ts @@ -0,0 +1,176 @@ +import { encodeStorageKey, decodeStorageKey } from './storage-service-utils'; + +describe('storage-service-utils', () => { + describe('encodeStorageKey', () => { + it('encodes hyphens as %2D', () => { + const result = encodeStorageKey('simple-key'); + + expect(result).toBe('simple%2Dkey'); + }); + + it('encodes slashes as %2F', () => { + const result = encodeStorageKey('nested/path/key'); + + expect(result).toBe('nested%2Fpath%2Fkey'); + }); + + it('encodes percent signs as %25', () => { + const result = encodeStorageKey('percent%key'); + + expect(result).toBe('percent%25key'); + }); + + it('encodes multiple hyphens', () => { + const result = encodeStorageKey('key-with-multiple-hyphens'); + + expect(result).toBe('key%2Dwith%2Dmultiple%2Dhyphens'); + }); + + it('encodes multiple slashes', () => { + const result = encodeStorageKey('a/b/c/d'); + + expect(result).toBe('a%2Fb%2Fc%2Fd'); + }); + + it('encodes mixed special characters', () => { + const result = encodeStorageKey('mixed-key/with%special'); + + expect(result).toBe('mixed%2Dkey%2Fwith%25special'); + }); + + it('encodes snap IDs with npm scope', () => { + const result = encodeStorageKey('npm:@metamask/bip32-keyring-snap'); + + expect(result).toBe('npm:@metamask%2Fbip32%2Dkeyring%2Dsnap'); + }); + + it('does not encode colons', () => { + const result = encodeStorageKey('tokensChainsCache:0x1'); + + expect(result).toBe('tokensChainsCache:0x1'); + }); + + it('does not encode underscores', () => { + const result = encodeStorageKey('safe_key_name'); + + expect(result).toBe('safe_key_name'); + }); + + it('does not encode alphanumeric characters', () => { + const result = encodeStorageKey('SimpleKey123'); + + expect(result).toBe('SimpleKey123'); + }); + + it('returns empty string for empty input', () => { + const result = encodeStorageKey(''); + + expect(result).toBe(''); + }); + + it('encodes percent first to avoid double-encoding', () => { + const result = encodeStorageKey('key%2Dalready'); + + expect(result).toBe('key%252Dalready'); + }); + }); + + describe('decodeStorageKey', () => { + it('decodes %2D back to hyphens', () => { + const result = decodeStorageKey('simple%2Dkey'); + + expect(result).toBe('simple-key'); + }); + + it('decodes %2F back to slashes', () => { + const result = decodeStorageKey('nested%2Fpath%2Fkey'); + + expect(result).toBe('nested/path/key'); + }); + + it('decodes %25 back to percent signs', () => { + const result = decodeStorageKey('percent%25key'); + + expect(result).toBe('percent%key'); + }); + + it('decodes multiple encoded hyphens', () => { + const result = decodeStorageKey('key%2Dwith%2Dmultiple%2Dhyphens'); + + expect(result).toBe('key-with-multiple-hyphens'); + }); + + it('decodes multiple encoded slashes', () => { + const result = decodeStorageKey('a%2Fb%2Fc%2Fd'); + + expect(result).toBe('a/b/c/d'); + }); + + it('decodes mixed encoded characters', () => { + const result = decodeStorageKey('mixed%2Dkey%2Fwith%25special'); + + expect(result).toBe('mixed-key/with%special'); + }); + + it('decodes snap IDs with npm scope', () => { + const result = decodeStorageKey('npm:@metamask%2Fbip32%2Dkeyring%2Dsnap'); + + expect(result).toBe('npm:@metamask/bip32-keyring-snap'); + }); + + it('handles lowercase encoding', () => { + const result = decodeStorageKey('key%2dvalue%2fpath'); + + expect(result).toBe('key-value/path'); + }); + + it('handles uppercase encoding', () => { + const result = decodeStorageKey('key%2Dvalue%2Fpath'); + + expect(result).toBe('key-value/path'); + }); + + it('returns empty string for empty input', () => { + const result = decodeStorageKey(''); + + expect(result).toBe(''); + }); + + it('returns unencoded strings unchanged', () => { + const result = decodeStorageKey('SimpleKey123'); + + expect(result).toBe('SimpleKey123'); + }); + }); + + describe('encode/decode roundtrip', () => { + const testCases = [ + 'simple-key', + 'nested/path/key', + 'mixed-key/with/path', + 'percent%encoded', + 'npm:@metamask/bip32-keyring-snap', + 'tokensChainsCache:0x1', + 'cache:0x1:tokens', + 'safe_key', + 'CamelCaseKey', + 'complex-key/with%special-chars', + '', + ]; + + it.each(testCases)('roundtrips "%s" correctly', (original) => { + const encoded = encodeStorageKey(original); + const decoded = decodeStorageKey(encoded); + + expect(decoded).toBe(original); + }); + + it('handles double encoding prevention', () => { + const original = 'key%2Dalready-encoded'; + const encoded = encodeStorageKey(original); + const decoded = decodeStorageKey(encoded); + + expect(decoded).toBe(original); + }); + }); +}); diff --git a/app/core/Engine/utils/storage-service-utils.ts b/app/core/Engine/utils/storage-service-utils.ts new file mode 100644 index 00000000000..ec8c7e8f8fc --- /dev/null +++ b/app/core/Engine/utils/storage-service-utils.ts @@ -0,0 +1,35 @@ +/** + * Utility functions for StorageService key encoding/decoding. + * + * These functions handle the quirks of redux-persist-filesystem-storage: + * 1. `/` in keys creates subdirectories, making keys unreachable via getAllKeys + * 2. `-` gets converted to `:` by fromFileName, corrupting the key + * + * We encode `-` and `/` but not `:` because we already have keys with colons + * in production (like `tokensChainsCache:0x1`), so encoding `:` would break + * existing data. + * + * We use URI-style encoding (%XX) for these characters because it's a + * well-understood, reversible format. + */ + +/** + * Encode a string to avoid issues with redux-persist-filesystem-storage. + * + * @param key - The string to encode (namespace or key). + * @returns The encoded string safe for filesystem storage. + */ +export const encodeStorageKey = (key: string): string => + key + .replace(/%/g, '%25') // Encode % first to avoid double-encoding + .replace(/\//g, '%2F') // Encode slashes (would create subdirectories) + .replace(/-/g, '%2D'); // Encode hyphens (would be converted to colons) + +/** + * Decode a key that was encoded with encodeStorageKey. + * + * @param encodedKey - The encoded key to decode. + * @returns The original key. + */ +export const decodeStorageKey = (encodedKey: string): string => + encodedKey.replace(/%2D/gi, '-').replace(/%2F/gi, '/').replace(/%25/g, '%'); diff --git a/app/store/migrations/119.test.ts b/app/store/migrations/119.test.ts index 5629127e5b7..c3ea4979706 100644 --- a/app/store/migrations/119.test.ts +++ b/app/store/migrations/119.test.ts @@ -130,23 +130,24 @@ describe(`migration #${migrationVersion}`, () => { const migratedState = await migrate(oldState); + // Snap IDs are encoded: hyphens become %2D expect(FilesystemStorage.setItem).toHaveBeenNthCalledWith( 1, - `${STORAGE_KEY_PREFIX}SnapController:mock-snap-id`, + `${STORAGE_KEY_PREFIX}SnapController:mock%2Dsnap%2Did`, '{"sourceCode":"sourceCode"}', true, ); expect(FilesystemStorage.setItem).toHaveBeenNthCalledWith( 2, - `${STORAGE_KEY_PREFIX}SnapController:foo-snap-id`, + `${STORAGE_KEY_PREFIX}SnapController:foo%2Dsnap%2Did`, '{"sourceCode":"sourceCode2"}', true, ); expect(FilesystemStorage.setItem).toHaveBeenNthCalledWith( 3, - `${STORAGE_KEY_PREFIX}SnapController:bar-snap-id`, + `${STORAGE_KEY_PREFIX}SnapController:bar%2Dsnap%2Did`, '{"sourceCode":"sourceCode3 "}', true, ); diff --git a/app/store/migrations/119.ts b/app/store/migrations/119.ts index 632189e9fc8..9831b886e83 100644 --- a/app/store/migrations/119.ts +++ b/app/store/migrations/119.ts @@ -5,6 +5,7 @@ import { getErrorMessage, hasProperty, isObject } from '@metamask/utils'; import FilesystemStorage from 'redux-persist-filesystem-storage'; import { STORAGE_KEY_PREFIX } from '@metamask/storage-service'; import Device from '../../util/device'; +import { encodeStorageKey } from '../../core/Engine/utils/storage-service-utils'; export const migrationVersion = 119; @@ -89,7 +90,9 @@ async function transformState(state: ValidState) { ).map(async (snap) => { const sourceCode = snap.sourceCode as string; - const fullKey = `${STORAGE_KEY_PREFIX}SnapController:${snap.id}`; + // Encode the snap ID to handle special characters (e.g., slashes and hyphens in npm:@metamask/bip32-keyring-snap) + const encodedSnapId = encodeStorageKey(snap.id as string); + const fullKey = `${STORAGE_KEY_PREFIX}SnapController:${encodedSnapId}`; await FilesystemStorage.setItem( fullKey, From a6b6df6f22e7cc0fc1362d61600cdb435aa77938 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 24 Feb 2026 19:06:53 +0000 Subject: [PATCH 020/131] [skip ci] Bump version number to 3787 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ad66552ab90..380ecf520ac 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.67.0" - versionCode 3785 + versionCode 3787 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 30e2814e2e1..23ddbee8ca3 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3785 + VERSION_NUMBER: 3787 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3785 + FLASK_VERSION_NUMBER: 3787 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 298d458956c..92cf8b078f0 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3785; + CURRENT_PROJECT_VERSION = 3787; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3785; + CURRENT_PROJECT_VERSION = 3787; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3785; + CURRENT_PROJECT_VERSION = 3787; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3785; + CURRENT_PROJECT_VERSION = 3787; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3785; + CURRENT_PROJECT_VERSION = 3787; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3785; + CURRENT_PROJECT_VERSION = 3787; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 860b51d7c905578b1eb3584ca863400d32d512ed Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 24 Feb 2026 20:16:41 +0000 Subject: [PATCH 021/131] bump semvar version to 7.66.1 && build version to 3788 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 985ec48ece6..35080903458 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -187,7 +187,7 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionName "7.66.0" + versionName "7.66.1" versionCode 3780 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bitrise.yml b/bitrise.yml index b6814b3881a..67e776c2a52 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3516,13 +3516,13 @@ app: PROJECT_LOCATION_IOS: ios - opts: is_expand: false - VERSION_NAME: 7.66.0 + VERSION_NAME: 7.66.1 - opts: is_expand: false VERSION_NUMBER: 3780 - opts: is_expand: false - FLASK_VERSION_NAME: 7.66.0 + FLASK_VERSION_NAME: 7.66.1 - opts: is_expand: false FLASK_VERSION_NUMBER: 3780 diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 51a0bb30c07..9219b3fdbb1 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1319,7 +1319,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.0; + MARKETING_VERSION = 7.66.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1385,7 +1385,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.0; + MARKETING_VERSION = 7.66.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1454,7 +1454,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.0; + MARKETING_VERSION = 7.66.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1518,7 +1518,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.0; + MARKETING_VERSION = 7.66.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1684,7 +1684,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.0; + MARKETING_VERSION = 7.66.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -1751,7 +1751,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.0; + MARKETING_VERSION = 7.66.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index 5e34182c11b..fb631930cc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "7.66.0", + "version": "7.66.1", "private": true, "scripts": { "install:foundryup": "yarn mm-foundryup", From d7d2c3d4928bc35fbcd9235bd7a8dc00d3090f1e Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 24 Feb 2026 20:16:52 +0000 Subject: [PATCH 022/131] update changelog for 7.66.1 (hotfix - no test plan) --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d5456db695..fd2cea1ccb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.66.1] + ## [7.65.0] ### Added @@ -10496,7 +10498,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.65.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.66.1...HEAD +[7.66.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.65.0...v7.66.1 [7.65.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.1...v7.65.0 [7.64.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.0...v7.64.1 [7.64.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.63.1...v7.64.0 From 2363997ec163958ced4cc78afeec96cc455544f8 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:21:11 +0000 Subject: [PATCH 023/131] chore(runway): cherry-pick 9b336fc (#26494) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - feat: Fix for Pull to Refresh IAB gesture bug (#26373) ## **Description** Pull-to-refresh gesture was intercepting taps on buttons near the top of the page (e.g., Polymarket "Sign Up" / "Log In" buttons). The gesture handler called `stateManager.activate()` immediately in `onTouchesDown` for the pull zone (`y < 50px`), stealing the touch from the WebView before knowing whether the user intended to tap or pull. ### Changes **Deferred activation for pull-to-refresh**: Instead of activating immediately, the gesture enters a `pending_refresh` state in `onTouchesDown`. New `onTouchesMove` and `onTouchesUp` handlers measure the movement delta: - Downward movement >10px → activate pull-to-refresh - Horizontal/upward movement >10px → fail and let WebView handle - Finger lifts with <10px movement (tap) → fail and let WebView handle **`Gesture.Simultaneous` instead of `Gesture.Race`**: Deferred activation requires that the Native gesture (WebView scroll) does not cancel our Pan while it is deciding. `Gesture.Simultaneous` allows both to run independently — taps pass through to the WebView while our Pan stays in BEGAN state deciding, and when Pan activates for a real pull at the top of the page, the WebView has nothing to scroll so both coexist without visual conflict. ### Why not other approaches - `Gesture.Race` + deferred activation: Native wins the race and cancels Pan before `onTouchesMove` fires - `Gesture.Race` + immediate activation + synthetic JS click replay: Pull-to-refresh works but synthetic events have `isTrusted = false` — React ignores them - `Gesture.Exclusive` + deferred activation: Blocks Native while Pan is in BEGAN — nothing works ## **Changelog** CHANGELOG entry: Fixed pull-to-refresh gesture intercepting taps on buttons near the top of the page in the in-app browser ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MCWP-352 ## **Manual testing steps** ```gherkin Feature: Browser gesture tap passthrough Scenario: User taps a button near the top of a webpage Given the user is on polymarket.com in the in-app browser And the page is scrolled to the top (not scrolled at all) When user taps the "Log In" or "Sign Up" button Then the login/signup modal should appear Scenario: User pulls to refresh from the top of the page Given the user is on any webpage in the in-app browser And the page is scrolled to the top When user places finger near the top of the page and drags downward Then the refresh indicator should appear And the page should reload when pulled past the threshold Scenario: User swipes back from the left edge Given the user has navigated to at least one page in the in-app browser When user swipes from the left edge of the screen toward the right Then the browser should navigate back to the previous page Scenario: User swipes forward from the right edge Given the user has navigated back at least once in the in-app browser When user swipes from the right edge of the screen toward the left Then the browser should navigate forward Scenario: User scrolls normally in the center of the page Given the user is on any webpage in the in-app browser When user scrolls up or down in the center of the page Then the page should scroll normally without triggering any gestures ``` ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/75c70094-6f39-4086-aab4-368cc6b81177 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Changes low-level gesture coordination between the WebView native handler and a manual-activation Pan gesture, which could affect navigation/scroll/tap behavior across devices. Added tests reduce regression risk but gesture interactions are inherently sensitive to edge cases. > > **Overview** > Fixes an in-app browser bug where pull-to-refresh could steal taps near the top of the page by introducing **deferred pull-to-refresh activation**: touches in the pull zone now enter a `pending_refresh` state and only activate refresh after moving downward more than `PULL_MOVE_ACTIVATION` (10px), while horizontal/upward movement or lifting the finger fails the gesture and lets the WebView handle the tap. > > Switches WebView gesture composition from `Gesture.Race` to `Gesture.Simultaneous` so the WebView’s native gesture can continue receiving taps while the Pan gesture decides whether to activate, and extends unit tests/mocks to cover the new `onTouchesMove`/`onTouchesUp` paths and the new constant. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit af9d1b77f716a2200f2e0871b5a43b76308c0022. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [9b336fc](https://github.com/MetaMask/metamask-mobile/commit/9b336fcb0f3787dcd0a40dec0afe17222d012f61) Co-authored-by: Aslau Mario-Daniel Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> From 2bb99c8df7aaca23eea896383159d2a8c72953e9 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 24 Feb 2026 20:22:39 +0000 Subject: [PATCH 024/131] [skip ci] Bump version number to 3789 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 380ecf520ac..bdc9277da6b 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.67.0" - versionCode 3787 + versionCode 3789 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 23ddbee8ca3..e4ecf1015b9 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3787 + VERSION_NUMBER: 3789 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3787 + FLASK_VERSION_NUMBER: 3789 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 92cf8b078f0..18b3be4aa2c 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3787; + CURRENT_PROJECT_VERSION = 3789; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3787; + CURRENT_PROJECT_VERSION = 3789; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3787; + CURRENT_PROJECT_VERSION = 3789; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3787; + CURRENT_PROJECT_VERSION = 3789; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3787; + CURRENT_PROJECT_VERSION = 3789; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3787; + CURRENT_PROJECT_VERSION = 3789; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 045220345aab22054a9558d812be1fbf9ed193b7 Mon Sep 17 00:00:00 2001 From: Wei Sun Date: Tue, 24 Feb 2026 13:30:57 -0800 Subject: [PATCH 025/131] revert version back to 7.66.0 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 35080903458..985ec48ece6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -187,7 +187,7 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionName "7.66.1" + versionName "7.66.0" versionCode 3780 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bitrise.yml b/bitrise.yml index 67e776c2a52..b6814b3881a 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3516,13 +3516,13 @@ app: PROJECT_LOCATION_IOS: ios - opts: is_expand: false - VERSION_NAME: 7.66.1 + VERSION_NAME: 7.66.0 - opts: is_expand: false VERSION_NUMBER: 3780 - opts: is_expand: false - FLASK_VERSION_NAME: 7.66.1 + FLASK_VERSION_NAME: 7.66.0 - opts: is_expand: false FLASK_VERSION_NUMBER: 3780 diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 9219b3fdbb1..51a0bb30c07 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1319,7 +1319,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.1; + MARKETING_VERSION = 7.66.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1385,7 +1385,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.1; + MARKETING_VERSION = 7.66.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1454,7 +1454,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.1; + MARKETING_VERSION = 7.66.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1518,7 +1518,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.1; + MARKETING_VERSION = 7.66.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1684,7 +1684,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.1; + MARKETING_VERSION = 7.66.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -1751,7 +1751,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.66.1; + MARKETING_VERSION = 7.66.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index fb631930cc5..5e34182c11b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "7.66.1", + "version": "7.66.0", "private": true, "scripts": { "install:foundryup": "yarn mm-foundryup", From 56dff689dea86c0c0be60e967faaa30c006dd094 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 24 Feb 2026 21:32:41 +0000 Subject: [PATCH 026/131] [skip ci] Bump version number to 3790 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 985ec48ece6..d111d3cb819 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.66.0" - versionCode 3780 + versionCode 3790 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index b6814b3881a..17c6434b89e 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.66.0 - opts: is_expand: false - VERSION_NUMBER: 3780 + VERSION_NUMBER: 3790 - opts: is_expand: false FLASK_VERSION_NAME: 7.66.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3780 + FLASK_VERSION_NUMBER: 3790 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 51a0bb30c07..a524dbbac83 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3780; + CURRENT_PROJECT_VERSION = 3790; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3780; + CURRENT_PROJECT_VERSION = 3790; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3780; + CURRENT_PROJECT_VERSION = 3790; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3780; + CURRENT_PROJECT_VERSION = 3790; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3780; + CURRENT_PROJECT_VERSION = 3790; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3780; + CURRENT_PROJECT_VERSION = 3790; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 9037babc4cbd1196778154de45855092c4172376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:40:21 +0000 Subject: [PATCH 027/131] Revert "[skip ci] Bump version number to 3790" This reverts commit 56dff689dea86c0c0be60e967faaa30c006dd094. --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d111d3cb819..985ec48ece6 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.66.0" - versionCode 3790 + versionCode 3780 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 17c6434b89e..b6814b3881a 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.66.0 - opts: is_expand: false - VERSION_NUMBER: 3790 + VERSION_NUMBER: 3780 - opts: is_expand: false FLASK_VERSION_NAME: 7.66.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3790 + FLASK_VERSION_NUMBER: 3780 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index a524dbbac83..51a0bb30c07 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3790; + CURRENT_PROJECT_VERSION = 3780; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3790; + CURRENT_PROJECT_VERSION = 3780; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3790; + CURRENT_PROJECT_VERSION = 3780; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3790; + CURRENT_PROJECT_VERSION = 3780; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3790; + CURRENT_PROJECT_VERSION = 3780; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3790; + CURRENT_PROJECT_VERSION = 3780; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 5a0adada67d4594cb29b722b605eb3f159b2d775 Mon Sep 17 00:00:00 2001 From: Wei Sun Date: Tue, 24 Feb 2026 13:51:32 -0800 Subject: [PATCH 028/131] update OTA version --- app/constants/ota.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/constants/ota.ts b/app/constants/ota.ts index 70e0dd691f3..5c97755907e 100644 --- a/app/constants/ota.ts +++ b/app/constants/ota.ts @@ -6,7 +6,7 @@ import otaConfig from '../../ota.config.js'; * Reset to v0 when releasing a new native build * We keep this OTA_VERSION here to because changes in ota.config.js will affect the fingerprint and break the workflow in Github Actions */ -export const OTA_VERSION: string = 'v7.65.1'; +export const OTA_VERSION: string = 'v7.66.1'; export const RUNTIME_VERSION = otaConfig.RUNTIME_VERSION; export const PROJECT_ID = otaConfig.PROJECT_ID; export const UPDATE_URL = otaConfig.UPDATE_URL; From 3b31dc8f25a261b8a8c1e84ffc33589c448fcc4d Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:01:38 -0800 Subject: [PATCH 029/131] chore(runway): cherry-pick chore: remove process.env spread (#26528) - chore: remove process.env spread (#26368) ## **Description** We shouldn't be spreading then accessing process.env variables. It doesn't work correctly with the dev watcher Example: ``` console.log({ x: process.env.METAMASK_ENVIRONMENT, y: { ...process.env }?.METAMASK_ENVIRONMENT, z: { ...process.env }?.NODE_ENV, }); Returns { x: 'dev', y: undefined // IT FAILS z: 'development' } ``` ## **Changelog** CHANGELOG entry: chore: remove process.env spread ## **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. ## **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** > Small change to environment detection logic plus test Babel config exclusion; low risk but could affect behavior where `isProduction()` gates production-only features if env vars are mis-set. > > **Overview** > Updates `isProduction()` in `app/util/environment.ts` to directly read `process.env.METAMASK_ENVIRONMENT` (removing the `{ ...process.env }` spread workaround) to avoid incorrect/undefined values under the dev watcher. > > Adjusts `babel.config.tests.js` exclusions so `transform-inline-environment-variables` does not transform `app/util/environment.ts` (and its test), keeping runtime env var reads intact during tests. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b64f8fa45bef77b8e811f9221cfef08da28ba899. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [f07db0a](https://github.com/MetaMask/metamask-mobile/commit/f07db0a325ea9a47c9db4de23a86eba9ba59801d) --------- Co-authored-by: Prithpal Sooriya Co-authored-by: Wei Sun --- app/util/environment.ts | 4 +--- babel.config.tests.js | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/util/environment.ts b/app/util/environment.ts index 7cd325cbea3..01c267532bf 100644 --- a/app/util/environment.ts +++ b/app/util/environment.ts @@ -4,9 +4,7 @@ // This needs to be updated to check for the METAMASK_ENVIRONMENT environment variable instead of NODE_ENV // Once this is updated, verify that e2e smoke tests are working as expected export const isProduction = (): boolean => - // TODO: process.env.NODE_ENV === 'production' doesn't work with tests yet. Once we make it work, - // we can remove the following line and use the code above instead. - ({ ...process.env })?.METAMASK_ENVIRONMENT === 'production'; + process.env.METAMASK_ENVIRONMENT === 'production'; export const isGatorPermissionsFeatureEnabled = (): boolean => process.env.GATOR_PERMISSIONS_ENABLED?.toString() === 'true'; diff --git a/babel.config.tests.js b/babel.config.tests.js index 1a235070d44..91cd8b8b7cc 100644 --- a/babel.config.tests.js +++ b/babel.config.tests.js @@ -33,6 +33,8 @@ const newOverrides = [ 'app/components/UI/Ramp/hooks/useRampsSmartRouting.test.ts', 'app/components/UI/Ramp/hooks/useRampTokens.ts', 'app/components/UI/Ramp/hooks/useRampTokens.test.ts', + 'app/util/environment.ts', + 'app/util/environment.test.ts', 'app/store/migrations/**', 'app/util/networks/customNetworks.tsx', ], From a64cba268063c2ae510d7e40bd2a0b5eac4ab856 Mon Sep 17 00:00:00 2001 From: Bernardo Garces Chapero Date: Tue, 24 Feb 2026 16:59:11 +0000 Subject: [PATCH 030/131] fix: bnjs audit issue (#26481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI is broken due to a vulnerability with bn.js image CHANGELOG entry: null Fixes: ```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] ``` - [ ] 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. - [ ] 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** > Dependency-only upgrade via Yarn `resolutions`; low behavioral risk, with main concern being potential big-number edge-case differences in packages relying on `bn.js`. > > **Overview** > Updates `bn.js` to patched releases by pinning `bn.js@4.11.6`→`4.12.3` and `bn.js@5.2.1`→`5.2.3` via `package.json` `resolutions`, and bumps the app’s `bnjs4`/`bnjs5` aliases accordingly. > > Aligns TypeScript typings by moving `@types/bnjs5` to `@types/bn.js@^5.2.0`, with corresponding `yarn.lock` updates to remove the older `@types/bn.js`/`bn.js` versions. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8b51e8528519d127b244406e4d7bd9037064e511. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- package.json | 10 ++++++---- yarn.lock | 47 ++++++++++++----------------------------------- 2 files changed, 18 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index eafb3ce84b6..9bf0c5c8511 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,9 @@ "qs": "6.14.1", "@metamask/transaction-controller@npm:^61.0.0": "patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch", "@metamask/transaction-controller@npm:^62.9.2": "^62.14.0", - "viem": "2.31.3" + "viem": "2.31.3", + "bn.js@npm:4.11.6": "4.12.3", + "bn.js@npm:5.2.1": "5.2.3" }, "dependencies": { "@config-plugins/detox": "^9.0.0", @@ -343,8 +345,8 @@ "axios": "^1.13.5", "bignumber.js": "^9.0.1", "bitcoin-address-validation": "2.2.3", - "bnjs4": "npm:bn.js@^4.12.0", - "bnjs5": "npm:bn.js@^5.2.1", + "bnjs4": "npm:bn.js@^4.12.3", + "bnjs5": "npm:bn.js@^5.2.3", "buffer": "6.0.3", "cockatiel": "^3.1.2", "compare-versions": "^3.6.0", @@ -528,7 +530,7 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/react-native": "^13.2.0", "@types/bnjs4": "npm:@types/bn.js@^4.11.6", - "@types/bnjs5": "npm:@types/bn.js@^5.1.6", + "@types/bnjs5": "npm:@types/bn.js@^5.2.0", "@types/enzyme": "^3.10.12", "@types/eth-url-parser": "^1.0.0", "@types/i18n-js": "^3.8.4", diff --git a/yarn.lock b/yarn.lock index 223c1926e08..2632b17576a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18231,7 +18231,7 @@ __metadata: languageName: node linkType: hard -"@types/bn.js@npm:^5.1.0, @types/bn.js@npm:^5.1.5": +"@types/bn.js@npm:^5.1.0, @types/bn.js@npm:^5.1.5, @types/bnjs5@npm:@types/bn.js@^5.2.0": version: 5.2.0 resolution: "@types/bn.js@npm:5.2.0" dependencies: @@ -18240,15 +18240,6 @@ __metadata: languageName: node linkType: hard -"@types/bnjs5@npm:@types/bn.js@^5.1.6": - version: 5.1.6 - resolution: "@types/bn.js@npm:5.1.6" - dependencies: - "@types/node": "npm:*" - checksum: 10/db565b5a2af59b09459d74441153bf23a0e80f1fb2d070330786054e7ce1a7285dc40afcd8f289426c61a83166bdd70814f70e2d439744686aac5d3ea75daf13 - languageName: node - linkType: hard - "@types/body-parser@npm:*": version: 1.19.2 resolution: "@types/body-parser@npm:1.19.2" @@ -23177,31 +23168,17 @@ __metadata: languageName: node linkType: hard -"bn.js@npm:4.11.6": - version: 4.11.6 - resolution: "bn.js@npm:4.11.6" - checksum: 10/22741b015c9fff60fce32fc9988331b298eb9b6db5bfb801babb23b846eaaf894e440e0d067b2b3ae4e46aab754e90972f8f333b31bf94a686bbcb054bfa7b14 - languageName: node - linkType: hard - -"bn.js@npm:5.2.1, bnjs5@npm:bn.js@^5.2.1": - version: 5.2.1 - resolution: "bn.js@npm:5.2.1" - checksum: 10/7a7e8764d7a6e9708b8b9841b2b3d6019cc154d2fc23716d0efecfe1e16921b7533c6f7361fb05471eab47986c4aa310c270f88e3507172104632ac8df2cfd84 - languageName: node - linkType: hard - -"bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.0, bn.js@npm:^4.11.8, bn.js@npm:^4.11.9, bnjs4@npm:bn.js@^4.12.0": - version: 4.12.0 - resolution: "bn.js@npm:4.12.0" - checksum: 10/10f8db196d3da5adfc3207d35d0a42aa29033eb33685f20ba2c36cadfe2de63dad05df0a20ab5aae01b418d1c4b3d4d205273085262fa020d17e93ff32b67527 +"bn.js@npm:4.12.3, bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.0, bn.js@npm:^4.11.8, bn.js@npm:^4.11.9, bnjs4@npm:bn.js@^4.12.3": + version: 4.12.3 + resolution: "bn.js@npm:4.12.3" + checksum: 10/57ed5a055f946f3e009f1589c45a5242db07f3dddfc72e4506f0dd9d8b145f0dbee4edabc2499288f3fc338eb712fb96a1c623a2ed2bcd49781df1a64db64dd1 languageName: node linkType: hard -"bn.js@npm:^5.0.0, bn.js@npm:^5.1.2, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1, bn.js@npm:^5.2.2": - version: 5.2.2 - resolution: "bn.js@npm:5.2.2" - checksum: 10/51ebb2df83b33e5d8581165206e260d5e9c873752954616e5bf3758952b84d7399a9c6d00852815a0aeefb1150a7f34451b62d4287342d457fa432eee869e83e +"bn.js@npm:5.2.3, bn.js@npm:^5.0.0, bn.js@npm:^5.1.2, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1, bn.js@npm:^5.2.2, bnjs5@npm:bn.js@^5.2.3": + version: 5.2.3 + resolution: "bn.js@npm:5.2.3" + checksum: 10/dfb3927e0d531e6ec4f191597ce6f7f7665310c356fef5f968ada676b8058027f959af42eaa37b5f5c63617e819d3741813025ab15dd71a90f2e74698df0b58e languageName: node linkType: hard @@ -35580,7 +35557,7 @@ __metadata: "@tommasini/react-native-scrollable-tab-view": "npm:^1.1.1" "@tradle/react-native-http": "npm:2.0.1" "@types/bnjs4": "npm:@types/bn.js@^4.11.6" - "@types/bnjs5": "npm:@types/bn.js@^5.1.6" + "@types/bnjs5": "npm:@types/bn.js@^5.2.0" "@types/enzyme": "npm:^3.10.12" "@types/eth-url-parser": "npm:^1.0.0" "@types/he": "npm:^1.2.3" @@ -35636,8 +35613,8 @@ __metadata: base64-js: "npm:^1.5.1" bignumber.js: "npm:^9.0.1" bitcoin-address-validation: "npm:2.2.3" - bnjs4: "npm:bn.js@^4.12.0" - bnjs5: "npm:bn.js@^5.2.1" + bnjs4: "npm:bn.js@^4.12.3" + bnjs5: "npm:bn.js@^5.2.3" buffer: "npm:6.0.3" chromedriver: "npm:^123.0.1" cockatiel: "npm:^3.1.2" From e6447f93941dc07e4b9f7ca4cee1cc3201d366b5 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 25 Feb 2026 15:59:32 +0000 Subject: [PATCH 031/131] [skip ci] Bump version number to 3799 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index bdc9277da6b..4648ed6c47b 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.67.0" - versionCode 3789 + versionCode 3799 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index e4ecf1015b9..40651843e1e 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3789 + VERSION_NUMBER: 3799 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3789 + FLASK_VERSION_NUMBER: 3799 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 18b3be4aa2c..932958b4220 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3789; + CURRENT_PROJECT_VERSION = 3799; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3789; + CURRENT_PROJECT_VERSION = 3799; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3789; + CURRENT_PROJECT_VERSION = 3799; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3789; + CURRENT_PROJECT_VERSION = 3799; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3789; + CURRENT_PROJECT_VERSION = 3799; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3789; + CURRENT_PROJECT_VERSION = 3799; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 8360a748910e67a7b5b50eacd2e67735a3c4a626 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:05:57 +0000 Subject: [PATCH 032/131] chore(runway): cherry-pick fix: MUSD-368 fixed left-aligned header title in activity details cp-7.67.0 (#26530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: MUSD-368 fixed left-aligned header title in activity details cp-7.67.0 (#26495) ## **Description** Fixes UI bug where the mUSD conversion header was left-aligned on Android in the activity details view. ## **Changelog** CHANGELOG entry: fix UI bug where the mUSD conversion header was left-aligned on Android in the activity details view ## **Related issues** Fixes: [MUSD-368: Conversion header in activity list is left-aligned on Android](https://consensyssoftware.atlassian.net/browse/MUSD-368) ## **Manual testing steps** ```gherkin Feature: Centered navbar title in shared title options Scenario: user opens a screen that uses shared navigation title options on Android Given user is on an Android screen configured with getNavigationOptionsTitle When the screen header is rendered Then the header title is centered ``` ## **Screenshots/Recordings** ### **Before** image ### **After** image ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Single UI-only navigation option tweak (`headerTitleAlign: 'center'`) with minimal behavioral impact and no security/data implications. > > **Overview** > Fixes an Android header alignment issue by explicitly setting `headerTitleAlign: 'center'` in `getNavigationOptionsTitle` (Navbar) so screens using the shared title options render a centered header title instead of left-aligned. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 1606ec1d64e8684f23e9e0b565eda1d9bfe1aae4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [43ae73c](https://github.com/MetaMask/metamask-mobile/commit/43ae73c1af4b251c8c3a8246674b41b0a46da605) Co-authored-by: Matthew Grainger <46547583+Matt561@users.noreply.github.com> Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> --- app/components/UI/Navbar/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index f3655320422..f8f1a4063a5 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -245,6 +245,7 @@ export function getNavigationOptionsTitle( return { title, headerTitle: {title}, + headerTitleAlign: 'center', headerRight: () => isFullScreenModal ? ( Date: Wed, 25 Feb 2026 18:07:30 +0000 Subject: [PATCH 033/131] [skip ci] Bump version number to 3800 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 4648ed6c47b..84ff5ca5ec0 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.67.0" - versionCode 3799 + versionCode 3800 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 40651843e1e..0427d9c55bf 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3799 + VERSION_NUMBER: 3800 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3799 + FLASK_VERSION_NUMBER: 3800 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 932958b4220..db6f6fbe4d0 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3799; + CURRENT_PROJECT_VERSION = 3800; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3799; + CURRENT_PROJECT_VERSION = 3800; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3799; + CURRENT_PROJECT_VERSION = 3800; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3799; + CURRENT_PROJECT_VERSION = 3800; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3799; + CURRENT_PROJECT_VERSION = 3800; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3799; + CURRENT_PROJECT_VERSION = 3800; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From ad68f51a49a6397a7328008d6228242947e74fe2 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:28:30 +0000 Subject: [PATCH 034/131] chore(runway): cherry-pick 1b1705e (#26551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - feat: cp-7.67.0 metamask pay live token balances (#26134) ## **Description** Bumps `@metamask/transaction-pay-controller` to include live on-chain balance validation and refresh for MetaMask Pay transactions. ## **Changelog** CHANGELOG entry: null ## **Related issues** Relates to https://github.com/MetaMask/core/pull/7935 ## **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. ## **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** > Upgrades core transaction/pay controller dependencies and alters transaction handling (gas fee auto-updates and metrics) for additional transaction types, which could impact transaction submission behavior and test stability. > > **Overview** > MetaMask Pay dependencies are updated (notably `@metamask/transaction-pay-controller` `v16` and `@metamask/transaction-controller` `v62.18+`), pulling in live on-chain balance validation/refresh and associated controller stack bumps via `yarn.lock`. > > To support new relay-based deposit flows, the PR introduces `RELAY_DEPOSIT_TYPES` (including `perps`/`predict` variants), disables automatic gas fee updates for all relay deposits, and extends transaction metrics mapping so the new transaction types emit distinct analytics values. > > Smoke test fixtures for mUSD conversion are updated to work with the new Pay balance validation by seeding an ERC20 stub contract code on Anvil and making `createMusdFixture` async. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3a08273883616225453db9b5c4116b252499e1a4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [1b1705e](https://github.com/MetaMask/metamask-mobile/commit/1b1705eb065d8b7b01b889443bfd4a6012678c75) Co-authored-by: Matthew Walsh Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> --- .../confirmations/constants/confirmations.ts | 6 + .../metrics_properties/base.ts | 4 + .../transaction-controller-init.ts | 7 +- .../transaction-controller-messenger.ts | 2 + package.json | 5 +- tests/smoke/wallet/helpers/musd-fixture.ts | 29 +- yarn.lock | 257 ++++++++++++------ 7 files changed, 223 insertions(+), 87 deletions(-) diff --git a/app/components/Views/confirmations/constants/confirmations.ts b/app/components/Views/confirmations/constants/confirmations.ts index 9752c338186..a51239b3a60 100644 --- a/app/components/Views/confirmations/constants/confirmations.ts +++ b/app/components/Views/confirmations/constants/confirmations.ts @@ -86,3 +86,9 @@ export const POST_QUOTE_TRANSACTION_TYPES = [ * mUSD is a stablecoin pegged to USD, so we convert to user's local currency. */ export const USER_CURRENCY_TYPES = [TransactionType.musdClaim] as const; + +export const RELAY_DEPOSIT_TYPES = [ + TransactionType.relayDeposit, + TransactionType.perpsRelayDeposit, + TransactionType.predictRelayDeposit, +]; diff --git a/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts b/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts index 0295e1441b8..25e2cbbcda4 100644 --- a/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts +++ b/app/core/Engine/controllers/transaction-controller/metrics_properties/base.ts @@ -91,6 +91,10 @@ export function getTransactionTypeValue( return 'perps_deposit'; case TransactionType.perpsDepositAndOrder: return 'perps_deposit_and_order'; + case TransactionType.perpsRelayDeposit: + return 'perps_relay_deposit'; + case TransactionType.predictRelayDeposit: + return 'predict_relay_deposit'; case TransactionType.signTypedData: return 'eth_sign_typed_data'; case TransactionType.relayDeposit: diff --git a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts index 3427dad79e4..9255c847adf 100644 --- a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts +++ b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts @@ -16,7 +16,10 @@ import { SmartTransactionStatuses, } from '@metamask/smart-transactions-controller'; -import { REDESIGNED_TRANSACTION_TYPES } from '../../../../components/Views/confirmations/constants/confirmations'; +import { + REDESIGNED_TRANSACTION_TYPES, + RELAY_DEPOSIT_TYPES, +} from '../../../../components/Views/confirmations/constants/confirmations'; import { getSmartTransactionsFeatureFlagsForChain, selectShouldUseSmartTransaction, @@ -370,7 +373,7 @@ function beforeSign( } function isAutomaticGasFeeUpdateEnabled(transaction: TransactionMeta) { - if (hasTransactionType(transaction, [TransactionType.relayDeposit])) { + if (hasTransactionType(transaction, RELAY_DEPOSIT_TYPES)) { return false; } 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 671a9f76eb1..30be40b78a4 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 @@ -97,6 +97,7 @@ type InitMessengerActions = | CurrencyRateControllerActions | DelegationControllerSignDelegationAction | NetworkControllerFindNetworkClientIdByChainIdAction + | NetworkControllerGetNetworkClientByIdAction | KeyringControllerSignEip7702AuthorizationAction | KeyringControllerSignTypedMessageAction | NetworkControllerGetEIP1559CompatibilityAction @@ -154,6 +155,7 @@ export function getTransactionControllerInitMessenger( 'DelegationController:signDelegation', 'NetworkController:findNetworkClientIdByChainId', 'NetworkController:getEIP1559Compatibility', + 'NetworkController:getNetworkClientById', 'KeyringController:signEip7702Authorization', 'KeyringController:signTypedMessage', 'RemoteFeatureFlagController:getState', diff --git a/package.json b/package.json index 9bf0c5c8511..f23d19c037f 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,7 @@ "@metamask/transaction-controller@npm:^61.0.0": "patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch", "@metamask/transaction-controller@npm:^62.9.2": "^62.14.0", "viem": "2.31.3", + "@metamask/core-backend": "^5.0.0", "bn.js@npm:4.11.6": "4.12.3", "bn.js@npm:5.2.1": "5.2.3" }, @@ -288,8 +289,8 @@ "@metamask/storage-service": "^1.0.0", "@metamask/swappable-obj-proxy": "^2.1.0", "@metamask/swaps-controller": "^15.0.0", - "@metamask/transaction-controller": "^62.16.0", - "@metamask/transaction-pay-controller": "^15.0.1", + "@metamask/transaction-controller": "^62.18.0", + "@metamask/transaction-pay-controller": "^16.0.0", "@metamask/tron-wallet-snap": "^1.21.1", "@metamask/utils": "^11.8.1", "@myx-trade/sdk": "^0.1.265", diff --git a/tests/smoke/wallet/helpers/musd-fixture.ts b/tests/smoke/wallet/helpers/musd-fixture.ts index 62b59762a24..d8cbb2b1aa9 100644 --- a/tests/smoke/wallet/helpers/musd-fixture.ts +++ b/tests/smoke/wallet/helpers/musd-fixture.ts @@ -11,17 +11,30 @@ const USDC_DECIMALS = 6; const MUSD_DECIMALS = 6; const ETH_NATIVE_ADDRESS = '0x0000000000000000000000000000000000000000'; +/** + * Minimal EVM bytecode that returns 10 000 USDC (10 000 000 000 raw) for any + * call, including `balanceOf(address)`. Deployed at the USDC contract address + * on Anvil to satisfy the TransactionPayController balance validation. + */ +const ERC20_STUB_BYTECODE = + '0x7f00000000000000000000000000000000000000000000000000000002540be40060005260206000f3'; + export type { MusdFixtureOptions }; /** * Builds a fixture for mUSD conversion E2E tests using FixtureBuilder: * Mainnet, ETH/USDC/mUSD tokens, rates, balances, and mUSD eligibility state. */ -export function createMusdFixture( +export async function createMusdFixture( node: AnvilManager, options: MusdFixtureOptions, -): ReturnType { +): Promise> { const rpcPort = node?.getPort?.() ?? AnvilPort(); + + if (node) { + await seedErc20Stub(node, USDC_MAINNET); + } + const baseTokens = [ { address: toChecksumHexAddress(ETH_NATIVE_ADDRESS), @@ -70,3 +83,15 @@ export function createMusdFixture( .withMusdConversion(options) .build(); } + +async function seedErc20Stub( + node: AnvilManager, + tokenAddress: string, +): Promise { + const { testClient } = node.getProvider(); + + await testClient.setCode({ + address: tokenAddress as `0x${string}`, + bytecode: ERC20_STUB_BYTECODE as `0x${string}`, + }); +} diff --git a/yarn.lock b/yarn.lock index 2632b17576a..24c98ca7f96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7656,9 +7656,9 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^36.0.0": - version: 36.0.0 - resolution: "@metamask/accounts-controller@npm:36.0.0" +"@metamask/accounts-controller@npm:^36.0.0, @metamask/accounts-controller@npm:^36.0.1": + version: 36.0.1 + resolution: "@metamask/accounts-controller@npm:36.0.1" dependencies: "@ethereumjs/util": "npm:^9.1.0" "@metamask/base-controller": "npm:^9.0.0" @@ -7668,7 +7668,7 @@ __metadata: "@metamask/keyring-internal-api": "npm:^10.0.0" "@metamask/keyring-utils": "npm:^3.1.0" "@metamask/messenger": "npm:^0.3.0" - "@metamask/network-controller": "npm:^29.0.0" + "@metamask/network-controller": "npm:^30.0.0" "@metamask/snaps-controllers": "npm:^17.2.0" "@metamask/snaps-sdk": "npm:^10.3.0" "@metamask/snaps-utils": "npm:^11.7.0" @@ -7682,7 +7682,7 @@ __metadata: peerDependencies: "@metamask/providers": ^22.0.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/fe8199bda61fda63ce9d787115a536645a485bb797f154566396144c4da00fe01b782c75aa063b7e5089a75cc3d0570c6e9fdea4212bc1604d9474c255604a46 + checksum: 10/b60d45d06d85d481c2f6b397a1a2845866f8cfa18dd6d02ca0ab81809a1e543667c5d2bba82abc06ef53600bcd4e6c233268a530a81dce2779c3721d81946465 languageName: node linkType: hard @@ -7783,7 +7783,63 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:^99.3.2, @metamask/assets-controllers@npm:^99.4.0": +"@metamask/assets-controllers@npm:^100.0.2": + version: 100.0.3 + resolution: "@metamask/assets-controllers@npm:100.0.3" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/abi-utils": "npm:^2.0.3" + "@metamask/account-tree-controller": "npm:^4.1.1" + "@metamask/accounts-controller": "npm:^36.0.1" + "@metamask/approval-controller": "npm:^8.0.0" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/contract-metadata": "npm:^2.4.0" + "@metamask/controller-utils": "npm:^11.19.0" + "@metamask/core-backend": "npm:^6.0.0" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/keyring-api": "npm:^21.5.0" + "@metamask/keyring-controller": "npm:^25.1.0" + "@metamask/messenger": "npm:^0.3.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-account-service": "npm:^7.0.0" + "@metamask/network-controller": "npm:^30.0.0" + "@metamask/network-enablement-controller": "npm:^4.1.2" + "@metamask/permission-controller": "npm:^12.2.0" + "@metamask/phishing-controller": "npm:^16.3.0" + "@metamask/polling-controller": "npm:^16.0.3" + "@metamask/preferences-controller": "npm:^22.1.0" + "@metamask/profile-sync-controller": "npm:^27.1.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-controllers": "npm:^17.2.0" + "@metamask/snaps-sdk": "npm:^10.3.0" + "@metamask/snaps-utils": "npm:^11.7.0" + "@metamask/storage-service": "npm:^1.0.0" + "@metamask/transaction-controller": "npm:^62.18.0" + "@metamask/utils": "npm:^11.9.0" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + async-mutex: "npm:^0.5.0" + bitcoin-address-validation: "npm:^2.2.3" + bn.js: "npm:^5.2.1" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + multiformats: "npm:^9.9.0" + reselect: "npm:^5.1.1" + single-call-balance-checker-abi: "npm:^1.0.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/fd5ef80f4b3d55bcc5bb98dcf6a1fcbfa4b7401ca2591658b2474f0cd69d69c4b4ba6b5e2e4c9b13d1e3ba55f38e501494601eb8c16e702b715bd32283271a17 + languageName: node + linkType: hard + +"@metamask/assets-controllers@npm:^99.4.0": version: 99.4.0 resolution: "@metamask/assets-controllers@npm:99.4.0" dependencies: @@ -7908,7 +7964,7 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-controller@npm:^66.1.1, @metamask/bridge-controller@npm:^66.2.0": +"@metamask/bridge-controller@npm:^66.2.0": version: 66.2.0 resolution: "@metamask/bridge-controller@npm:66.2.0" dependencies: @@ -7939,7 +7995,39 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-status-controller@npm:^66.0.2, @metamask/bridge-status-controller@npm:^66.1.0": +"@metamask/bridge-controller@npm:^67.1.1": + version: 67.2.0 + resolution: "@metamask/bridge-controller@npm:67.2.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:^36.0.1" + "@metamask/assets-controllers": "npm:^100.0.2" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/controller-utils": "npm:^11.19.0" + "@metamask/gas-fee-controller": "npm:^26.0.3" + "@metamask/keyring-api": "npm:^21.5.0" + "@metamask/messenger": "npm:^0.3.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-network-controller": "npm:^3.0.4" + "@metamask/network-controller": "npm:^30.0.0" + "@metamask/polling-controller": "npm:^16.0.3" + "@metamask/profile-sync-controller": "npm:^27.1.0" + "@metamask/remote-feature-flag-controller": "npm:^4.0.0" + "@metamask/snaps-controllers": "npm:^17.2.0" + "@metamask/transaction-controller": "npm:^62.18.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/524ed3663b3e1f87a34171dc0b43b7b5751288d410d90b5d266923e71c974b47f3ec4e87d95baf6aa83ff41120b7625488f563e1c63048657241b24711089a95 + languageName: node + linkType: hard + +"@metamask/bridge-status-controller@npm:^66.1.0": version: 66.1.0 resolution: "@metamask/bridge-status-controller@npm:66.1.0" dependencies: @@ -7960,6 +8048,28 @@ __metadata: languageName: node linkType: hard +"@metamask/bridge-status-controller@npm:^67.0.1": + version: 67.0.1 + resolution: "@metamask/bridge-status-controller@npm:67.0.1" + dependencies: + "@metamask/accounts-controller": "npm:^36.0.1" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/bridge-controller": "npm:^67.1.1" + "@metamask/controller-utils": "npm:^11.19.0" + "@metamask/gas-fee-controller": "npm:^26.0.3" + "@metamask/network-controller": "npm:^30.0.0" + "@metamask/polling-controller": "npm:^16.0.3" + "@metamask/profile-sync-controller": "npm:^27.1.0" + "@metamask/snaps-controllers": "npm:^17.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/transaction-controller": "npm:^62.17.1" + "@metamask/utils": "npm:^11.9.0" + bignumber.js: "npm:^9.1.2" + uuid: "npm:^8.3.2" + checksum: 10/b75c377dcb35ccfb5e17f221f0c6f03cf2224b1fa34ba57f9fb0192d40097d699cc1fd9944d526820e4af4c1905a3c2b5bef17c2705822a87b4b135d3823ca91 + languageName: node + linkType: hard + "@metamask/browser-passworder@npm:^5.0.0": version: 5.0.0 resolution: "@metamask/browser-passworder@npm:5.0.0" @@ -8048,23 +8158,7 @@ __metadata: languageName: node linkType: hard -"@metamask/core-backend@npm:5.0.0": - version: 5.0.0 - resolution: "@metamask/core-backend@npm:5.0.0" - dependencies: - "@metamask/controller-utils": "npm:^11.16.0" - "@metamask/messenger": "npm:^0.3.0" - "@metamask/profile-sync-controller": "npm:^27.0.0" - "@metamask/utils": "npm:^11.8.1" - uuid: "npm:^8.3.2" - peerDependencies: - "@metamask/accounts-controller": ^35.0.0 - "@metamask/keyring-controller": ^25.0.0 - checksum: 10/c3c8d527ccbc9d56f6ddb5579cc8c58af971e9b81ece48ea7107c48e496ec2574283119cd4b258cc6c733f15d1432632a4e975d7616809147e2d4510dba59219 - languageName: node - linkType: hard - -"@metamask/core-backend@npm:^5.0.0, @metamask/core-backend@npm:^5.1.1": +"@metamask/core-backend@npm:^5.0.0": version: 5.1.1 resolution: "@metamask/core-backend@npm:5.1.1" dependencies: @@ -8627,16 +8721,16 @@ __metadata: languageName: node linkType: hard -"@metamask/gas-fee-controller@npm:^26.0.2": - version: 26.0.2 - resolution: "@metamask/gas-fee-controller@npm:26.0.2" +"@metamask/gas-fee-controller@npm:^26.0.2, @metamask/gas-fee-controller@npm:^26.0.3": + version: 26.0.3 + resolution: "@metamask/gas-fee-controller@npm:26.0.3" dependencies: "@metamask/base-controller": "npm:^9.0.0" - "@metamask/controller-utils": "npm:^11.18.0" + "@metamask/controller-utils": "npm:^11.19.0" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-unit": "npm:^0.3.0" - "@metamask/network-controller": "npm:^29.0.0" - "@metamask/polling-controller": "npm:^16.0.2" + "@metamask/network-controller": "npm:^30.0.0" + "@metamask/polling-controller": "npm:^16.0.3" "@metamask/utils": "npm:^11.9.0" "@types/bn.js": "npm:^5.1.5" "@types/uuid": "npm:^8.3.0" @@ -8644,7 +8738,7 @@ __metadata: uuid: "npm:^8.3.2" peerDependencies: "@babel/runtime": ^7.0.0 - checksum: 10/055f93e947039edd640971ae6f9b126cc1a10d0a55a4f92f67fa1c33e7b7375c50b3db9f323d3d0b8b8bbc5d5ed5aa9dd015c7aef42e165d657760dd15b617a5 + checksum: 10/c554094e845f93d19a81b5d0273648f0a066039e3296549eb57cd53e2a48ccb503bc261feb55d8a932d2eef7b68913d25319e91bebd53fb2ba7cbfca6ddfc8cb languageName: node linkType: hard @@ -9052,22 +9146,22 @@ __metadata: languageName: node linkType: hard -"@metamask/multichain-network-controller@npm:^3.0.2, @metamask/multichain-network-controller@npm:^3.0.3": - version: 3.0.3 - resolution: "@metamask/multichain-network-controller@npm:3.0.3" +"@metamask/multichain-network-controller@npm:^3.0.3, @metamask/multichain-network-controller@npm:^3.0.4": + version: 3.0.4 + resolution: "@metamask/multichain-network-controller@npm:3.0.4" dependencies: - "@metamask/accounts-controller": "npm:^36.0.0" + "@metamask/accounts-controller": "npm:^36.0.1" "@metamask/base-controller": "npm:^9.0.0" - "@metamask/controller-utils": "npm:^11.18.0" + "@metamask/controller-utils": "npm:^11.19.0" "@metamask/keyring-api": "npm:^21.5.0" "@metamask/keyring-internal-api": "npm:^10.0.0" "@metamask/messenger": "npm:^0.3.0" - "@metamask/network-controller": "npm:^29.0.0" + "@metamask/network-controller": "npm:^30.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/ea4b79ad5c35c660617a146822ad708a2ea978d1a8f907432fac6f6083dc303fa4faf1609cbf8ff5b068f11857e10893320b733e0aed2b44f4dfb5586fcaa383 + checksum: 10/bbdda3f49160b9a23bab2f9695b1eba7e2ccd29b4ba72347cd9efdd22f898c1b910965a220d969ded28f2624d0d8783f885f5f727e2e8f2f027481c21d5c8fb5 languageName: node linkType: hard @@ -9188,21 +9282,21 @@ __metadata: languageName: node linkType: hard -"@metamask/network-enablement-controller@npm:^4.1.0": - version: 4.1.0 - resolution: "@metamask/network-enablement-controller@npm:4.1.0" +"@metamask/network-enablement-controller@npm:^4.1.0, @metamask/network-enablement-controller@npm:^4.1.2": + version: 4.1.2 + resolution: "@metamask/network-enablement-controller@npm:4.1.2" dependencies: "@metamask/base-controller": "npm:^9.0.0" - "@metamask/controller-utils": "npm:^11.18.0" - "@metamask/keyring-api": "npm:^21.0.0" + "@metamask/controller-utils": "npm:^11.19.0" + "@metamask/keyring-api": "npm:^21.5.0" "@metamask/messenger": "npm:^0.3.0" - "@metamask/multichain-network-controller": "npm:^3.0.2" - "@metamask/network-controller": "npm:^29.0.0" + "@metamask/multichain-network-controller": "npm:^3.0.4" + "@metamask/network-controller": "npm:^30.0.0" "@metamask/slip44": "npm:^4.3.0" - "@metamask/transaction-controller": "npm:^62.9.2" + "@metamask/transaction-controller": "npm:^62.17.1" "@metamask/utils": "npm:^11.9.0" reselect: "npm:^5.1.1" - checksum: 10/3cc79865a49b95e7c7577eda6c1d00e1fdeb60c4357adde865fcacd4e334d421865f319d78c2659bd6745196beefed4651075bbb81e4b8caeeede20d5f8622ae + checksum: 10/2860220c63c941173a66be21011c58014421868d1e0e67d05d7ae30c156ac8352d752317e31ac773fa1420a5fdc7126b7c78e317d53d404cd76fd0781cf2c0cb languageName: node linkType: hard @@ -9288,20 +9382,20 @@ __metadata: languageName: node linkType: hard -"@metamask/phishing-controller@npm:^16.1.0, @metamask/phishing-controller@npm:^16.2.0": - version: 16.2.0 - resolution: "@metamask/phishing-controller@npm:16.2.0" +"@metamask/phishing-controller@npm:^16.1.0, @metamask/phishing-controller@npm:^16.2.0, @metamask/phishing-controller@npm:^16.3.0": + version: 16.3.0 + resolution: "@metamask/phishing-controller@npm:16.3.0" dependencies: "@metamask/base-controller": "npm:^9.0.0" "@metamask/controller-utils": "npm:^11.18.0" "@metamask/messenger": "npm:^0.3.0" - "@metamask/transaction-controller": "npm:^62.16.0" + "@metamask/transaction-controller": "npm:^62.17.0" "@noble/hashes": "npm:^1.8.0" "@types/punycode": "npm:^2.1.0" ethereum-cryptography: "npm:^2.1.2" fastest-levenshtein: "npm:^1.0.16" punycode: "npm:^2.1.1" - checksum: 10/964a980d5f4a741c224e785e625a1a135a57cc8f90a1de0f48f4b9a40a87c05ebd20aee9ab84869472804bae90ab8dba3f3455387d8f14e32232a5e600f53e43 + checksum: 10/a2c38aa69c5158d4bec2b1e37af449e7f9f67f24d151dcf4b6cd7e5a65567236b360488d91f4efde4898703de39eed5cc79e0c36369bf96dbedfed05daf41ecc languageName: node linkType: hard @@ -9321,18 +9415,18 @@ __metadata: languageName: node linkType: hard -"@metamask/polling-controller@npm:^16.0.0, @metamask/polling-controller@npm:^16.0.2": - version: 16.0.2 - resolution: "@metamask/polling-controller@npm:16.0.2" +"@metamask/polling-controller@npm:^16.0.0, @metamask/polling-controller@npm:^16.0.2, @metamask/polling-controller@npm:^16.0.3": + version: 16.0.3 + resolution: "@metamask/polling-controller@npm:16.0.3" dependencies: "@metamask/base-controller": "npm:^9.0.0" - "@metamask/controller-utils": "npm:^11.18.0" - "@metamask/network-controller": "npm:^29.0.0" + "@metamask/controller-utils": "npm:^11.19.0" + "@metamask/network-controller": "npm:^30.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/a956c11f7b29321267ae33a4f29a0663637a83fbf1a21602bfcbb3a2d1fc5a28d1fc7cff32be09bf460ebda000bd339a65e3391dfe0fa19d67a6ca591af386b6 + checksum: 10/31182b6d62fa949bf8bee834a65aba819e52ce77c208faebb33f6e3982834e87877e29d2d93952264988ea309d110b003adf69f358dc5820eeab7ab3c09f924f languageName: node linkType: hard @@ -10102,9 +10196,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^62.14.0, @metamask/transaction-controller@npm:^62.16.0, @metamask/transaction-controller@npm:^62.17.0": - version: 62.17.0 - resolution: "@metamask/transaction-controller@npm:62.17.0" +"@metamask/transaction-controller@npm:^62.17.0, @metamask/transaction-controller@npm:^62.17.1, @metamask/transaction-controller@npm:^62.18.0": + version: 62.19.0 + resolution: "@metamask/transaction-controller@npm:62.19.0" dependencies: "@ethereumjs/common": "npm:^4.4.0" "@ethereumjs/tx": "npm:^5.4.0" @@ -10113,16 +10207,16 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@ethersproject/wallet": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^36.0.0" + "@metamask/accounts-controller": "npm:^36.0.1" "@metamask/approval-controller": "npm:^8.0.0" "@metamask/base-controller": "npm:^9.0.0" - "@metamask/controller-utils": "npm:^11.18.0" - "@metamask/core-backend": "npm:5.0.0" + "@metamask/controller-utils": "npm:^11.19.0" + "@metamask/core-backend": "npm:^6.0.0" "@metamask/eth-query": "npm:^4.0.0" - "@metamask/gas-fee-controller": "npm:^26.0.2" + "@metamask/gas-fee-controller": "npm:^26.0.3" "@metamask/messenger": "npm:^0.3.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^29.0.0" + "@metamask/network-controller": "npm:^30.0.0" "@metamask/nonce-tracker": "npm:^6.0.0" "@metamask/remote-feature-flag-controller": "npm:^4.0.0" "@metamask/rpc-errors": "npm:^7.0.2" @@ -10137,7 +10231,7 @@ __metadata: peerDependencies: "@babel/runtime": ^7.0.0 "@metamask/eth-block-tracker": ">=9" - checksum: 10/5d97bd9711dda578af1d563da9e24db78087ad46636cd924b8d53d68d30ec00fb23aee3cf5ebf95d5cba95eddf794c3d7dd8dcec78c5c6e9a36dd70be6deeac0 + checksum: 10/d98cff056d00830a962dfd41f26211334fe52cf7a3f2833ad3e542303d7c43011918760f37d5a9dd145088b9c45376914de6a523716686897e899842a5d19038 languageName: node linkType: hard @@ -10180,29 +10274,30 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-pay-controller@npm:^15.0.1": - version: 15.0.1 - resolution: "@metamask/transaction-pay-controller@npm:15.0.1" +"@metamask/transaction-pay-controller@npm:^16.0.0": + version: 16.0.0 + resolution: "@metamask/transaction-pay-controller@npm:16.0.0" dependencies: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" - "@metamask/assets-controllers": "npm:^99.3.2" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/assets-controllers": "npm:^100.0.2" "@metamask/base-controller": "npm:^9.0.0" - "@metamask/bridge-controller": "npm:^66.1.1" - "@metamask/bridge-status-controller": "npm:^66.0.2" - "@metamask/controller-utils": "npm:^11.18.0" - "@metamask/gas-fee-controller": "npm:^26.0.2" + "@metamask/bridge-controller": "npm:^67.1.1" + "@metamask/bridge-status-controller": "npm:^67.0.1" + "@metamask/controller-utils": "npm:^11.19.0" + "@metamask/gas-fee-controller": "npm:^26.0.3" "@metamask/messenger": "npm:^0.3.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^29.0.0" + "@metamask/network-controller": "npm:^30.0.0" "@metamask/remote-feature-flag-controller": "npm:^4.0.0" - "@metamask/transaction-controller": "npm:^62.17.0" + "@metamask/transaction-controller": "npm:^62.18.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/088e3e86cd9af66618fec66b3aaf7f4f34bafb36118b0a08f1a200bbe7f457f8335cae07f409a481b4a415dd732a6baa3bd91812aba8efa35b2370c1db7a325f + checksum: 10/c23b5ffd00a6cb60ed306ad6f0d24a1b6744f8b00db6475a5c46c22860ed68653fa1f1c1e633de7b38b161a07a441c674761c30a1f7ff37e8e01f218703f2344 languageName: node linkType: hard @@ -35499,8 +35594,8 @@ __metadata: "@metamask/test-dapp": "npm:9.5.0" "@metamask/test-dapp-multichain": "npm:^0.17.1" "@metamask/test-dapp-solana": "npm:^0.3.0" - "@metamask/transaction-controller": "npm:^62.16.0" - "@metamask/transaction-pay-controller": "npm:^15.0.1" + "@metamask/transaction-controller": "npm:^62.18.0" + "@metamask/transaction-pay-controller": "npm:^16.0.0" "@metamask/tron-wallet-snap": "npm:^1.21.1" "@metamask/utils": "npm:^11.8.1" "@myx-trade/sdk": "npm:^0.1.265" From 7a7eb4aec6fe5ef6b70c105a797db30f3e37fa8f Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:28:32 +0000 Subject: [PATCH 035/131] chore(runway): cherry-pick fix(ramps): gate RampsController.init() behind rampsUnifiedBuyV2 feature flag cp-7.67.0 (#26538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(ramps): gate RampsController.init() behind rampsUnifiedBuyV2 feature flag cp-7.67.0 (#26458) ## **Description** `RampsController.init()` was firing on every app startup regardless of the `rampsUnifiedBuyV2` feature flag, making unnecessary v2 API calls when V2 is disabled: - `GET v2/regions/countries` - `GET v2/regions/{region}/tokens` - `GET v2/regions/{region}/providers` **Note:** The `GET /geolocation` call is not gated by this change — it is also called independently by the existing `useDetectGeolocation` hook outside of `RampsController`, so it continues to fire regardless of the V2 flag. This PR gates the `init()` call behind the `rampsUnifiedBuyV2` remote feature flag using the established `initMessenger` pattern (same approach as `AssetsController`, `NetworkController`, `DeFiPositionsController`, etc.). The `RampsController` is always instantiated (preserving persisted state and messenger registration), but the eager network calls are skipped when V2 is disabled. When V2 is eventually enabled, `init()` fires as before, and `useHydrateRampsController` continues to handle the hydration path independently. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [TRAM-3278](https://consensyssoftware.atlassian.net/browse/TRAM-3278) ## **References** - Incident: [incident-metamask-1469](https://app.firehydrant.io/org/consensys/incidents/9926d359-d5d1-468b-bea6-95cdbf09b890/incident/overview) (SEV1 — Buy/Sell quotes not loading, 2026-02-23) - GitHub Issue: https://github.com/MetaMask/metamask-mobile/issues/26534 ## **Manual testing steps** ```gherkin Feature: Gate RampsController.init() behind feature flag Scenario: user launches app with V2 feature flag disabled Given the rampsUnifiedBuyV2 remote feature flag is disabled When user launches the app Then RampsController.init() is not called And no v2 API calls are made (countries, tokens, providers) Scenario: user launches app with V2 feature flag enabled Given the rampsUnifiedBuyV2 remote feature flag is enabled And active is true and minimumVersion is set to a version <= current app version When user launches the app Then RampsController.init() is called as before And v2 API calls proceed normally ``` ## **Screenshots/Recordings** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. [TRAM-3278]: https://consensyssoftware.atlassian.net/browse/TRAM-3278?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- > [!NOTE] > **Medium Risk** > Changes startup initialization behavior for `RampsController` and adds a new init-messenger dependency on remote feature-flag state; risk is mainly around mis-gating causing missing initialization or unexpected API calls. > > **Overview** > `RampsController.init()` is now conditionally invoked at startup based on the remote `rampsUnifiedBuyV2` feature flag (including minimum app version checks), preventing eager V2 network calls when the flag is off. > > This introduces a dedicated `RampsControllerInitMessenger` that can read `RemoteFeatureFlagController:getState`, wires it into the controller init plumbing (`CONTROLLER_MESSENGERS`), and expands tests to cover enabled/disabled/invalid flag states and error handling while ensuring the controller is still always instantiated. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a5471c0a33963fec6b4357cc88d81ea7e92af9ac. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor [a68265a](https://github.com/MetaMask/metamask-mobile/commit/a68265a3da0dcdf699cb8fca7487e9e3ecbca4be) [TRAM-3278]: https://consensyssoftware.atlassian.net/browse/TRAM-3278?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ Co-authored-by: Amitabh Aggarwal Co-authored-by: Cursor Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> --- .../ramps-controller-init.test.ts | 122 ++++++++++++++++-- .../ramps-controller/ramps-controller-init.ts | 60 +++++++-- app/core/Engine/messengers/index.ts | 7 +- .../ramps-controller-messenger/index.ts | 6 +- .../ramps-controller-messenger.ts | 32 +++++ 5 files changed, 204 insertions(+), 23 deletions(-) diff --git a/app/core/Engine/controllers/ramps-controller/ramps-controller-init.test.ts b/app/core/Engine/controllers/ramps-controller/ramps-controller-init.test.ts index 9c7a02b3fd4..ae2e5d09ade 100644 --- a/app/core/Engine/controllers/ramps-controller/ramps-controller-init.test.ts +++ b/app/core/Engine/controllers/ramps-controller/ramps-controller-init.test.ts @@ -10,6 +10,7 @@ import { } from '@metamask/ramps-controller'; import { rampsControllerInit } from './ramps-controller-init'; import { MOCK_ANY_NAMESPACE, MockAnyNamespace } from '@metamask/messenger'; +import type { RampsControllerInitMessenger } from '../../messengers/ramps-controller-messenger'; const createMockUserRegion = (regionCode: string): UserRegion => { const parts = regionCode.toLowerCase().split('-'); @@ -60,19 +61,55 @@ jest.mock('@metamask/ramps-controller', () => { }; }); +jest.mock('react-native-device-info', () => ({ + getVersion: () => '99.0.0', +})); + +const createMockInitMessenger = ( + overrides: { + active?: boolean; + minimumVersion?: string | null; + } = {}, +): RampsControllerInitMessenger => { + const { active = false, minimumVersion = null } = overrides; + + return { + call: jest.fn().mockReturnValue({ + remoteFeatureFlags: { + rampsUnifiedBuyV2: { + active, + minimumVersion, + }, + }, + }), + } as unknown as RampsControllerInitMessenger; +}; + describe('ramps controller init', () => { const rampsControllerClassMock = jest.mocked(RampsController); let initRequestMock: jest.Mocked< - ControllerInitRequest + ControllerInitRequest< + RampsControllerMessenger, + RampsControllerInitMessenger + > >; beforeEach(() => { jest.clearAllMocks(); mockInit.mockResolvedValue(undefined); + const baseControllerMessenger = new ExtendedMessenger({ namespace: MOCK_ANY_NAMESPACE, }); - initRequestMock = buildControllerInitRequestMock(baseControllerMessenger); + initRequestMock = { + ...buildControllerInitRequestMock(baseControllerMessenger), + initMessenger: createMockInitMessenger(), + } as jest.Mocked< + ControllerInitRequest< + RampsControllerMessenger, + RampsControllerInitMessenger + > + >; }); it('uses default state when no initial state is passed in', () => { @@ -166,21 +203,84 @@ describe('ramps controller init', () => { expect(rampsControllerState).toStrictEqual(initialRampsControllerState); }); - it('calls init at startup', async () => { - rampsControllerInit(initRequestMock); + describe('when V2 feature flag is enabled', () => { + it('calls init at startup', async () => { + initRequestMock.initMessenger = createMockInitMessenger({ + active: true, + minimumVersion: '1.0.0', + }); + + rampsControllerInit(initRequestMock); + + await waitFor(() => { + expect(mockInit).toHaveBeenCalledTimes(1); + }); + }); + + it('handles init failure gracefully', async () => { + initRequestMock.initMessenger = createMockInitMessenger({ + active: true, + minimumVersion: '1.0.0', + }); + mockInit.mockRejectedValue(new Error('Network error')); + + expect(() => rampsControllerInit(initRequestMock)).not.toThrow(); - await waitFor(() => { - expect(mockInit).toHaveBeenCalledTimes(1); + await waitFor(() => { + expect(mockInit).toHaveBeenCalledTimes(1); + }); }); }); - it('handles init failure gracefully', async () => { - mockInit.mockRejectedValue(new Error('Network error')); + describe('when V2 feature flag is disabled', () => { + it('does not call init at startup', async () => { + initRequestMock.initMessenger = createMockInitMessenger({ + active: false, + }); - expect(() => rampsControllerInit(initRequestMock)).not.toThrow(); + rampsControllerInit(initRequestMock); - await waitFor(() => { - expect(mockInit).toHaveBeenCalledTimes(1); + await waitFor(() => { + expect(mockInit).not.toHaveBeenCalled(); + }); }); + + it('does not call init when active is true but minimumVersion is missing', async () => { + initRequestMock.initMessenger = createMockInitMessenger({ + active: true, + minimumVersion: null, + }); + + rampsControllerInit(initRequestMock); + + await waitFor(() => { + expect(mockInit).not.toHaveBeenCalled(); + }); + }); + + it('does not call init when RemoteFeatureFlagController throws', async () => { + initRequestMock.initMessenger = { + call: jest.fn().mockImplementation(() => { + throw new Error('Controller not ready'); + }), + } as unknown as RampsControllerInitMessenger; + + rampsControllerInit(initRequestMock); + + await waitFor(() => { + expect(mockInit).not.toHaveBeenCalled(); + }); + }); + }); + + it('always returns the controller instance regardless of flag state', () => { + initRequestMock.initMessenger = createMockInitMessenger({ + active: false, + }); + + const result = rampsControllerInit(initRequestMock); + + expect(result.controller).toBeDefined(); + expect(rampsControllerClassMock).toHaveBeenCalledTimes(1); }); }); diff --git a/app/core/Engine/controllers/ramps-controller/ramps-controller-init.ts b/app/core/Engine/controllers/ramps-controller/ramps-controller-init.ts index 0e7e3634718..2336e985e02 100644 --- a/app/core/Engine/controllers/ramps-controller/ramps-controller-init.ts +++ b/app/core/Engine/controllers/ramps-controller/ramps-controller-init.ts @@ -1,9 +1,45 @@ -import { ControllerInitFunction } from '../../types'; +import type { ControllerInitFunction } from '../../types'; import { RampsController, RampsControllerMessenger, getDefaultRampsControllerState, } from '@metamask/ramps-controller'; +import type { RampsControllerInitMessenger } from '../../messengers/ramps-controller-messenger'; +import { hasMinimumRequiredVersion } from '../../../../components/UI/Ramp/utils/hasMinimumRequiredVersion'; + +interface RampsUnifiedBuyV2Config { + active?: boolean; + minimumVersion?: string; +} + +const RAMPS_UNIFIED_BUY_V2_FLAG_KEY = 'rampsUnifiedBuyV2'; + +/** + * Determines whether the ramps unified buy V2 feature is enabled + * by reading the remote feature flag state. + * + * @param initMessenger - The init messenger to read RemoteFeatureFlagController state. + * @returns Whether V2 is enabled. + */ +function getIsRampsUnifiedBuyV2Enabled( + initMessenger: RampsControllerInitMessenger, +): boolean { + try { + const remoteState = initMessenger.call( + 'RemoteFeatureFlagController:getState', + ); + const config = (remoteState?.remoteFeatureFlags?.[ + RAMPS_UNIFIED_BUY_V2_FLAG_KEY + ] ?? {}) as RampsUnifiedBuyV2Config; + + return hasMinimumRequiredVersion( + config.minimumVersion, + config.active ?? false, + ); + } catch { + return false; + } +} /** * Initialize the ramps controller. @@ -11,12 +47,14 @@ import { * @param request - The request object. * @param request.controllerMessenger - The messenger to use for the controller. * @param request.persistedState - The persisted state. + * @param request.initMessenger - The init messenger for reading feature flags. * @returns The initialized controller. */ export const rampsControllerInit: ControllerInitFunction< RampsController, - RampsControllerMessenger -> = ({ controllerMessenger, persistedState }) => { + RampsControllerMessenger, + RampsControllerInitMessenger +> = ({ controllerMessenger, persistedState, initMessenger }) => { const rampsControllerState = persistedState.RampsController ?? getDefaultRampsControllerState(); @@ -25,13 +63,17 @@ export const rampsControllerInit: ControllerInitFunction< state: rampsControllerState, }); - // Initialize controller at app startup (non-blocking) - // Defer to next tick to avoid affecting initial state snapshot - Promise.resolve().then(() => { - controller.init().catch(() => { - // Initialization failed - error state will be available via selectors + const isV2Enabled = getIsRampsUnifiedBuyV2Enabled(initMessenger); + + if (isV2Enabled) { + // Initialize controller at app startup (non-blocking) + // Defer to next tick to avoid affecting initial state snapshot + Promise.resolve().then(() => { + controller.init().catch(() => { + // Initialization failed - error state will be available via selectors + }); }); - }); + } return { controller, diff --git a/app/core/Engine/messengers/index.ts b/app/core/Engine/messengers/index.ts index 0066a5e7d28..9b236597ceb 100644 --- a/app/core/Engine/messengers/index.ts +++ b/app/core/Engine/messengers/index.ts @@ -125,7 +125,10 @@ import { getRemoteFeatureFlagControllerMessenger } from './remote-feature-flag-c import { getErrorReportingServiceMessenger } from './error-reporting-service-messenger'; import { getStorageServiceMessenger } from './storage-service-messenger'; import { getLoggingControllerMessenger } from './logging-controller-messenger'; -import { getRampsControllerMessenger } from './ramps-controller-messenger'; +import { + getRampsControllerMessenger, + getRampsControllerInitMessenger, +} from './ramps-controller-messenger'; import { getRampsServiceMessenger } from './ramps-service-messenger'; import { getTransakServiceMessenger } from './transak-service-messenger/transak-service-messenger'; import { getPhishingControllerMessenger } from './phishing-controller-messenger'; @@ -399,7 +402,7 @@ export const CONTROLLER_MESSENGERS = { }, RampsController: { getMessenger: getRampsControllerMessenger, - getInitMessenger: noop, + getInitMessenger: getRampsControllerInitMessenger, }, RampsService: { getMessenger: getRampsServiceMessenger, diff --git a/app/core/Engine/messengers/ramps-controller-messenger/index.ts b/app/core/Engine/messengers/ramps-controller-messenger/index.ts index 021ed42b91e..e0851abfcb3 100644 --- a/app/core/Engine/messengers/ramps-controller-messenger/index.ts +++ b/app/core/Engine/messengers/ramps-controller-messenger/index.ts @@ -1 +1,5 @@ -export * from './ramps-controller-messenger'; +export { + getRampsControllerMessenger, + getRampsControllerInitMessenger, + type RampsControllerInitMessenger, +} from './ramps-controller-messenger'; diff --git a/app/core/Engine/messengers/ramps-controller-messenger/ramps-controller-messenger.ts b/app/core/Engine/messengers/ramps-controller-messenger/ramps-controller-messenger.ts index e5dfb890229..6d10ab982f1 100644 --- a/app/core/Engine/messengers/ramps-controller-messenger/ramps-controller-messenger.ts +++ b/app/core/Engine/messengers/ramps-controller-messenger/ramps-controller-messenger.ts @@ -4,6 +4,7 @@ import { MessengerActions, MessengerEvents, } from '@metamask/messenger'; +import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; import { RootMessenger } from '../../types'; type AllowedActions = MessengerActions; @@ -69,3 +70,34 @@ export function getRampsControllerMessenger( return messenger; } + +export type RampsControllerInitMessenger = ReturnType< + typeof getRampsControllerInitMessenger +>; + +/** + * Get the init messenger for the RampsController. Scoped to actions + * needed during initialization (reading feature flags). + * + * @param rootMessenger - The root messenger. + * @returns The RampsControllerInitMessenger. + */ +export function getRampsControllerInitMessenger(rootMessenger: RootMessenger) { + const messenger = new Messenger< + 'RampsControllerInit', + RemoteFeatureFlagControllerGetStateAction, + never, + RootMessenger + >({ + namespace: 'RampsControllerInit', + parent: rootMessenger, + }); + + rootMessenger.delegate({ + actions: ['RemoteFeatureFlagController:getState'], + events: [], + messenger, + }); + + return messenger; +} From 2f7112d8abd01fd858ba310ed8ed399f7b451d25 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 25 Feb 2026 18:30:02 +0000 Subject: [PATCH 036/131] [skip ci] Bump version number to 3801 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 84ff5ca5ec0..b9a81518283 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.67.0" - versionCode 3800 + versionCode 3801 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 0427d9c55bf..78d66bd2c8b 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3800 + VERSION_NUMBER: 3801 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3800 + FLASK_VERSION_NUMBER: 3801 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index db6f6fbe4d0..956a53eed4b 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3800; + CURRENT_PROJECT_VERSION = 3801; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3800; + CURRENT_PROJECT_VERSION = 3801; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3800; + CURRENT_PROJECT_VERSION = 3801; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3800; + CURRENT_PROJECT_VERSION = 3801; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3800; + CURRENT_PROJECT_VERSION = 3801; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3800; + CURRENT_PROJECT_VERSION = 3801; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 6384078955be1d5b99ca7c1f350a124497061c2c Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:34:57 +0000 Subject: [PATCH 037/131] chore(runway): cherry-pick fix: redirect to market details page after filling order cp-7.67.0 (#26580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: redirect to market details page after filling order cp-7.67.0 (#26533) ## **Description** When a user selects a custom token to pay with on the PerpsOrderView (coming from Token Details), after tapping Long: - User lands briefly on Token Details (wrong) - Then gets redirected to Market Details The expected behavior (which works when Perps balance is selected) is to go directly to Market Details. ## **Changelog** CHANGELOG entry: Redirects user to Market details page instead of token details page after submitting a perps position when coming from token details page ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/26482 ## **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/1de2360a-cdd1-4750-a8e4-72f1a6f11a01 ### **After** https://github.com/user-attachments/assets/d19cafab-3ecd-465b-8540-c298dc3a0929 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Changes navigation behavior in the perps order/deposit flow and adds a new route param that must be propagated correctly; mistakes could cause broken back stacks or incorrect redirects, but scope is limited to perps/token-details entry. > > **Overview** > Fixes the perps *Token Details → one-click trade* flow by threading a new `fromTokenDetails` flag through `usePerpsActions` → `PerpsOrderRedirect` → confirmations → `PerpsOrderView`. > > When a deposit/confirm step is required (e.g., paying with a custom token), `PerpsOrderView` now conditionally performs a `CommonActions.reset` to `Routes.PERPS.MARKET_DETAILS` instead of `navigation.goBack()`, preventing the brief/incorrect return to Token Details after submission. Navigation param types are updated accordingly. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0ffa501e3120f272959c8111df4eced044d29dfc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [da415f7](https://github.com/MetaMask/metamask-mobile/commit/da415f7e4da6eab081abcc2f11c913c50fade241) Co-authored-by: sahar-fehri Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> --- .../UI/Perps/Views/PerpsOrderRedirect.tsx | 10 +++++-- .../Views/PerpsOrderView/PerpsOrderView.tsx | 30 +++++++++++++++++-- app/components/UI/Perps/types/navigation.ts | 2 ++ .../components/AssetOverviewContent.tsx | 1 + .../UI/TokenDetails/hooks/usePerpsActions.ts | 6 +++- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsOrderRedirect.tsx b/app/components/UI/Perps/Views/PerpsOrderRedirect.tsx index 9ec0666a78d..a3df2a74e04 100644 --- a/app/components/UI/Perps/Views/PerpsOrderRedirect.tsx +++ b/app/components/UI/Perps/Views/PerpsOrderRedirect.tsx @@ -39,8 +39,12 @@ type RouteParams = RouteProp; const PerpsOrderRedirect: React.FC = () => { const navigation = useNavigation(); const route = useRoute(); - const { direction, asset, assetsASSETS2493AbtestTokenDetailsLayout } = - route.params; + const { + direction, + asset, + fromTokenDetails, + assetsASSETS2493AbtestTokenDetailsLayout, + } = route.params; const { isConnected, isInitialized } = usePerpsConnection(); const { depositWithOrder } = usePerpsTrading(); @@ -71,6 +75,7 @@ const PerpsOrderRedirect: React.FC = () => { { direction, asset, + fromTokenDetails, assetsASSETS2493AbtestTokenDetailsLayout, showPerpsHeader: CONFIRMATION_HEADER_CONFIG.ShowPerpsHeaderForDepositAndTrade, @@ -95,6 +100,7 @@ const PerpsOrderRedirect: React.FC = () => { isInitialized, direction, asset, + fromTokenDetails, assetsASSETS2493AbtestTokenDetailsLayout, depositWithOrder, navigation, diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx index e5f1e9482ff..13b8a179f48 100644 --- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx +++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx @@ -1,6 +1,7 @@ import { useNavigation, useRoute, + CommonActions, type NavigationProp, type RouteProp, } from '@react-navigation/native'; @@ -164,6 +165,8 @@ interface OrderRouteParams { limitPriceUpdate?: string; // Hide TP/SL when modifying existing position hideTPSL?: boolean; + /** When true, the order was initiated from the token details screen */ + fromTokenDetails?: boolean; /** A/B test variant for token details layout - e.g. 'control' or 'treatment' */ assetsASSETS2493AbtestTokenDetailsLayout?: string; } @@ -194,6 +197,8 @@ const PerpsOrderViewContentBase: React.FC = ({ ? 'trending' : undefined; const navigation = useNavigation>(); + const route = useRoute>(); + const fromTokenDetails = route.params?.fromTokenDetails ?? false; const { colors } = useTheme(); const insets = useSafeAreaInsets(); @@ -863,9 +868,29 @@ const PerpsOrderViewContentBase: React.FC = ({ handleDepositConfirm(activeTransactionMeta, () => { handlePlaceOrder(true); }); - await onDepositConfirm(); - navigation.goBack(); + if (fromTokenDetails) { + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [ + { + name: Routes.PERPS.MARKET_DETAILS, + params: { + market: navigationMarketData, + monitoringIntent: { + asset: orderForm.asset, + monitorOrders: true, + monitorPositions: true, + }, + }, + }, + ], + }), + ); + } else { + navigation.goBack(); + } return; } @@ -1093,6 +1118,7 @@ const PerpsOrderViewContentBase: React.FC = ({ payToken, onDepositConfirm, handleDepositConfirm, + fromTokenDetails, ], ); diff --git a/app/components/UI/Perps/types/navigation.ts b/app/components/UI/Perps/types/navigation.ts index 67709da93b5..1862788381d 100644 --- a/app/components/UI/Perps/types/navigation.ts +++ b/app/components/UI/Perps/types/navigation.ts @@ -220,6 +220,8 @@ export interface PerpsNavigationParamList extends ParamListBase { PerpsOrderRedirect: { direction: 'long' | 'short'; asset: string; + /** When true, the order was initiated from the token details screen */ + fromTokenDetails?: boolean; /** A/B test variant for token details layout - e.g. 'control' or 'treatment' */ assetsASSETS2493AbtestTokenDetailsLayout?: string; }; diff --git a/app/components/UI/TokenDetails/components/AssetOverviewContent.tsx b/app/components/UI/TokenDetails/components/AssetOverviewContent.tsx index e1d957c301e..f070655fcd1 100644 --- a/app/components/UI/TokenDetails/components/AssetOverviewContent.tsx +++ b/app/components/UI/TokenDetails/components/AssetOverviewContent.tsx @@ -200,6 +200,7 @@ const AssetOverviewContent: React.FC = ({ handlePerpsAction, } = usePerpsActions({ symbol: isPerpsEnabled ? token.symbol : null, + fromTokenDetails: true, abTestTokenDetailsLayout: isTestActive ? variantName : undefined, }); diff --git a/app/components/UI/TokenDetails/hooks/usePerpsActions.ts b/app/components/UI/TokenDetails/hooks/usePerpsActions.ts index 0466cdd3153..4b36e3767e7 100644 --- a/app/components/UI/TokenDetails/hooks/usePerpsActions.ts +++ b/app/components/UI/TokenDetails/hooks/usePerpsActions.ts @@ -10,6 +10,8 @@ import type { OrderDirection } from '@metamask/perps-controller'; export interface UsePerpsActionsParams { /** Token symbol, or null to skip the perps market check */ symbol: string | null; + /** When true, signals that navigation originated from the token details screen */ + fromTokenDetails?: boolean; /** A/B test variant for token details layout - e.g. 'control' or 'treatment' */ abTestTokenDetailsLayout?: string; } @@ -41,6 +43,7 @@ export interface UsePerpsActionsResult extends UsePerpsMarketForAssetResult { */ export const usePerpsActions = ({ symbol, + fromTokenDetails, abTestTokenDetailsLayout, }: UsePerpsActionsParams): UsePerpsActionsResult => { const navigation = useNavigation(); @@ -59,13 +62,14 @@ export const usePerpsActions = ({ params: { direction, asset: marketData.symbol, + fromTokenDetails, ...(abTestTokenDetailsLayout && { assetsASSETS2493AbtestTokenDetailsLayout: abTestTokenDetailsLayout, }), }, }); }, - [navigation, marketData, abTestTokenDetailsLayout], + [navigation, marketData, fromTokenDetails, abTestTokenDetailsLayout], ); return useMemo( From 7dbe51af114d91746652b11f937425760b1f26cb Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:36:03 +0000 Subject: [PATCH 038/131] chore(runway): cherry-pick fix: fix spl token redirection to block explorer cp-7.67.0 (#26552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: fix spl token redirection to block explorer cp-7.67.0 (#26517) ## **Description** Fix spl token blockexplorer redirection ## **Changelog** CHANGELOG entry: Fix redirection to blockexplorer on token details page ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/26114 ## **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** https://github.com/user-attachments/assets/3f1874b7-0b61-4855-81c2-54427db4bb63 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Changes link-generation logic used by token details and introduces new multichain explorer URL templates; low complexity but impacts user-facing navigation across multiple chains/environments. > > **Overview** > Fixes token-detail "View on block explorer" routing by introducing `getBlockExplorerTokenUrl` and using it from `MoreTokenActionsMenu`, ensuring non-native assets open a dedicated token page when supported (notably Solana/SPL), with CAIP-asset addresses parsed to extract the underlying token reference. > > Extends multichain block-explorer URL templates to include optional `token` paths (added for Solscan across mainnet/devnet/testnet) and adds formatting helpers/tests so non-EVM chains can resolve token URLs while EVM/unsupported chains fall back to the existing address URL behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 66255f3f1dcd22b694d6afe9b7907c6e9c73cc84. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [775b112](https://github.com/MetaMask/metamask-mobile/commit/775b1123c6ddea158d99afa0ef8365d07950050b) Co-authored-by: sahar-fehri Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> --- .../useMultichainBlockExplorerTxUrl.test.tsx | 1 + .../components/MoreTokenActionsMenu.test.tsx | 4 ++ .../components/MoreTokenActionsMenu.tsx | 14 ++++-- app/components/hooks/useBlockExplorer.test.ts | 48 +++++++++++++++++++ app/components/hooks/useBlockExplorer.ts | 30 +++++++++++- app/core/Multichain/constants.ts | 3 ++ app/core/Multichain/networks.ts | 22 +++++++++ 7 files changed, 117 insertions(+), 5 deletions(-) diff --git a/app/components/UI/Bridge/hooks/useMultichainBlockExplorerTxUrl/useMultichainBlockExplorerTxUrl.test.tsx b/app/components/UI/Bridge/hooks/useMultichainBlockExplorerTxUrl/useMultichainBlockExplorerTxUrl.test.tsx index df3aa50c864..eea322511ec 100644 --- a/app/components/UI/Bridge/hooks/useMultichainBlockExplorerTxUrl/useMultichainBlockExplorerTxUrl.test.tsx +++ b/app/components/UI/Bridge/hooks/useMultichainBlockExplorerTxUrl/useMultichainBlockExplorerTxUrl.test.tsx @@ -44,6 +44,7 @@ describe('useMultichainBlockExplorerTxUrl', () => { getEvmBlockExplorerUrl: jest.fn(), getBlockExplorerName: jest.fn(), getBlockExplorerUrl: jest.fn(), + getBlockExplorerTokenUrl: jest.fn(), getBlockExplorerBaseUrl: jest.fn(), toBlockExplorer: jest.fn(), }; diff --git a/app/components/UI/TokenDetails/components/MoreTokenActionsMenu.test.tsx b/app/components/UI/TokenDetails/components/MoreTokenActionsMenu.test.tsx index e7aae318eb5..1c1508b5f59 100644 --- a/app/components/UI/TokenDetails/components/MoreTokenActionsMenu.test.tsx +++ b/app/components/UI/TokenDetails/components/MoreTokenActionsMenu.test.tsx @@ -130,6 +130,9 @@ const mockGetBlockExplorerName = jest.fn(() => 'Etherscan'); const mockGetBlockExplorerUrl = jest.fn( () => 'https://etherscan.io/token/0x123', ); +const mockGetBlockExplorerTokenUrl = jest.fn( + () => 'https://etherscan.io/token/0x123', +); const mockGetBlockExplorerBaseUrl = jest.fn(() => 'https://etherscan.io'); jest.mock('../../../hooks/useBlockExplorer', () => ({ @@ -137,6 +140,7 @@ jest.mock('../../../hooks/useBlockExplorer', () => ({ default: () => ({ getBlockExplorerName: mockGetBlockExplorerName, getBlockExplorerUrl: mockGetBlockExplorerUrl, + getBlockExplorerTokenUrl: mockGetBlockExplorerTokenUrl, getBlockExplorerBaseUrl: mockGetBlockExplorerBaseUrl, }), })); diff --git a/app/components/UI/TokenDetails/components/MoreTokenActionsMenu.tsx b/app/components/UI/TokenDetails/components/MoreTokenActionsMenu.tsx index dc531775490..fa3d1a06f1c 100644 --- a/app/components/UI/TokenDetails/components/MoreTokenActionsMenu.tsx +++ b/app/components/UI/TokenDetails/components/MoreTokenActionsMenu.tsx @@ -17,7 +17,7 @@ import { getDecimalChainId } from '../../../../util/networks'; import { MetaMetricsEvents } from '../../../../core/Analytics'; import { WalletActionsBottomSheetSelectorsIDs } from '../../../Views/WalletActions/WalletActionsBottomSheet.testIds'; import Logger from '../../../../util/Logger'; -import { Hex } from '@metamask/utils'; +import { Hex, isCaipAssetType, parseCaipAssetType } from '@metamask/utils'; import InAppBrowser from 'react-native-inappbrowser-reborn'; import { TokenI } from '../../Tokens/types'; import { RootState } from '../../../../reducers'; @@ -103,9 +103,15 @@ const MoreTokenActionsMenu = () => { }, [closeBottomSheetAndNavigate, onReceive]); const handleViewOnBlockExplorer = useCallback(() => { - const url = isNativeCurrency - ? explorer.getBlockExplorerBaseUrl(asset.chainId) - : explorer.getBlockExplorerUrl(asset.address, asset.chainId); + let url: string | null; + if (isNativeCurrency) { + url = explorer.getBlockExplorerBaseUrl(asset.chainId); + } else { + const tokenAddress = isCaipAssetType(asset.address) + ? parseCaipAssetType(asset.address).assetReference + : asset.address; + url = explorer.getBlockExplorerTokenUrl(tokenAddress, asset.chainId); + } if (url) { goToBrowserUrl(url, explorer.getBlockExplorerName(asset.chainId)); diff --git a/app/components/hooks/useBlockExplorer.test.ts b/app/components/hooks/useBlockExplorer.test.ts index a5d4ea1be98..59d7b3c85b8 100644 --- a/app/components/hooks/useBlockExplorer.test.ts +++ b/app/components/hooks/useBlockExplorer.test.ts @@ -156,6 +156,54 @@ describe('useBlockExplorer', () => { }); }); + describe('getBlockExplorerTokenUrl', () => { + it('returns dedicated token page URL for Solana tokens', () => { + const { result } = renderHookWithProvider(() => useBlockExplorer()); + const { getBlockExplorerTokenUrl } = result.current; + const mintAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + const solanaChainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; + + const url = getBlockExplorerTokenUrl(mintAddress, solanaChainId); + expect(url).toBe( + 'https://solscan.io/token/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + ); + }); + + it('returns dedicated token page URL for Solana devnet tokens', () => { + const { result } = renderHookWithProvider(() => useBlockExplorer()); + const { getBlockExplorerTokenUrl } = result.current; + const mintAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + const solanaDevnetChainId = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1'; + + const url = getBlockExplorerTokenUrl(mintAddress, solanaDevnetChainId); + expect(url).toBe( + 'https://solscan.io/token/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v?cluster=devnet', + ); + }); + + it('falls back to address URL for Bitcoin (no dedicated token page)', () => { + const { result } = renderHookWithProvider(() => useBlockExplorer()); + const { getBlockExplorerTokenUrl } = result.current; + const address = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'; + const bitcoinChainId = 'bip122:000000000019d6689c085ae165831e93'; + + const url = getBlockExplorerTokenUrl(address, bitcoinChainId); + expect(url).toBe( + 'https://mempool.space/address/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', + ); + }); + + it('falls back to standard address URL for EVM chains', () => { + const { result } = renderHookWithProvider(() => useBlockExplorer()); + const { getBlockExplorerTokenUrl } = result.current; + const address = '0x1234567890abcdef'; + const polygonChainId = '0x89'; + + const url = getBlockExplorerTokenUrl(address, polygonChainId); + expect(url).toBe('https://polygonscan.com/address/0x1234567890abcdef'); + }); + }); + describe('RPC networks with custom block explorers', () => { it('uses custom RPC block explorer when available', () => { // This test verifies the RPC fallback path is covered diff --git a/app/components/hooks/useBlockExplorer.ts b/app/components/hooks/useBlockExplorer.ts index 075ec48663d..9579f12d3c8 100644 --- a/app/components/hooks/useBlockExplorer.ts +++ b/app/components/hooks/useBlockExplorer.ts @@ -24,7 +24,10 @@ import { BASE_MAINNET_BLOCK_EXPLORER, } from '../../constants/urls'; import { CHAIN_IDS } from '@metamask/transaction-controller'; -import { formatBlockExplorerAddressUrl } from '../../core/Multichain/networks'; +import { + formatBlockExplorerAddressUrl, + formatBlockExplorerTokenUrl, +} from '../../core/Multichain/networks'; import { MULTICHAIN_NETWORK_BLOCK_EXPLORER_FORMAT_URLS_MAP } from '../../core/Multichain/constants'; import { isNonEvmChainId } from '../../core/Multichain/utils'; import { parseCaipChainId, isCaipChainId } from '@metamask/utils'; @@ -126,6 +129,30 @@ const useBlockExplorer = (chainId?: string) => { ], ); + /** + * Returns the block explorer URL for a token's detail page. + * Uses a dedicated token page URL when the explorer supports it, + * otherwise falls back to the standard address URL. + */ + const getBlockExplorerTokenUrl = useCallback( + (address: string, targetChainId?: string) => { + const currentChainId = targetChainId || chainId; + + if (currentChainId && isNonEvmChainId(currentChainId)) { + const blockExplorerUrls = + MULTICHAIN_NETWORK_BLOCK_EXPLORER_FORMAT_URLS_MAP[ + currentChainId as keyof typeof MULTICHAIN_NETWORK_BLOCK_EXPLORER_FORMAT_URLS_MAP + ]; + return blockExplorerUrls + ? formatBlockExplorerTokenUrl(blockExplorerUrls, address) + : null; + } + + return getBlockExplorerUrl(address, targetChainId); + }, + [chainId, getBlockExplorerUrl], + ); + /** * Returns the base block explorer URL for a given chain (without any address path). * Useful for navigating to the block explorer homepage for native currencies. @@ -243,6 +270,7 @@ const useBlockExplorer = (chainId?: string) => { return { toBlockExplorer, getBlockExplorerUrl, + getBlockExplorerTokenUrl, getBlockExplorerName, getEvmBlockExplorerUrl, getBlockExplorerBaseUrl, diff --git a/app/core/Multichain/constants.ts b/app/core/Multichain/constants.ts index 1d94ee4c0b3..1612297e1be 100644 --- a/app/core/Multichain/constants.ts +++ b/app/core/Multichain/constants.ts @@ -53,16 +53,19 @@ export const MULTICHAIN_NETWORK_BLOCK_EXPLORER_FORMAT_URLS_MAP: Record< url: 'https://solscan.io/', address: 'https://solscan.io/account/{address}', transaction: 'https://solscan.io/tx/{txId}', + token: 'https://solscan.io/token/{address}', }, [SolScope.Devnet]: { url: 'https://solscan.io/', address: 'https://solscan.io/account/{address}?cluster=devnet', transaction: 'https://solscan.io/tx/{txId}?cluster=devnet', + token: 'https://solscan.io/token/{address}?cluster=devnet', }, [SolScope.Testnet]: { url: 'https://solscan.io/', address: 'https://solscan.io/account/{address}?cluster=testnet', transaction: 'https://solscan.io/tx/{txId}?cluster=testnet', + token: 'https://solscan.io/token/{address}?cluster=testnet', }, [TrxScope.Mainnet]: { url: 'https://tronscan.org/', diff --git a/app/core/Multichain/networks.ts b/app/core/Multichain/networks.ts index f1fa269f8dc..a9da50b09a1 100644 --- a/app/core/Multichain/networks.ts +++ b/app/core/Multichain/networks.ts @@ -37,6 +37,12 @@ export interface MultichainBlockExplorerFormatUrls { * Format URL of the block explorer for transactions. */ transaction: MultichainBlockExplorerFormatUrl<'txId'>; + + /** + * Format URL of the block explorer for token detail pages. + * Not all chains/explorers support a dedicated token page. + */ + token?: MultichainBlockExplorerFormatUrl<'address'>; } /** @@ -69,6 +75,22 @@ export function formatBlockExplorerAddressUrl( return formatBlockExplorerUrl(urls.address, 'address', address); } +/** + * Format a URL for token detail pages. + * Falls back to the address URL if no dedicated token URL is configured. + * + * @param urls - The group of format URLs for a given block explorer. + * @param address - The token address (e.g. mint address) to create the URL for. + * @returns The formatted URL for the given token. + */ +export function formatBlockExplorerTokenUrl( + urls: MultichainBlockExplorerFormatUrls, + address: string, +) { + const template = urls.token ?? urls.address; + return formatBlockExplorerUrl(template, 'address', address); +} + /** * Format a URL for transactions. * From 086c9a54e3039018027639629601ddb39b56bf18 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 25 Feb 2026 18:39:19 +0000 Subject: [PATCH 039/131] [skip ci] Bump version number to 3804 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index b9a81518283..82bd12cf0c9 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.67.0" - versionCode 3801 + versionCode 3804 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 78d66bd2c8b..665bade08e1 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3801 + VERSION_NUMBER: 3804 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3801 + FLASK_VERSION_NUMBER: 3804 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 956a53eed4b..565d3405fb4 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3801; + CURRENT_PROJECT_VERSION = 3804; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3801; + CURRENT_PROJECT_VERSION = 3804; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3801; + CURRENT_PROJECT_VERSION = 3804; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3801; + CURRENT_PROJECT_VERSION = 3804; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3801; + CURRENT_PROJECT_VERSION = 3804; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3801; + CURRENT_PROJECT_VERSION = 3804; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 35f8c01c143f57382e5c5a17119fca70be053695 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:32:53 +0000 Subject: [PATCH 040/131] chore(runway): cherry-pick fix: perps verbose when clearing subscriptions cp-7.67.0 (#26579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: perps verbose when clearing subscriptions cp-7.67.0 (#26556) ## **Description** During account switches, network changes, or app backgrounding, the Perps WebSocket connection is intentionally torn down via `clearAll()`. This causes pending subscription operations (subscribe/unsubscribe) to reject with `WebSocketRequestError` from the HyperLiquid SDK — which is expected behavior. However, 25 out of 30 error handlers in `HyperLiquidSubscriptionService` were unconditionally calling `Logger.error()`, sending these expected cleanup errors to Sentry. Only 5 handlers checked the `#isClearing` flag before logging. This PR: - Adds a `#logErrorUnlessClearing()` private helper that checks the `#isClearing` flag before forwarding to `Logger.error()` (Sentry) - Routes all 30 error handlers through this helper (previously only 5 were guarded) - Removes redundant inline `if (!this.#isClearing)` checks that are now handled by the helper - Imports `PerpsLogger` type for proper parameter typing **Net effect**: During intentional disconnects, `WebSocketRequestError` errors from pending subscription operations are suppressed from Sentry. Genuine connection failures (when `#isClearing` is false) are still reported. ## **Changelog** CHANGELOG entry: Fixed: Suppressed expected WebSocket cleanup errors from being reported to Sentry during intentional Perps disconnections ## **Related issues** Fixes: Excessive `WebSocketRequestError: Failed to establish WebSocket connection` errors on Sentry during account switches and reconnections ## **Manual testing steps** ```gherkin Feature: Perps WebSocket error suppression during cleanup Scenario: user switches accounts while on Perps tab Given the user is on the Perps tab with an active WebSocket connection When user switches to a different account Then the Perps connection reconnects successfully And no WebSocketRequestError errors are logged to Sentry during the transition Scenario: user navigates away from Perps and returns Given the user is on the Perps tab with an active WebSocket connection When user navigates to the Wallet tab and back to Perps Then the connection is re-established without Sentry error noise ``` ## **Screenshots/Recordings** N/A — No UI changes. This is an internal error logging improvement. ### **Before** Expected `WebSocketRequestError` errors during cleanup were sent to Sentry (25 out of 30 error handlers were unguarded). ### **After** All 30 error handlers now check `#isClearing` via the `#logErrorUnlessClearing()` helper — expected errors during intentional disconnect are suppressed. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Logging-only refactor that changes when errors are reported (suppresses during `clearAll()`), with minimal risk of impacting subscription behavior aside from potentially hiding unexpected errors if `#isClearing` is set incorrectly. > > **Overview** > Reduces Sentry noise during intentional Perps disconnects (e.g., `clearAll()` on account/network switches) by introducing `#logErrorUnlessClearing()` and using it everywhere subscription promises can reject during teardown. > > All previously unguarded `logger.error` calls in `HyperLiquidSubscriptionService` (subscribe/ensure/cleanup/unsubscribe paths) are funneled through this helper, and redundant inline `if (!#isClearing)` checks are removed; `PerpsLogger` is imported to type the helper’s context parameter. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5888c46c25d10d595ebc762c843fd760cfbbeae4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [8e687b1](https://github.com/MetaMask/metamask-mobile/commit/8e687b142d44c961276815e4382bd1c783250d87) Co-authored-by: Alejandro Garcia Anglada Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> --- .../HyperLiquidSubscriptionService.ts | 173 +++++++++--------- 1 file changed, 91 insertions(+), 82 deletions(-) diff --git a/app/controllers/perps/services/HyperLiquidSubscriptionService.ts b/app/controllers/perps/services/HyperLiquidSubscriptionService.ts index d7d5cfc46ff..2d5a1bf612b 100644 --- a/app/controllers/perps/services/HyperLiquidSubscriptionService.ts +++ b/app/controllers/perps/services/HyperLiquidSubscriptionService.ts @@ -32,6 +32,7 @@ import type { OrderBookData, OrderBookLevel, PerpsPlatformDependencies, + PerpsLogger, } from '../types'; import type { HyperLiquidClientService } from './HyperLiquidClientService'; import type { HyperLiquidWalletService } from './HyperLiquidWalletService'; @@ -255,6 +256,24 @@ export class HyperLiquidSubscriptionService { this.#blocklistMarkets = blocklistMarkets ?? []; } + /** + * Conditionally log an error to Sentry, suppressing during intentional disconnect. + * When `clearAll()` is called, pending subscription promises reject with + * `WebSocketRequestError` — these are expected and must not pollute Sentry. + * + * @param error - The error to log + * @param context - Sentry context from #getErrorContext() + */ + #logErrorUnlessClearing( + error: Error, + context: Parameters[1], + ): void { + if (this.#isClearing) { + return; + } + this.#deps.logger.error(error, context); + } + /** * Get error context for logging with searchable tags and context. * Enables Sentry dashboard filtering by feature, provider, and network. @@ -476,7 +495,7 @@ export class HyperLiquidSubscriptionService { try { await this.#ensureAssetCtxsSubscription(dex); } catch (error) { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.updateFeatureFlags', @@ -517,7 +536,7 @@ export class HyperLiquidSubscriptionService { `Established user data subscriptions for new DEX: ${dex}`, ); } catch (error) { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.updateFeatureFlags', @@ -531,7 +550,7 @@ export class HyperLiquidSubscriptionService { }), ); } catch (error) { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.updateFeatureFlags', @@ -954,7 +973,7 @@ export class HyperLiquidSubscriptionService { dexsNeeded.forEach((dex) => { const dexName = dex ?? ''; this.#ensureAssetCtxsSubscription(dexName).catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.subscribeToPrices', @@ -1186,7 +1205,7 @@ export class HyperLiquidSubscriptionService { ); } } catch (error) { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.createUserDataSubscription', @@ -1207,7 +1226,7 @@ export class HyperLiquidSubscriptionService { return undefined; }) .catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.createUserDataSubscription', @@ -1231,7 +1250,7 @@ export class HyperLiquidSubscriptionService { return undefined; }) .catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.createUserDataSubscription', @@ -1331,7 +1350,7 @@ export class HyperLiquidSubscriptionService { ); } } catch (error) { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.createUserDataSubscription', @@ -1356,7 +1375,7 @@ export class HyperLiquidSubscriptionService { return undefined; }) .catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.createUserDataSubscription', @@ -1504,7 +1523,7 @@ export class HyperLiquidSubscriptionService { // Remove this DEX from expected set so it doesn't block notifications for other DEXs this.#expectedDexs.delete(dexName); - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.createClearinghouseSubscription', @@ -1632,7 +1651,7 @@ export class HyperLiquidSubscriptionService { // Remove this DEX from expected set so it doesn't block notifications for other DEXs this.#expectedDexs.delete(dexName); - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.createOpenOrdersSubscription', @@ -1736,20 +1755,18 @@ export class HyperLiquidSubscriptionService { if (this.#webData3Subscriptions.size > 0) { this.#webData3Subscriptions.forEach((subscription, dexName) => { subscription.unsubscribe().catch((error: Error) => { - if (!this.#isClearing) { - this.#deps.logger.error( - ensureError( - error, - 'HyperLiquidSubscriptionService.cleanupSharedWebData3ISubscription', - ), - this.#getErrorContext( - 'cleanupSharedWebData3ISubscription.webData3', - { - dex: dexName, - }, - ), - ); - } + this.#logErrorUnlessClearing( + ensureError( + error, + 'HyperLiquidSubscriptionService.cleanupSharedWebData3ISubscription', + ), + this.#getErrorContext( + 'cleanupSharedWebData3ISubscription.webData3', + { + dex: dexName, + }, + ), + ); }); }); this.#webData3Subscriptions.clear(); @@ -1761,43 +1778,39 @@ export class HyperLiquidSubscriptionService { this.#clearinghouseStateSubscriptions.forEach( (subscription, dexName) => { subscription.unsubscribe().catch((error: Error) => { - if (!this.#isClearing) { - this.#deps.logger.error( - ensureError( - error, - 'HyperLiquidSubscriptionService.cleanupSharedWebData3ISubscription', - ), - this.#getErrorContext( - 'cleanupSharedWebData3ISubscription.clearinghouseState', - { - dex: dexName, - }, - ), - ); - } - }); - }, - ); - this.#clearinghouseStateSubscriptions.clear(); - } - - if (this.#openOrdersSubscriptions.size > 0) { - this.#openOrdersSubscriptions.forEach((subscription, dexName) => { - subscription.unsubscribe().catch((error: Error) => { - if (!this.#isClearing) { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.cleanupSharedWebData3ISubscription', ), this.#getErrorContext( - 'cleanupSharedWebData3ISubscription.openOrders', + 'cleanupSharedWebData3ISubscription.clearinghouseState', { dex: dexName, }, ), ); - } + }); + }, + ); + this.#clearinghouseStateSubscriptions.clear(); + } + + if (this.#openOrdersSubscriptions.size > 0) { + this.#openOrdersSubscriptions.forEach((subscription, dexName) => { + subscription.unsubscribe().catch((error: Error) => { + this.#logErrorUnlessClearing( + ensureError( + error, + 'HyperLiquidSubscriptionService.cleanupSharedWebData3ISubscription', + ), + this.#getErrorContext( + 'cleanupSharedWebData3ISubscription.openOrders', + { + dex: dexName, + }, + ), + ); }); }); this.#openOrdersSubscriptions.clear(); @@ -1863,7 +1876,7 @@ export class HyperLiquidSubscriptionService { // Ensure shared subscription is active this.#ensureSharedWebData3Subscription(accountId).catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.subscribeToPositions', @@ -1905,7 +1918,7 @@ export class HyperLiquidSubscriptionService { // Ensure webData3 subscription is active (OI caps come from webData3) this.#ensureSharedWebData3Subscription(accountId).catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError(error, 'HyperLiquidSubscriptionService.subscribeToOICaps'), this.#getErrorContext('subscribeToOICaps'), ); @@ -1947,7 +1960,7 @@ export class HyperLiquidSubscriptionService { // Ensure subscription is established for this accountId this.#ensureOrderFillISubscription(accountId).catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.subscribeToOrderFills', @@ -1966,7 +1979,7 @@ export class HyperLiquidSubscriptionService { this.#orderFillSubscriptions.get(normalizedAccountId); if (subscription) { subscription.unsubscribe().catch((error: Error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.subscribeToOrderFills', @@ -2092,7 +2105,7 @@ export class HyperLiquidSubscriptionService { // Ensure shared subscription is active this.#ensureSharedWebData3Subscription(accountId).catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError(error, 'HyperLiquidSubscriptionService.subscribeToOrders'), this.#getErrorContext('subscribeToOrders'), ); @@ -2129,7 +2142,7 @@ export class HyperLiquidSubscriptionService { // Ensure shared subscription is active (reuses existing connection) this.#ensureSharedWebData3Subscription(accountId).catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError(error, 'HyperLiquidSubscriptionService.subscribeToAccount'), this.#getErrorContext('subscribeToAccount'), ); @@ -2401,7 +2414,7 @@ export class HyperLiquidSubscriptionService { // Clear the promise on error so it can be retried this.#globalAllMidsPromise = undefined; - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.ensureGlobalAllMidsSubscription', @@ -2506,7 +2519,7 @@ export class HyperLiquidSubscriptionService { return undefined; }) .catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.ensureActiveAssetSubscription', @@ -2701,7 +2714,7 @@ export class HyperLiquidSubscriptionService { return undefined; }) .catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.createAssetCtxsSubscription', @@ -2736,7 +2749,7 @@ export class HyperLiquidSubscriptionService { if (subscription) { subscription.unsubscribe().catch((error: Error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.cleanupAssetCtxsSubscription', @@ -2799,7 +2812,7 @@ export class HyperLiquidSubscriptionService { return undefined; }) .catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.ensureBboSubscription', @@ -2890,7 +2903,7 @@ export class HyperLiquidSubscriptionService { try { await sub.unsubscribe(); } catch (unsubError: unknown) { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( unsubError, 'HyperLiquidSubscriptionService.subscribeToOrderBook', @@ -2907,7 +2920,7 @@ export class HyperLiquidSubscriptionService { return undefined; }) .catch((error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.subscribeToOrderBook', @@ -2926,7 +2939,7 @@ export class HyperLiquidSubscriptionService { cancelled = true; if (subscription) { subscription.unsubscribe().catch((error: Error) => { - this.#deps.logger.error( + this.#logErrorUnlessClearing( ensureError( error, 'HyperLiquidSubscriptionService.subscribeToOrderBook', @@ -3278,14 +3291,12 @@ export class HyperLiquidSubscriptionService { if (this.#clearinghouseStateSubscriptions.size > 0) { this.#clearinghouseStateSubscriptions.forEach((subscription, dexName) => { subscription.unsubscribe().catch((error: Error) => { - if (!this.#isClearing) { - this.#deps.logger.error( - ensureError(error, 'HyperLiquidSubscriptionService.clearAll'), - this.#getErrorContext('clearAll.clearinghouseState', { - dex: dexName, - }), - ); - } + this.#logErrorUnlessClearing( + ensureError(error, 'HyperLiquidSubscriptionService.clearAll'), + this.#getErrorContext('clearAll.clearinghouseState', { + dex: dexName, + }), + ); }); }); this.#clearinghouseStateSubscriptions.clear(); @@ -3294,14 +3305,12 @@ export class HyperLiquidSubscriptionService { if (this.#openOrdersSubscriptions.size > 0) { this.#openOrdersSubscriptions.forEach((subscription, dexName) => { subscription.unsubscribe().catch((error: Error) => { - if (!this.#isClearing) { - this.#deps.logger.error( - ensureError(error, 'HyperLiquidSubscriptionService.clearAll'), - this.#getErrorContext('clearAll.openOrders', { - dex: dexName, - }), - ); - } + this.#logErrorUnlessClearing( + ensureError(error, 'HyperLiquidSubscriptionService.clearAll'), + this.#getErrorContext('clearAll.openOrders', { + dex: dexName, + }), + ); }); }); this.#openOrdersSubscriptions.clear(); From 42fca9dde1d89c5cf15bf1abdad12ddd386d3c27 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 25 Feb 2026 19:34:28 +0000 Subject: [PATCH 041/131] [skip ci] Bump version number to 3805 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 82bd12cf0c9..c73d80e81d2 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.67.0" - versionCode 3804 + versionCode 3805 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 665bade08e1..2cf49a64ab3 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3804 + VERSION_NUMBER: 3805 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3804 + FLASK_VERSION_NUMBER: 3805 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 565d3405fb4..e5b905faaaf 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3804; + CURRENT_PROJECT_VERSION = 3805; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3804; + CURRENT_PROJECT_VERSION = 3805; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3804; + CURRENT_PROJECT_VERSION = 3805; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3804; + CURRENT_PROJECT_VERSION = 3805; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3804; + CURRENT_PROJECT_VERSION = 3805; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3804; + CURRENT_PROJECT_VERSION = 3805; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 86362e88f868fe493cbe06c16605ff769422b199 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:40:58 +0000 Subject: [PATCH 042/131] chore(runway): cherry-pick fix(card): resolve screen freeze when dismissing Daimo Pay webview (#26597) - fix(card): cp-7.67.0 resolve screen freeze when dismissing Daimo Pay webview (#26359) ## **Description** Fixes several issues in the Metal Card Daimo Pay checkout flow where the screen would freeze and become unresponsive after interacting with the Daimo webview. Key changes: - **Fix Pay button spinner stuck in loading state** (`ReviewOrder.tsx`): Moved `setIsCreatingPayment(false)` into a `finally` block so the loading state is always cleared after navigation, not only on error - **Fix screen freeze when dismissing Daimo webview** (`DaimoPayModal.tsx`): Injected a click listener that detects taps on the Daimo close button (`aria-label="Close"`) and backdrop (`daimo-modal-backdrop`), then sends a synthetic `modalClosed` event to trigger proper navigation back. Daimo's webview does not emit a `modalClosed` postMessage event natively. - **Add modal scrim backdrop** (`DaimoPayModal.tsx`): Changed the DaimoPayModal container background from fully transparent to a semi-transparent dark overlay (`bg-black/40`) so the modal has a visible backdrop - **Fix demo API URL** (`DaimoPayService.ts`): Updated the Daimo demo payment API URL from `pay.daimo.com` to `daimo.com` ## **Changelog** CHANGELOG entry: Fixed screen freeze when closing the Daimo Pay checkout webview and Pay button spinner getting stuck in loading state ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: Daimo Pay checkout flow stability Scenario: Pay button spinner resets after navigating to Daimo webview Given the user is on the Review Order screen When the user taps the Pay button And the Daimo payment is created successfully And the Daimo webview opens Then the Pay button should not show a loading spinner Scenario: Closing Daimo webview via X button unfreezes the screen Given the user is on the Daimo Pay webview When the user taps the X (Close) button inside the webview Then the Daimo webview should be dismissed And the Review Order screen should be fully interactive And the user should be able to tap Pay again or navigate back Scenario: Closing Daimo webview via backdrop tap unfreezes the screen Given the user is on the Daimo Pay webview When the user taps the backdrop area outside the Daimo dialog Then the Daimo webview should be dismissed And the Review Order screen should be fully interactive Scenario: Modal scrim is visible behind Daimo dialog Given the user is on the Daimo Pay webview Then a semi-transparent dark overlay should be visible behind the Daimo checkout dialog ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Touches the card checkout flow, including navigation and cache invalidation on payment success; issues could impact purchase completion paths but changes are small and covered by tests. > > **Overview** > Fixes several Daimo Pay checkout UX issues in the Metal Card flow. > > `ReviewOrder` now always clears the Pay button loading state via a `finally` block after attempting to create a payment (preventing a stuck spinner). > > `DaimoPayModal` now dispatches `clearCacheData('card-details')` on payment success, adds a semi-transparent scrim (`bg-black/40`) behind the WebView, and includes a new unit test asserting the cache clear behavior. The Daimo demo payment API endpoint is updated from `pay.daimo.com` to `daimo.com` (with test updates). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 43ad17bb25463b422d47fe0a2236c48e3f353819. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [2b6f9cb](https://github.com/MetaMask/metamask-mobile/commit/2b6f9cb4e1b4941d37345ac6749ac1111839caec) Co-authored-by: Bruno Nascimento --- .../UI/Card/Views/ReviewOrder/ReviewOrder.tsx | 1 + .../DaimoPayModal/DaimoPayModal.test.tsx | 43 ++++++++++++++++ .../DaimoPayModal/DaimoPayModal.tsx | 50 +++++++++++-------- .../UI/Card/services/DaimoPayService.test.ts | 2 +- .../UI/Card/services/DaimoPayService.ts | 2 +- 5 files changed, 74 insertions(+), 24 deletions(-) diff --git a/app/components/UI/Card/Views/ReviewOrder/ReviewOrder.tsx b/app/components/UI/Card/Views/ReviewOrder/ReviewOrder.tsx index 39ae37b26d3..e0fd9ca369b 100644 --- a/app/components/UI/Card/Views/ReviewOrder/ReviewOrder.tsx +++ b/app/components/UI/Card/Views/ReviewOrder/ReviewOrder.tsx @@ -149,6 +149,7 @@ const ReviewOrder = () => { 'ReviewOrder: Failed to create Daimo payment', ); setPaymentError(strings('card.review_order.payment_creation_error')); + } finally { setIsCreatingPayment(false); } }, [ diff --git a/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.test.tsx b/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.test.tsx index e6cfb7eeb34..572b2d9fb5c 100644 --- a/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.test.tsx +++ b/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.test.tsx @@ -5,6 +5,7 @@ import DaimoPayModal from './DaimoPayModal'; import { DaimoPayModalSelectors } from './DaimoPayModal.testIds'; import { MetaMetricsEvents } from '../../../../hooks/useMetrics'; import { CardScreens } from '../../util/metrics'; +import { clearCacheData } from '../../../../../core/redux/slices/card'; const mockNavigate = jest.fn(); const mockGoBack = jest.fn(); @@ -54,8 +55,10 @@ jest.mock('../../sdk', () => ({ }), })); +const mockReduxDispatch = jest.fn(); jest.mock('react-redux', () => ({ useSelector: jest.fn(() => []), + useDispatch: () => mockReduxDispatch, })); jest.mock('../../../../hooks/useMetrics', () => ({ @@ -160,6 +163,14 @@ jest.mock('../../../../../selectors/snaps/permissionController', () => ({ selectPermissionControllerState: jest.fn(() => ({})), })); +jest.mock('../../../../../core/redux/slices/card', () => ({ + selectIsDaimoDemo: jest.fn(), + clearCacheData: jest.fn((key: string) => ({ + type: 'card/clearCacheData', + payload: key, + })), +})); + jest.mock('../../../../../util/Logger', () => ({ error: jest.fn(), log: jest.fn(), @@ -473,6 +484,38 @@ describe('DaimoPayModal', () => { expect(mockDispatch).toHaveBeenCalled(); }); + it('clears card-details cache on payment success', async () => { + mockGetDaimoEnvironment.mockReturnValue('demo'); + + render(); + + await waitFor(() => { + expect(mockOnMessage).not.toBeNull(); + }); + + await act(async () => { + if (mockOnMessage) { + mockOnMessage({ + nativeEvent: { + data: JSON.stringify({ + source: 'daimo-pay', + version: 1, + type: 'paymentCompleted', + payload: { + txHash: '0x123', + chainId: 59144, + }, + }), + }, + }); + } + }); + + expect(mockReduxDispatch).toHaveBeenCalledWith( + clearCacheData('card-details'), + ); + }); + it('does not navigate on paymentCompleted in production mode - lets polling handle navigation', async () => { mockGetDaimoEnvironment.mockReturnValue('production'); diff --git a/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.tsx b/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.tsx index 8668235e8f3..f22c45aac4b 100644 --- a/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.tsx +++ b/app/components/UI/Card/components/DaimoPayModal/DaimoPayModal.tsx @@ -11,7 +11,7 @@ import { ButtonVariant, ButtonSize, } from '@metamask/design-system-react-native'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { isEqual } from 'lodash'; import { useParams } from '../../../../../util/navigation/navUtils'; import Logger from '../../../../../util/Logger'; @@ -35,7 +35,10 @@ import AppConstants from '../../../../../core/AppConstants'; import { getPermittedEvmAddressesByHostname } from '../../../../../core/Permissions'; import { selectPermissionControllerState } from '../../../../../selectors/snaps/permissionController'; import type { RootState } from '../../../../../reducers'; -import { selectIsDaimoDemo } from '../../../../../core/redux/slices/card'; +import { + selectIsDaimoDemo, + clearCacheData, +} from '../../../../../core/redux/slices/card'; import { getDaimoEnvironment } from '../../util/getDaimoEnvironment'; const POLLING_INTERVAL_MS = 5000; @@ -74,6 +77,7 @@ const DaimoPayModal: React.FC = () => { const iconRef = useRef(undefined); const navigation = useNavigation(); + const dispatch = useDispatch(); const { trackEvent, createEventBuilder } = useMetrics(); const { payId, fromUpgrade, orderId } = useParams(); const tw = useTailwind(); @@ -165,6 +169,8 @@ const DaimoPayModal: React.FC = () => { pollingIntervalRef.current = null; } + dispatch(clearCacheData('card-details')); + const parentNavigator = navigation.dangerouslyGetParent(); if (parentNavigator) { parentNavigator.dispatch( @@ -202,7 +208,7 @@ const DaimoPayModal: React.FC = () => { ); } }, - [trackEvent, createEventBuilder, navigation, fromUpgrade], + [trackEvent, createEventBuilder, navigation, fromUpgrade, dispatch], ); const handlePaymentBounced = useCallback( @@ -453,68 +459,68 @@ const DaimoPayModal: React.FC = () => { // These flags help wagmi identify the provider window.ethereum.isMetaMask = true; window.ethereum.isMetaMaskMobile = true; - + // Ensure _metamask namespace exists with required methods window.ethereum._metamask = window.ethereum._metamask || {}; if (!window.ethereum._metamask.isUnlocked) { - window.ethereum._metamask.isUnlocked = function() { - return Promise.resolve(true); + window.ethereum._metamask.isUnlocked = function() { + return Promise.resolve(true); }; } - + // Add connect method if missing if (!window.ethereum.connect) { window.ethereum.connect = function() { return window.ethereum.request({ method: 'eth_requestAccounts' }); }; } - + // Click interceptor to bypass wagmi's broken connector validation // and directly trigger eth_requestAccounts document.addEventListener('click', function(e) { var target = e.target; - + // Only process clicks on actual DOM elements within the document body if (!target || !document.body.contains(target)) { return; } - + var el = target; var foundWalletButton = null; - + // Walk up the DOM tree to find if this is a MetaMask wallet button // Look for max 5 levels to avoid matching large containers var depth = 0; while (el && el !== document.body && depth < 5) { var text = (el.innerText || el.textContent || '').toLowerCase().trim(); - + // Check if this looks like a wallet button: // 1. Contains "metamask" text // 2. Is short text (wallet name only, not a container) // 3. Is a clickable element (button, has role, or has cursor pointer) - var isClickable = el.tagName === 'BUTTON' || + var isClickable = el.tagName === 'BUTTON' || el.getAttribute('role') === 'button' || el.tagName === 'A' || (el.onclick !== null) || (window.getComputedStyle && window.getComputedStyle(el).cursor === 'pointer'); - - var isMetaMaskText = text === 'metamask' || + + var isMetaMaskText = text === 'metamask' || (text.includes('metamask') && text.length < 30); - + if (isMetaMaskText && isClickable) { foundWalletButton = el; break; } - + el = el.parentElement; depth++; } - + if (foundWalletButton) { // Prevent the default wagmi connector action e.preventDefault(); e.stopPropagation(); - + // Directly call eth_requestAccounts if (window.ethereum && window.ethereum.request) { window.ethereum.request({ method: 'eth_requestAccounts' }); @@ -526,7 +532,7 @@ const DaimoPayModal: React.FC = () => { // Transparency styles document.documentElement.style.backgroundColor = 'transparent'; document.body.style.backgroundColor = 'transparent'; - + var style = document.createElement('style'); style.textContent = \` html, body { @@ -545,7 +551,7 @@ const DaimoPayModal: React.FC = () => { } \`; document.head.appendChild(style); - + var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { @@ -616,7 +622,7 @@ const DaimoPayModal: React.FC = () => { return ( { expect(result.payId).toBe(mockPayId); expect(mockFetch).toHaveBeenCalledWith( - 'https://pay.daimo.com/api/payment', + 'https://daimo.com/api/payment', expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ diff --git a/app/components/UI/Card/services/DaimoPayService.ts b/app/components/UI/Card/services/DaimoPayService.ts index aeb451d99c9..c5b4b6f4f8c 100644 --- a/app/components/UI/Card/services/DaimoPayService.ts +++ b/app/components/UI/Card/services/DaimoPayService.ts @@ -11,7 +11,7 @@ export const DAIMO_WEBVIEW_BASE_URL = export const DAIMO_ALLOWED_ORIGIN = 'https://miniapp.daimo.com'; -const DAIMO_DEMO_API_URL = 'https://pay.daimo.com/api/payment'; +const DAIMO_DEMO_API_URL = 'https://daimo.com/api/payment'; const DAIMO_DEMO_API_KEY = 'pay-demo'; const DEMO_PAYMENT_CONFIG = { From 85a6b836a43e302bbd63024bc1a5cec99ddd0938 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 25 Feb 2026 21:42:36 +0000 Subject: [PATCH 043/131] [skip ci] Bump version number to 3806 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index c73d80e81d2..0ed96cea79f 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.67.0" - versionCode 3805 + versionCode 3806 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 2cf49a64ab3..64f07e2d532 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3805 + VERSION_NUMBER: 3806 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3805 + FLASK_VERSION_NUMBER: 3806 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index e5b905faaaf..b691612aaba 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3805; + CURRENT_PROJECT_VERSION = 3806; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3805; + CURRENT_PROJECT_VERSION = 3806; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3805; + CURRENT_PROJECT_VERSION = 3806; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3805; + CURRENT_PROJECT_VERSION = 3806; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3805; + CURRENT_PROJECT_VERSION = 3806; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3805; + CURRENT_PROJECT_VERSION = 3806; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From f365105f71efb1f0d98637453bc33aa5174c249a Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:15:33 +0000 Subject: [PATCH 044/131] chore(runway): cherry-pick fix: add optional abtest to more swaps and perps events (#26599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: add optional abtest to more swaps and perps events cp-7.67.0 (#26354) ## **Description** Core PR related: https://github.com/MetaMask/core/pull/8007 The Token Details page is running an A/B test (`tokenDetailsV2AbTest`) that compares a control layout vs. a treatment layout for action buttons (Swap, buy, sell, long, short). To make conversion measurement easier, we need to know whether users who **completed** a swap or perps trade were in the control or treatment group. Currently on `main`, only page-level view events carry the `ab_tests` property. Completion events (swap confirmed, perps trade executed) do not. ### What this PR does **1. Swaps: Thread `ab_tests` through `BridgeStatusController` (via patch)** The `ab_tests` context is read from the Redux `bridge` slice in `useSubmitBridgeTx` and passed through to `BridgeStatusController.submitTx` / `submitIntent`. **2. Perps: Thread `ab_tests` through `OrderParams.trackingData`** The `ab_tests` context (already available in `PerpsOrderView` via navigation params) is passed through the existing `trackingData` mechanism to `TradingService.#trackOrderResult`, which includes it in `PERPS_TRADE_TRANSACTION` events. ### Events carrying `ab_tests` — before vs. after #### Already on `main` (no changes needed) | Event | Flow | Where | |-------|------|-------| | `SWAP_PAGE_VIEWED` | Swaps | `BridgeView` reads from Redux `abTestContext` | | `PERPS_SCREEN_VIEWED` | Perps | `PerpsOrderView` reads from route params | #### New in this PR | Event | Flow | Where added | |-------|------|-------------| | `Unified SwapBridge Submitted` | Swaps | `BridgeStatusController.submitTx` / `submitIntent` | | `Unified SwapBridge Completed` | Swaps | `BridgeStatusController` history lookup | | `Unified SwapBridge Failed` | Swaps | `BridgeStatusController` history lookup | | `Unified SwapBridge Polling Status Updated` | Swaps | `BridgeStatusController` history lookup | | `PERPS_TRADE_TRANSACTION` (status: `executed`) | Perps | `TradingService.#trackOrderResult` | | `PERPS_TRADE_TRANSACTION` (status: `failed`) | Perps | `TradingService.#trackOrderResult` | ## **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. ## **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** > Touches analytics plumbing across swap/bridge controller patches and perps trade tracking; risk is mainly around event schema/propagation and controller API signature changes, not core transaction execution. > > **Overview** > Threads optional **A/B test context** into swap/bridge and perps *completion* analytics so downstream events can be attributed to token-details layout experiments. > > For swaps/bridge, the PR adds Yarn patches to `@metamask/bridge-controller`/`@metamask/bridge-status-controller` to accept and emit `ab_tests`, stores `abTests` on bridge tx history items, and resolves it for later `UnifiedSwapBridge` events (completed/failed/polling updates). The app now reads `abTestContext` from the Redux `bridge` slice in `useSubmitBridgeTx` and passes it through both `submitTx` and intent flow (`handleIntentTransaction` → `submitIntent`). > > For perps, `PerpsOrderView` includes the route’s token-details layout variant in `OrderParams.trackingData.abTests`, `TradingService` adds `ab_tests` to `PERPS_TRADE_TRANSACTION` properties when present, and perps analytics typing is widened to allow `Record` values. Separately, `analytics/queue` now re-drains the queue after a processing batch to handle operations enqueued during processing. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f86de6f378bd7f8e8fa22edf28dee76387728411. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [4135c1a](https://github.com/MetaMask/metamask-mobile/commit/4135c1a705cde5318f57a8efaa4651a71a0c073d) Co-authored-by: sahar-fehri --- ...dge-controller-npm-66.2.0-8ed148964f.patch | 21 ++ ...tus-controller-npm-66.1.0-a2a478ef94.patch | 182 ++++++++++++++++++ .../Views/PerpsOrderView/PerpsOrderView.tsx | 7 + app/controllers/perps/constants/eventNames.ts | 1 + .../perps/services/TradingService.ts | 7 + app/controllers/perps/types/index.ts | 5 +- app/lib/transaction/intent.ts | 2 + app/util/analytics/queue.ts | 5 + .../bridge/hooks/useSubmitBridgeTx.test.tsx | 6 + app/util/bridge/hooks/useSubmitBridgeTx.ts | 12 +- package.json | 6 +- yarn.lock | 60 +++++- 12 files changed, 306 insertions(+), 8 deletions(-) create mode 100644 .yarn/patches/@metamask-bridge-controller-npm-66.2.0-8ed148964f.patch create mode 100644 .yarn/patches/@metamask-bridge-status-controller-npm-66.1.0-a2a478ef94.patch diff --git a/.yarn/patches/@metamask-bridge-controller-npm-66.2.0-8ed148964f.patch b/.yarn/patches/@metamask-bridge-controller-npm-66.2.0-8ed148964f.patch new file mode 100644 index 00000000000..81600648e17 --- /dev/null +++ b/.yarn/patches/@metamask-bridge-controller-npm-66.2.0-8ed148964f.patch @@ -0,0 +1,21 @@ +diff --git a/dist/utils/metrics/types.d.cts b/dist/utils/metrics/types.d.cts +index 7710230d39c86041c7e0fecd7caa3e27a8130b16..9e1e6978d432b9703aaafdd5b8f238cd5f302d2c 100644 +--- a/dist/utils/metrics/types.d.cts ++++ b/dist/utils/metrics/types.d.cts +@@ -151,6 +151,7 @@ type RequiredEventContextFromClientBase = { + export type RequiredEventContextFromClient = { + [K in keyof RequiredEventContextFromClientBase]: RequiredEventContextFromClientBase[K] & { + location?: MetaMetricsSwapsEventSource; ++ ab_tests?: Record; + }; + }; + /** +@@ -196,6 +197,7 @@ export type EventPropertiesFromControllerState = { + export type CrossChainSwapsEventProperties = { + action_type: MetricsActionType; + location: MetaMetricsSwapsEventSource; ++ ab_tests?: Record; + } | Pick[T] | Pick[T]; + export {}; + //# sourceMappingURL=types.d.cts.map +\ No newline at end of file diff --git a/.yarn/patches/@metamask-bridge-status-controller-npm-66.1.0-a2a478ef94.patch b/.yarn/patches/@metamask-bridge-status-controller-npm-66.1.0-a2a478ef94.patch new file mode 100644 index 00000000000..7428514b798 --- /dev/null +++ b/.yarn/patches/@metamask-bridge-status-controller-npm-66.1.0-a2a478ef94.patch @@ -0,0 +1,182 @@ +diff --git a/dist/bridge-status-controller.cjs b/dist/bridge-status-controller.cjs +index 45003640c463ecf21f4a6ddd57d3d4244d46be94..28bb117e186c4a81bbd63966c2547b0c590f9b1f 100644 +--- a/dist/bridge-status-controller.cjs ++++ b/dist/bridge-status-controller.cjs +@@ -207,7 +207,7 @@ class BridgeStatusController extends (0, polling_controller_1.StaticIntervalPoll + }); + }); + _BridgeStatusController_addTxToHistory.set(this, (startPollingForBridgeTxStatusArgs, actionId) => { +- const { bridgeTxMeta, statusRequest, quoteResponse, startTime, slippagePercentage, initialDestAssetBalance, targetContractAddress, approvalTxId, isStxEnabled, location, accountAddress: selectedAddress, } = startPollingForBridgeTxStatusArgs; ++ const { bridgeTxMeta, statusRequest, quoteResponse, startTime, slippagePercentage, initialDestAssetBalance, targetContractAddress, approvalTxId, isStxEnabled, location, abTests, accountAddress: selectedAddress, } = startPollingForBridgeTxStatusArgs; + // Determine the key for this history item: + // - For pre-submission (non-batch EVM): use actionId + // - For post-submission or other cases: use bridgeTxMeta.id +@@ -248,6 +248,7 @@ class BridgeStatusController extends (0, polling_controller_1.StaticIntervalPoll + isStxEnabled: isStxEnabled ?? false, + featureId: quoteResponse.featureId, + location, ++ ...(abTests && { abTests }), + }; + this.update((state) => { + // Use actionId as key for pre-submission, or txMeta.id for post-submission +@@ -704,9 +705,10 @@ class BridgeStatusController extends (0, polling_controller_1.StaticIntervalPoll + * @param isStxEnabledOnClient - Whether smart transactions are enabled on the client, for example the getSmartTransactionsEnabled selector value from the extension + * @param quotesReceivedContext - The context for the QuotesReceived event + * @param location - The entry point from which the user initiated the swap or bridge (e.g. Main View, Token View, Trending Explore) ++ * @param abTests - A/B test context to attribute events to specific experiments + * @returns The transaction meta + */ +- this.submitTx = async (accountAddress, quoteResponse, isStxEnabledOnClient, quotesReceivedContext, location = bridge_controller_1.MetaMetricsSwapsEventSource.MainView) => { ++ this.submitTx = async (accountAddress, quoteResponse, isStxEnabledOnClient, quotesReceivedContext, location = bridge_controller_1.MetaMetricsSwapsEventSource.MainView, abTests) => { + this.messenger.call('BridgeController:stopPollingForQuotes', bridge_controller_1.AbortReason.TransactionSubmitted, + // If trade is submitted before all quotes are loaded, the QuotesReceived event is published + // If the trade has a featureId, it means it was submitted outside of the Unified Swap and Bridge experience, so no QuotesReceived event is published +@@ -716,7 +718,7 @@ class BridgeStatusController extends (0, polling_controller_1.StaticIntervalPoll + throw new Error('Failed to submit cross-chain swap transaction: undefined multichain account'); + } + const isHardwareAccount = (0, bridge_controller_1.isHardwareWallet)(selectedAccount); +- const preConfirmationProperties = (0, metrics_1.getPreConfirmationPropertiesFromQuote)(quoteResponse, isStxEnabledOnClient, isHardwareAccount, location); ++ const preConfirmationProperties = (0, metrics_1.getPreConfirmationPropertiesFromQuote)(quoteResponse, isStxEnabledOnClient, isHardwareAccount, location, abTests); + // Emit Submitted event after submit button is clicked + !quoteResponse.featureId && + __classPrivateFieldGet(this, _BridgeStatusController_trackUnifiedSwapBridgeEvent, "f").call(this, bridge_controller_1.UnifiedSwapBridgeEventName.Submitted, undefined, preConfirmationProperties); +@@ -838,6 +840,7 @@ class BridgeStatusController extends (0, polling_controller_1.StaticIntervalPoll + startTime, + approvalTxId, + location, ++ abTests, + }, actionId); + // Pass txFee when gasIncluded is true to use the quote's gas fees + // instead of re-estimating (which would fail for max native token swaps) +@@ -878,6 +881,7 @@ class BridgeStatusController extends (0, polling_controller_1.StaticIntervalPoll + startTime, + approvalTxId, + location, ++ abTests, + }); + } + if ((0, bridge_controller_1.isNonEvmChainId)(quoteResponse.quote.srcChainId)) { +@@ -903,15 +907,16 @@ class BridgeStatusController extends (0, polling_controller_1.StaticIntervalPoll + * @param params.signature - Hex signature produced by eth_signTypedData_v4 + * @param params.accountAddress - The EOA submitting the order + * @param params.location - The entry point from which the user initiated the swap or bridge ++ * @param params.abTests - A/B test context to attribute events to specific experiments + * @returns A lightweight TransactionMeta-like object for history linking + */ + this.submitIntent = async (params) => { +- const { quoteResponse, signature, accountAddress, location } = params; ++ const { quoteResponse, signature, accountAddress, location, abTests } = params; + this.messenger.call('BridgeController:stopPollingForQuotes', bridge_controller_1.AbortReason.TransactionSubmitted); + // Build pre-confirmation properties for error tracking parity with submitTx + const account = __classPrivateFieldGet(this, _BridgeStatusController_instances, "m", _BridgeStatusController_getMultichainSelectedAccount).call(this, accountAddress); + const isHardwareAccount = Boolean(account) && (0, bridge_controller_1.isHardwareWallet)(account); +- const preConfirmationProperties = (0, metrics_1.getPreConfirmationPropertiesFromQuote)(quoteResponse, false, isHardwareAccount, location); ++ const preConfirmationProperties = (0, metrics_1.getPreConfirmationPropertiesFromQuote)(quoteResponse, false, isHardwareAccount, location, abTests); + try { + const intent = (0, transaction_1.getIntentFromQuote)(quoteResponse); + // If backend provided an approval tx for this intent quote, submit it first (on-chain), +@@ -1007,6 +1012,7 @@ class BridgeStatusController extends (0, polling_controller_1.StaticIntervalPoll + approvalTxId, + startTime, + location, ++ abTests + }); + // Start polling using the orderId key to route to intent manager + __classPrivateFieldGet(this, _BridgeStatusController_startPollingForTxId, "f").call(this, bridgeHistoryKey); +@@ -1033,12 +1039,20 @@ class BridgeStatusController extends (0, polling_controller_1.StaticIntervalPoll + * @param eventProperties - The properties for the event + */ + _BridgeStatusController_trackUnifiedSwapBridgeEvent.set(this, (eventName, txMetaId, eventProperties) => { ++ const historyAbTests = txMetaId ++ ? this.state.txHistory?.[txMetaId]?.abTests ++ : undefined; ++ const resolvedAbTests = eventProperties?.ab_tests ?? historyAbTests ?? undefined; + const baseProperties = { + action_type: bridge_controller_1.MetricsActionType.SWAPBRIDGE_V1, + location: eventProperties?.location ?? + (txMetaId ? this.state.txHistory?.[txMetaId]?.location : undefined) ?? + bridge_controller_1.MetaMetricsSwapsEventSource.MainView, + ...(eventProperties ?? {}), ++ ...(resolvedAbTests && ++ Object.keys(resolvedAbTests).length > 0 && { ++ ab_tests: resolvedAbTests, ++ }), + }; + // This will publish events for PERPS dropped tx failures as well + if (!txMetaId) { +diff --git a/dist/bridge-status-controller.d.cts b/dist/bridge-status-controller.d.cts +index a2b3f4c..b5e8d1a 100644 +--- a/dist/bridge-status-controller.d.cts ++++ b/dist/bridge-status-controller.d.cts +@@ -85,9 +85,10 @@ export declare class BridgeStatusController extends BridgeStatusController_base< + * @param isStxEnabledOnClient - Whether smart transactions are enabled on the client, for example the getSmartTransactionsEnabled selector value from the extension + * @param quotesReceivedContext - The context for the QuotesReceived event + * @param location - The entry point from which the user initiated the swap or bridge (e.g. Main View, Token View, Trending Explore) ++ * @param abTests - A/B test context to attribute events to specific experiments + * @returns The transaction meta + */ +- submitTx: (accountAddress: string, quoteResponse: QuoteResponse & QuoteMetadata, isStxEnabledOnClient: boolean, quotesReceivedContext?: RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesReceived], location?: MetaMetricsSwapsEventSource) => Promise>; ++ submitTx: (accountAddress: string, quoteResponse: QuoteResponse & QuoteMetadata, isStxEnabledOnClient: boolean, quotesReceivedContext?: RequiredEventContextFromClient[UnifiedSwapBridgeEventName.QuotesReceived], location?: MetaMetricsSwapsEventSource, abTests?: Record) => Promise>; + /** + * UI-signed intent submission (fast path): the UI generates the EIP-712 signature and calls this with the raw signature. + * Here we submit the order to the intent provider and create a synthetic history entry for UX. +@@ -97,12 +98,14 @@ export declare class BridgeStatusController extends BridgeStatusController_base< + * @param params.signature - Hex signature produced by eth_signTypedData_v4 + * @param params.accountAddress - The EOA submitting the order + * @param params.location - The entry point from which the user initiated the swap or bridge ++ * @param params.abTests - A/B test context to attribute events to specific experiments + * @returns A lightweight TransactionMeta-like object for history linking + */ + submitIntent: (params: { + quoteResponse: QuoteResponse & QuoteMetadata; + signature: string; + accountAddress: string; + location?: MetaMetricsSwapsEventSource; ++ abTests?: Record; + }) => Promise>; + } +diff --git a/dist/types.d.cts b/dist/types.d.cts +index a77021907623ca60681a979c0ef0bfeee5ca3150..0175527dc5c6e7b48bb0dfdf95f43e66cc5c8b2f 100644 +--- a/dist/types.d.cts ++++ b/dist/types.d.cts +@@ -95,6 +95,11 @@ export type BridgeHistoryItem = { + * Used to attribute swaps to specific flows (e.g. Trending Explore). + */ + location?: MetaMetricsSwapsEventSource; ++ /** ++ * A/B test context to attribute swap/bridge events to specific experiments. ++ * Keys are test names, values are variant names (e.g. { token_details_layout: 'treatment' }). ++ */ ++ abTests?: Record; + /** + * Attempts tracking for exponential backoff on failed fetches. + * We track the number of attempts and the last attempt time for each txMetaId that has failed at least once +@@ -160,6 +165,7 @@ export type StartPollingForBridgeTxStatusArgs = { + approvalTxId?: BridgeHistoryItem['approvalTxId']; + isStxEnabled?: BridgeHistoryItem['isStxEnabled']; + location?: BridgeHistoryItem['location']; ++ abTests?: BridgeHistoryItem['abTests']; + accountAddress: string; + }; + /** +diff --git a/dist/utils/metrics.cjs b/dist/utils/metrics.cjs +index 775367bc08c8a46c19a78913903573a295d1f677..51778fae2ab2c5f08eb66ee18667451002f62296 100644 +--- a/dist/utils/metrics.cjs ++++ b/dist/utils/metrics.cjs +@@ -109,7 +109,7 @@ exports.getPriceImpactFromQuote = getPriceImpactFromQuote; + * @param location - The entry point from which the user initiated the swap or bridge (e.g. Main View, Token View, Trending Explore) + * @returns The properties for the pre-confirmation event + */ +-const getPreConfirmationPropertiesFromQuote = (quoteResponse, isStxEnabledOnClient, isHardwareAccount, location = bridge_controller_1.MetaMetricsSwapsEventSource.MainView) => { ++const getPreConfirmationPropertiesFromQuote = (quoteResponse, isStxEnabledOnClient, isHardwareAccount, location = bridge_controller_1.MetaMetricsSwapsEventSource.MainView, abTests) => { + const { quote } = quoteResponse; + return { + ...(0, exports.getPriceImpactFromQuote)(quote), +@@ -125,6 +125,7 @@ const getPreConfirmationPropertiesFromQuote = (quoteResponse, isStxEnabledOnClie + action_type: bridge_controller_1.MetricsActionType.SWAPBRIDGE_V1, + custom_slippage: false, // TODO detect whether the user changed the default slippage + location, ++ ...(abTests && Object.keys(abTests).length > 0 && { ab_tests: abTests }), + }; + }; + exports.getPreConfirmationPropertiesFromQuote = getPreConfirmationPropertiesFromQuote; diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx index 13b8a179f48..439ce05266b 100644 --- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx +++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx @@ -1039,6 +1039,12 @@ const PerpsOrderViewContentBase: React.FC = ({ mmPayTokenSelected: payToken.symbol ?? '', mmPayNetworkSelected: String(payToken.chainId ?? ''), }), + ...(routeAbTestTokenDetailsLayout && { + abTests: { + assetsASSETS2493AbtestTokenDetailsLayout: + routeAbTestTokenDetailsLayout, + }, + }), }, }; @@ -1118,6 +1124,7 @@ const PerpsOrderViewContentBase: React.FC = ({ payToken, onDepositConfirm, handleDepositConfirm, + routeAbTestTokenDetailsLayout, fromTokenDetails, ], ); diff --git a/app/controllers/perps/constants/eventNames.ts b/app/controllers/perps/constants/eventNames.ts index c051a5c6a0c..2d596f1b56b 100644 --- a/app/controllers/perps/constants/eventNames.ts +++ b/app/controllers/perps/constants/eventNames.ts @@ -84,6 +84,7 @@ export const PERPS_EVENT_PROPERTY = { WARNING_MESSAGE: 'warning_message', ERROR_TYPE: 'error_type', ERROR_MESSAGE: 'error_message', + AB_TESTS: 'ab_tests', COMPLETION_DURATION_TUTORIAL: 'completion_duration_tutorial', STEPS_VIEWED: 'steps_viewed', VIEW_OCCURRENCES: 'view_occurrences', diff --git a/app/controllers/perps/services/TradingService.ts b/app/controllers/perps/services/TradingService.ts index 20a2d39ea8e..db5dc1b950f 100644 --- a/app/controllers/perps/services/TradingService.ts +++ b/app/controllers/perps/services/TradingService.ts @@ -211,6 +211,13 @@ export class TradingService { error?.message ?? result?.error ?? 'Unknown error'; } + if ( + params.trackingData?.abTests && + Object.keys(params.trackingData.abTests).length > 0 + ) { + properties[PERPS_EVENT_PROPERTY.AB_TESTS] = params.trackingData.abTests; + } + this.#deps.metrics.trackPerpsEvent( PerpsAnalyticsEvent.TradeTransaction, properties, diff --git a/app/controllers/perps/types/index.ts b/app/controllers/perps/types/index.ts index 8bcbab46a94..ea6bdaa78ff 100644 --- a/app/controllers/perps/types/index.ts +++ b/app/controllers/perps/types/index.ts @@ -126,6 +126,9 @@ export type TrackingData = { tradeWithToken?: boolean; mmPayTokenSelected?: string; // Token symbol when tradeWithToken is true mmPayNetworkSelected?: string; // chainId when tradeWithToken is true + + // A/B test context to attribute trade events to specific experiments + abTests?: Record; }; // TP/SL-specific tracking data for analytics events @@ -1269,7 +1272,7 @@ export type PerpsTraceValue = string | number | boolean; */ export type PerpsAnalyticsProperties = Record< string, - string | number | boolean | null | undefined + string | number | boolean | Record | null | undefined >; /** diff --git a/app/lib/transaction/intent.ts b/app/lib/transaction/intent.ts index 4654a39e072..9c545a92bd0 100644 --- a/app/lib/transaction/intent.ts +++ b/app/lib/transaction/intent.ts @@ -104,6 +104,7 @@ function isBytes32Hex(value: string): boolean { export async function handleIntentTransaction( quoteResponse: BridgeQuoteResponse, selectedAccountAddress: string | undefined, + abTests?: Record, ) { const signatureControllerMessenger = getSignatureControllerMessenger( Engine.controllerMessenger, @@ -170,6 +171,7 @@ export async function handleIntentTransaction( >[0]['quoteResponse'], signature, accountAddress, + abTests, }); } diff --git a/app/util/analytics/queue.ts b/app/util/analytics/queue.ts index 74396db393a..eeebdc64279 100644 --- a/app/util/analytics/queue.ts +++ b/app/util/analytics/queue.ts @@ -234,6 +234,11 @@ export const createAnalyticsQueueManager = ( type: 'SET_PROCESSING', payload: { processing: false, promise: null }, }); + + // Drain any operations that were queued while this batch was running + if (canProcessQueue(state)) { + await processQueue(); + } }); // Set processing promise in state (mark as processing) - must happen BEFORE the promise starts executing diff --git a/app/util/bridge/hooks/useSubmitBridgeTx.test.tsx b/app/util/bridge/hooks/useSubmitBridgeTx.test.tsx index 9ed8bb1f6a3..5bccc34ee1a 100644 --- a/app/util/bridge/hooks/useSubmitBridgeTx.test.tsx +++ b/app/util/bridge/hooks/useSubmitBridgeTx.test.tsx @@ -119,6 +119,9 @@ describe('useSubmitBridgeTx', () => { }, }, }, + bridge: { + abTestContext: undefined, + }, swaps: { featureFlags: { smart_transactions: { @@ -172,6 +175,7 @@ describe('useSubmitBridgeTx', () => { true, undefined, undefined, + undefined, ); expect(txResult).toEqual({ chainId: '0x1', @@ -221,6 +225,7 @@ describe('useSubmitBridgeTx', () => { true, undefined, undefined, + undefined, ); expect(txResult).toEqual({ chainId: '0x1', @@ -383,6 +388,7 @@ describe('useSubmitBridgeTx', () => { expect(mockHandleIntentTransaction).toHaveBeenCalledWith( mockQuoteResponse, '0x1234567890123456789012345678901234567890', + undefined, ); expect(mockSubmitTx).not.toHaveBeenCalled(); expect(txResult).toEqual(mockIntentResult); diff --git a/app/util/bridge/hooks/useSubmitBridgeTx.ts b/app/util/bridge/hooks/useSubmitBridgeTx.ts index 00aedf2bbc1..c85ff2e955b 100644 --- a/app/util/bridge/hooks/useSubmitBridgeTx.ts +++ b/app/util/bridge/hooks/useSubmitBridgeTx.ts @@ -4,11 +4,20 @@ import Engine from '../../../core/Engine'; import { useSelector } from 'react-redux'; import { selectShouldUseSmartTransaction } from '../../../selectors/smartTransactionsController'; import { selectSourceWalletAddress } from '../../../selectors/bridge'; +import { selectAbTestContext } from '../../../core/redux/slices/bridge'; import { handleIntentTransaction } from '../../../lib/transaction/intent'; export default function useSubmitBridgeTx() { const stxEnabled = useSelector(selectShouldUseSmartTransaction); const walletAddress = useSelector(selectSourceWalletAddress); + const abTestContext = useSelector(selectAbTestContext); + + const abTests = abTestContext?.assetsASSETS2493AbtestTokenDetailsLayout + ? { + assetsASSETS2493AbtestTokenDetailsLayout: + abTestContext.assetsASSETS2493AbtestTokenDetailsLayout, + } + : undefined; const submitBridgeTx = async ({ quoteResponse, @@ -20,7 +29,7 @@ export default function useSubmitBridgeTx() { }) => { // check whether quoteResponse is an intent transaction if (quoteResponse.quote.intent) { - return handleIntentTransaction(quoteResponse, walletAddress); + return handleIntentTransaction(quoteResponse, walletAddress, abTests); } if (!walletAddress) { throw new Error('Wallet address is not set'); @@ -34,6 +43,7 @@ export default function useSubmitBridgeTx() { stxEnabled, undefined, // quotesReceivedContext location, + abTests, ); }; diff --git a/package.json b/package.json index f23d19c037f..3a2ccf0d2dc 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,8 @@ "@metamask/transaction-controller@npm:^61.0.0": "patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch", "@metamask/transaction-controller@npm:^62.9.2": "^62.14.0", "viem": "2.31.3", + "@metamask/bridge-controller@npm:^66.2.0": "patch:@metamask/bridge-controller@npm%3A66.2.0#~/.yarn/patches/@metamask-bridge-controller-npm-66.2.0-8ed148964f.patch", + "@metamask/bridge-status-controller@npm:^66.0.2": "patch:@metamask/bridge-status-controller@npm%3A66.1.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-66.1.0-a2a478ef94.patch", "@metamask/core-backend": "^5.0.0", "bn.js@npm:4.11.6": "4.12.3", "bn.js@npm:5.2.1": "5.2.3" @@ -200,8 +202,8 @@ "@metamask/assets-controllers": "^99.4.0", "@metamask/base-controller": "^9.0.0", "@metamask/bitcoin-wallet-snap": "^1.10.0", - "@metamask/bridge-controller": "^66.2.0", - "@metamask/bridge-status-controller": "^66.1.0", + "@metamask/bridge-controller": "patch:@metamask/bridge-controller@npm%3A66.2.0#~/.yarn/patches/@metamask-bridge-controller-npm-66.2.0-8ed148964f.patch", + "@metamask/bridge-status-controller": "patch:@metamask/bridge-status-controller@npm%3A66.1.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-66.1.0-a2a478ef94.patch", "@metamask/chain-agnostic-permission": "^1.3.0", "@metamask/connectivity-controller": "^0.1.0", "@metamask/controller-utils": "^11.18.0", diff --git a/yarn.lock b/yarn.lock index 24c98ca7f96..e3e8fd98cf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7964,7 +7964,7 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-controller@npm:^66.2.0": +"@metamask/bridge-controller@npm:66.2.0": version: 66.2.0 resolution: "@metamask/bridge-controller@npm:66.2.0" dependencies: @@ -8027,7 +8027,38 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-status-controller@npm:^66.1.0": +"@metamask/bridge-controller@patch:@metamask/bridge-controller@npm%3A66.2.0#~/.yarn/patches/@metamask-bridge-controller-npm-66.2.0-8ed148964f.patch": + version: 66.2.0 + resolution: "@metamask/bridge-controller@patch:@metamask/bridge-controller@npm%3A66.2.0#~/.yarn/patches/@metamask-bridge-controller-npm-66.2.0-8ed148964f.patch::version=66.2.0&hash=b845b2" + 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:^36.0.0" + "@metamask/assets-controllers": "npm:^99.4.0" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/controller-utils": "npm:^11.18.0" + "@metamask/gas-fee-controller": "npm:^26.0.2" + "@metamask/keyring-api": "npm:^21.5.0" + "@metamask/messenger": "npm:^0.3.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-network-controller": "npm:^3.0.3" + "@metamask/network-controller": "npm:^29.0.0" + "@metamask/polling-controller": "npm:^16.0.2" + "@metamask/remote-feature-flag-controller": "npm:^4.0.0" + "@metamask/snaps-controllers": "npm:^17.2.0" + "@metamask/transaction-controller": "npm:^62.17.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/f8105319ffdc0baa8c8296ba202c6e41b06c21b5fdd75bcfa26b5c1e11b4edc8d6fabda8664763c39accff6c451b599c0d6597cb52150a6540dce37ef2c3c339 + languageName: node + linkType: hard + +"@metamask/bridge-status-controller@npm:66.1.0": version: 66.1.0 resolution: "@metamask/bridge-status-controller@npm:66.1.0" dependencies: @@ -8070,6 +8101,27 @@ __metadata: languageName: node linkType: hard +"@metamask/bridge-status-controller@patch:@metamask/bridge-status-controller@npm%3A66.1.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-66.1.0-a2a478ef94.patch": + version: 66.1.0 + resolution: "@metamask/bridge-status-controller@patch:@metamask/bridge-status-controller@npm%3A66.1.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-66.1.0-a2a478ef94.patch::version=66.1.0&hash=85f030" + dependencies: + "@metamask/accounts-controller": "npm:^36.0.0" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/bridge-controller": "npm:^66.2.0" + "@metamask/controller-utils": "npm:^11.18.0" + "@metamask/gas-fee-controller": "npm:^26.0.2" + "@metamask/network-controller": "npm:^29.0.0" + "@metamask/polling-controller": "npm:^16.0.2" + "@metamask/snaps-controllers": "npm:^17.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/transaction-controller": "npm:^62.17.0" + "@metamask/utils": "npm:^11.9.0" + bignumber.js: "npm:^9.1.2" + uuid: "npm:^8.3.2" + checksum: 10/dda8c7049da9542e08647e3a546421993271b7e6049eca1722df4d1be6f8ef5773a1da6d1a6833a920dc9cdd02aa78a0cdfdd1b2b6520b56fe8123b86ff1fe87 + languageName: node + linkType: hard + "@metamask/browser-passworder@npm:^5.0.0": version: 5.0.0 resolution: "@metamask/browser-passworder@npm:5.0.0" @@ -35493,8 +35545,8 @@ __metadata: "@metamask/auto-changelog": "npm:^5.3.0" "@metamask/base-controller": "npm:^9.0.0" "@metamask/bitcoin-wallet-snap": "npm:^1.10.0" - "@metamask/bridge-controller": "npm:^66.2.0" - "@metamask/bridge-status-controller": "npm:^66.1.0" + "@metamask/bridge-controller": "patch:@metamask/bridge-controller@npm%3A66.2.0#~/.yarn/patches/@metamask-bridge-controller-npm-66.2.0-8ed148964f.patch" + "@metamask/bridge-status-controller": "patch:@metamask/bridge-status-controller@npm%3A66.1.0#~/.yarn/patches/@metamask-bridge-status-controller-npm-66.1.0-a2a478ef94.patch" "@metamask/browser-passworder": "npm:^5.0.0" "@metamask/browser-playground": "npm:0.2.0" "@metamask/build-utils": "npm:^3.0.0" From a2a4fe934175ab9a902c21b7b1cc4df99abd1b2a Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 25 Feb 2026 23:17:09 +0000 Subject: [PATCH 045/131] [skip ci] Bump version number to 3807 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 0ed96cea79f..4ef0e759ae6 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.67.0" - versionCode 3806 + versionCode 3807 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 64f07e2d532..7a55158673b 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3806 + VERSION_NUMBER: 3807 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3806 + FLASK_VERSION_NUMBER: 3807 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index b691612aaba..26de1ad63cc 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3806; + CURRENT_PROJECT_VERSION = 3807; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3806; + CURRENT_PROJECT_VERSION = 3807; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3806; + CURRENT_PROJECT_VERSION = 3807; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3806; + CURRENT_PROJECT_VERSION = 3807; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3806; + CURRENT_PROJECT_VERSION = 3807; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3806; + CURRENT_PROJECT_VERSION = 3807; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From d06b101bb9f76d0dd68ef1d736b50e637af4a8c2 Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:42:57 +0100 Subject: [PATCH 046/131] chore(runway): cherry-pick fix: bnjs audit issue (#26627) Cherry-pick #26481 CI is broken due to a vulnerability with bn.js image CHANGELOG entry: null Fixes: ```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] ``` - [ ] 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. - [ ] 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 because this only changes dependency pins/lockfile, but it can affect any code paths relying on `bn.js` behavior across transitive deps. > > **Overview** > Pins `bn.js` to newer patch releases by adding explicit Yarn `resolutions` for both v4 and v5, and updates the `bnjs4`/`bnjs5` aliases accordingly to resolve an audit vulnerability. > > Updates related type dependency pinning by moving `@types/bnjs5` to `@types/bn.js@^5.2.0`, with corresponding `yarn.lock` refresh to reflect the new `bn.js` and `@types/bn.js` versions. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a946b35edae8f4414e895487c7f39745086a80d4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). ## **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. ## **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. --------- Co-authored-by: Bernardo Garces Chapero --- package.json | 10 ++++++---- yarn.lock | 47 ++++++++++++----------------------------------- 2 files changed, 18 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index 5e34182c11b..396f10582fe 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,9 @@ "@metamask/transaction-controller@npm:^61.0.0": "patch:@metamask/transaction-controller@npm%3A62.10.0#~/.yarn/patches/@metamask-transaction-controller-npm-62.10.0-e1d24e7db5.patch", "@metamask/transaction-controller@npm:^62.9.2": "^62.14.0", "viem": "2.31.3", - "@metamask/assets-controllers@npm:^99.2.0": "patch:@metamask/assets-controllers@npm%3A99.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-99.2.0-bf6ecbcde9.patch" + "@metamask/assets-controllers@npm:^99.2.0": "patch:@metamask/assets-controllers@npm%3A99.2.0#~/.yarn/patches/@metamask-assets-controllers-npm-99.2.0-bf6ecbcde9.patch", + "bn.js@npm:4.11.6": "4.12.3", + "bn.js@npm:5.2.1": "5.2.3" }, "dependencies": { "@config-plugins/detox": "^9.0.0", @@ -331,8 +333,8 @@ "axios": "^1.13.5", "bignumber.js": "^9.0.1", "bitcoin-address-validation": "2.2.3", - "bnjs4": "npm:bn.js@^4.12.0", - "bnjs5": "npm:bn.js@^5.2.1", + "bnjs4": "npm:bn.js@^4.12.3", + "bnjs5": "npm:bn.js@^5.2.3", "buffer": "6.0.3", "cockatiel": "^3.1.2", "compare-versions": "^3.6.0", @@ -516,7 +518,7 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/react-native": "^13.2.0", "@types/bnjs4": "npm:@types/bn.js@^4.11.6", - "@types/bnjs5": "npm:@types/bn.js@^5.1.6", + "@types/bnjs5": "npm:@types/bn.js@^5.2.0", "@types/enzyme": "^3.10.12", "@types/eth-url-parser": "^1.0.0", "@types/i18n-js": "^3.8.4", diff --git a/yarn.lock b/yarn.lock index c17bab36410..7908eb76d30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18132,7 +18132,7 @@ __metadata: languageName: node linkType: hard -"@types/bn.js@npm:^5.1.0, @types/bn.js@npm:^5.1.5": +"@types/bn.js@npm:^5.1.0, @types/bn.js@npm:^5.1.5, @types/bnjs5@npm:@types/bn.js@^5.2.0": version: 5.2.0 resolution: "@types/bn.js@npm:5.2.0" dependencies: @@ -18141,15 +18141,6 @@ __metadata: languageName: node linkType: hard -"@types/bnjs5@npm:@types/bn.js@^5.1.6": - version: 5.1.6 - resolution: "@types/bn.js@npm:5.1.6" - dependencies: - "@types/node": "npm:*" - checksum: 10/db565b5a2af59b09459d74441153bf23a0e80f1fb2d070330786054e7ce1a7285dc40afcd8f289426c61a83166bdd70814f70e2d439744686aac5d3ea75daf13 - languageName: node - linkType: hard - "@types/body-parser@npm:*": version: 1.19.2 resolution: "@types/body-parser@npm:1.19.2" @@ -23038,31 +23029,17 @@ __metadata: languageName: node linkType: hard -"bn.js@npm:4.11.6": - version: 4.11.6 - resolution: "bn.js@npm:4.11.6" - checksum: 10/22741b015c9fff60fce32fc9988331b298eb9b6db5bfb801babb23b846eaaf894e440e0d067b2b3ae4e46aab754e90972f8f333b31bf94a686bbcb054bfa7b14 - languageName: node - linkType: hard - -"bn.js@npm:5.2.1, bnjs5@npm:bn.js@^5.2.1": - version: 5.2.1 - resolution: "bn.js@npm:5.2.1" - checksum: 10/7a7e8764d7a6e9708b8b9841b2b3d6019cc154d2fc23716d0efecfe1e16921b7533c6f7361fb05471eab47986c4aa310c270f88e3507172104632ac8df2cfd84 - languageName: node - linkType: hard - -"bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.0, bn.js@npm:^4.11.8, bn.js@npm:^4.11.9, bnjs4@npm:bn.js@^4.12.0": - version: 4.12.0 - resolution: "bn.js@npm:4.12.0" - checksum: 10/10f8db196d3da5adfc3207d35d0a42aa29033eb33685f20ba2c36cadfe2de63dad05df0a20ab5aae01b418d1c4b3d4d205273085262fa020d17e93ff32b67527 +"bn.js@npm:4.12.3, bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.0, bn.js@npm:^4.11.8, bn.js@npm:^4.11.9, bnjs4@npm:bn.js@^4.12.3": + version: 4.12.3 + resolution: "bn.js@npm:4.12.3" + checksum: 10/57ed5a055f946f3e009f1589c45a5242db07f3dddfc72e4506f0dd9d8b145f0dbee4edabc2499288f3fc338eb712fb96a1c623a2ed2bcd49781df1a64db64dd1 languageName: node linkType: hard -"bn.js@npm:^5.0.0, bn.js@npm:^5.1.2, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1, bn.js@npm:^5.2.2": - version: 5.2.2 - resolution: "bn.js@npm:5.2.2" - checksum: 10/51ebb2df83b33e5d8581165206e260d5e9c873752954616e5bf3758952b84d7399a9c6d00852815a0aeefb1150a7f34451b62d4287342d457fa432eee869e83e +"bn.js@npm:5.2.3, bn.js@npm:^5.0.0, bn.js@npm:^5.1.2, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1, bn.js@npm:^5.2.2, bnjs5@npm:bn.js@^5.2.3": + version: 5.2.3 + resolution: "bn.js@npm:5.2.3" + checksum: 10/dfb3927e0d531e6ec4f191597ce6f7f7665310c356fef5f968ada676b8058027f959af42eaa37b5f5c63617e819d3741813025ab15dd71a90f2e74698df0b58e languageName: node linkType: hard @@ -35381,7 +35358,7 @@ __metadata: "@tommasini/react-native-scrollable-tab-view": "npm:^1.1.1" "@tradle/react-native-http": "npm:2.0.1" "@types/bnjs4": "npm:@types/bn.js@^4.11.6" - "@types/bnjs5": "npm:@types/bn.js@^5.1.6" + "@types/bnjs5": "npm:@types/bn.js@^5.2.0" "@types/enzyme": "npm:^3.10.12" "@types/eth-url-parser": "npm:^1.0.0" "@types/he": "npm:^1.2.3" @@ -35436,8 +35413,8 @@ __metadata: base64-js: "npm:^1.5.1" bignumber.js: "npm:^9.0.1" bitcoin-address-validation: "npm:2.2.3" - bnjs4: "npm:bn.js@^4.12.0" - bnjs5: "npm:bn.js@^5.2.1" + bnjs4: "npm:bn.js@^4.12.3" + bnjs5: "npm:bn.js@^5.2.3" buffer: "npm:6.0.3" chromedriver: "npm:^123.0.1" cockatiel: "npm:^3.1.2" From 87003b2ab220d991e8496cab86b215fad1884c64 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:06:24 +0000 Subject: [PATCH 047/131] chore(runway): cherry-pick refactor: Updated affected pages to use SafeAreaView from react-native-safe-area-context cp-7.67.0 (#26624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refactor: Updated affected pages to use SafeAreaView from react-native-safe-area-context cp-7.67.0 (#26536) ## **Description** This PR aligns four screens with the app’s header pattern: use `SafeAreaView` from `react-native-safe-area-context` (not from `react-native`) and pass `includesTopInset` on headers so the header handles the top safe area consistently. **Reason for change:** Screens that use headers from `@app/component-library/components-temp/` should be wrapped in `SafeAreaView` from `react-native-safe-area-context` and use `includesTopInset` on the header for correct safe area behavior, especially on devices with notches and dynamic islands. **What changed:** 1. **ImportPrivateKey** (`app/components/Views/ImportPrivateKey/index.tsx`) – Replaced `SafeAreaView` import from `react-native` with `react-native-safe-area-context`. `HeaderStackedStandard` already had `includesTopInset`; no header change. 2. **SelectHardware** (`app/components/Views/ConnectHardware/SelectHardware/index.tsx`) – Replaced `SafeAreaView` import from `react-native` with `react-native-safe-area-context`. Added `includesTopInset` to `HeaderStackedStandard`. 3. **AppInformation** (`app/components/Views/Settings/AppInformation/index.js`) – Replaced `SafeAreaView` import from `react-native` with `react-native-safe-area-context`. Added `includesTopInset` to `HeaderCompactStandard`. 4. **PredictFeed** (`app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx`) – Wrapped the screen in `SafeAreaView` from `react-native-safe-area-context` with `tw.style('flex-1 bg-default')`. Added `includesTopInset` to `HeaderCompactStandard`. Removed manual `paddingTop: insets.top` from the header container and removed the unused `useSafeAreaInsets()` call in the main component (insets remain used in `PredictSearchOverlay`). ## **Changelog** This PR is not end-user-facing; it improves safe area handling and header alignment on devices with notches and dynamic islands. CHANGELOG entry: Fixed Header position for Import Private Key, Select hardware, app ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/DSYS-363?focusedCommentId=395773 ## **Manual testing steps** ```gherkin Feature: SafeAreaView and header top inset Scenario: Import Private Key screen respects safe area Given the app is open When the user navigates to Import Private Key (e.g. Add account → Import private key) Then the header and content respect the device safe area (no overlap with notch/status bar) Scenario: Select Hardware screen respects safe area Given the app is open When the user navigates to Connect Hardware Wallet → Select hardware Then the header and content respect the device safe area Scenario: App Information (About MetaMask) respects safe area Given the app is open When the user navigates to Settings → About Then the header and content respect the device safe area Scenario: Predict feed respects safe area Given the app is open and user can access Predict When the user opens the Predict feed (market list) Then the header and content respect the device safe area ``` ## **Screenshots/Recordings** ### **Before** ### **After** ![IMG_0099](https://github.com/user-attachments/assets/2594e6f1-bb9d-4ff6-b948-bbf28b678b87) ![IMG_0100](https://github.com/user-attachments/assets/0d0c439a-4be1-4751-b31a-02a7ddd48276) ![IMG_0101](https://github.com/user-attachments/assets/9f728173-7930-4e49-9847-5a1078bb6987) ![IMG_0102](https://github.com/user-attachments/assets/c57a3c25-e25f-40ee-8044-36aa2086617b) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Low risk UI/layout refactor that adjusts safe-area handling on a few screens; potential regressions are limited to header/content spacing on devices with notches or unusual insets. > > **Overview** > Updates several screens to standardize safe-area behavior by switching to `SafeAreaView` from `react-native-safe-area-context` with `edges={{ bottom: 'additive' }}`. > > Aligns headers with the shared pattern by enabling `includesTopInset` on `HeaderCompactStandard`/`HeaderStackedStandard` and removing manual top inset padding in `PredictFeed` (now wrapped in a `SafeAreaView`). Jest snapshots are updated to reflect the new `RNCSafeAreaView` output and edge configuration. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 33ad4884cf943990a35e8d9b54132891133c3337. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [3f4e05e](https://github.com/MetaMask/metamask-mobile/commit/3f4e05e0eaaf98327840688e726e8a2f032aee94) Co-authored-by: Brian August Nguyen --- .../Predict/views/PredictFeed/PredictFeed.tsx | 119 +++++++++--------- .../ConnectHardware/SelectHardware/index.tsx | 12 +- .../__snapshots__/index.test.tsx.snap | 12 +- .../Views/ImportPrivateKey/index.tsx | 11 +- .../__snapshots__/index.test.tsx.snap | 16 ++- .../Views/Settings/AppInformation/index.js | 4 +- 6 files changed, 96 insertions(+), 78 deletions(-) diff --git a/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx b/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx index b38cd007cb5..6a3db326fc7 100644 --- a/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx +++ b/app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx @@ -14,7 +14,10 @@ import { LayoutChangeEvent, } from 'react-native'; import PagerView from 'react-native-pager-view'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { + SafeAreaView, + useSafeAreaInsets, +} from 'react-native-safe-area-context'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; import { Box, @@ -584,7 +587,6 @@ const PredictFeed: React.FC = () => { const tw = useTailwind(); const { colors } = useTheme(); - const insets = useSafeAreaInsets(); const navigation = useNavigation(); const route = useRoute>(); @@ -678,68 +680,73 @@ const PredictFeed: React.FC = () => { ); return ( - - - - - - + + + - {layoutReady && ( - + - )} - - - + {layoutReady && ( + + )} + + + + + ); }; diff --git a/app/components/Views/ConnectHardware/SelectHardware/index.tsx b/app/components/Views/ConnectHardware/SelectHardware/index.tsx index fb4a3456728..25ab3dbcc58 100644 --- a/app/components/Views/ConnectHardware/SelectHardware/index.tsx +++ b/app/components/Views/ConnectHardware/SelectHardware/index.tsx @@ -3,13 +3,8 @@ import { useNavigation } from '@react-navigation/native'; import React, { useEffect } from 'react'; -import { - Image, - SafeAreaView, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native'; +import { Image, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { strings } from '../../../../../locales/i18n'; import Text, { TextVariant, @@ -160,8 +155,9 @@ const SelectHardwareWallet = () => { }; return ( - + - - + diff --git a/app/components/Views/ImportPrivateKey/index.tsx b/app/components/Views/ImportPrivateKey/index.tsx index 8bb909912db..49c9bcdc3a4 100644 --- a/app/components/Views/ImportPrivateKey/index.tsx +++ b/app/components/Views/ImportPrivateKey/index.tsx @@ -1,12 +1,7 @@ import React, { useEffect, useState, useRef } from 'react'; import { useSelector } from 'react-redux'; -import { - Alert, - TextInput, - View, - DimensionValue, - SafeAreaView, -} from 'react-native'; +import { Alert, TextInput, View, DimensionValue } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import { ScreenshotDeterrent } from '../../UI/ScreenshotDeterrent'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; @@ -145,7 +140,7 @@ const ImportPrivateKey = () => { }; return ( - + - - + diff --git a/app/components/Views/Settings/AppInformation/index.js b/app/components/Views/Settings/AppInformation/index.js index 3f4e2165d40..18b1d3f1101 100644 --- a/app/components/Views/Settings/AppInformation/index.js +++ b/app/components/Views/Settings/AppInformation/index.js @@ -1,7 +1,6 @@ /* eslint-disable dot-notation */ import React, { PureComponent } from 'react'; import { - SafeAreaView, StyleSheet, Image, Text, @@ -10,6 +9,7 @@ import { ScrollView, TouchableOpacity, } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { getApplicationName, getVersion, @@ -203,10 +203,12 @@ class AppInformation extends PureComponent { return ( this.props.navigation.goBack()} backButtonProps={{ testID: AboutMetaMaskSelectorsIDs.BACK_BUTTON }} From d5e6fbf2a8d619ec50817f41b7560015788dabe1 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 26 Feb 2026 16:07:58 +0000 Subject: [PATCH 048/131] [skip ci] Bump version number to 3814 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 4ef0e759ae6..c97677d05ec 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.67.0" - versionCode 3807 + versionCode 3814 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 7a55158673b..80898010de8 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3807 + VERSION_NUMBER: 3814 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3807 + FLASK_VERSION_NUMBER: 3814 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 26de1ad63cc..13a6e195b88 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3807; + CURRENT_PROJECT_VERSION = 3814; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3807; + CURRENT_PROJECT_VERSION = 3814; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3807; + CURRENT_PROJECT_VERSION = 3814; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3807; + CURRENT_PROJECT_VERSION = 3814; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3807; + CURRENT_PROJECT_VERSION = 3814; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3807; + CURRENT_PROJECT_VERSION = 3814; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 37c9992f603d929c4e5ffe29386358508b496f78 Mon Sep 17 00:00:00 2001 From: Wei Sun Date: Thu, 26 Feb 2026 09:53:39 -0800 Subject: [PATCH 049/131] add changelog for ota 7.66.1 --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2cea1ccb1..a2c777b3f3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [7.66.1] +### OTA fixes + +- remove process.env spread + ## [7.65.0] ### Added @@ -10498,8 +10502,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.66.1...HEAD -[7.66.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.65.0...v7.66.1 +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.66.0...HEAD +[7.66.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.65.0...v7.66.0 [7.65.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.1...v7.65.0 [7.64.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.0...v7.64.1 [7.64.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.63.1...v7.64.0 From d1cd8e802745cfb5d9ea4b981939b0a0a4fcd472 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:13:07 +0000 Subject: [PATCH 050/131] chore(runway): cherry-pick fix: AccountsApiBalanceFetcher stricter zero out conditions (#26642) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: AccountsApiBalanceFetcher stricter zero out conditions cp-7.67.0 (#26590) ## **Description** Patch for the core fix: https://github.com/MetaMask/core/pull/8044 https://consensyssoftware.atlassian.net/browse/ASSETS-2796 ## **Changelog** CHANGELOG entry: fix: AccountsApiBalanceFetcher stricter zero out conditions ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/40324 (this issue also happens on mobile!) #26618 ## **Manual testing steps** 1. Add avalanche and have avalanche tokens 2. Select avalanche only in the network picker - EXPECTED: see avalanche erc-20 balances 3. Select popular networks in the network picket - EXPECTED: see avalanche erc-20 balances ## **Screenshots/Recordings** ### **Before** ### **After** https://www.loom.com/share/d6e472d2ab444205acdad64507f43eed ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Touches balance-fetching logic (via a Yarn patch to `@metamask/assets-controllers`), which can affect displayed native/ERC-20 balances across networks if the new gating conditions are wrong. Scope is small and isolated to when missing API results are backfilled with zeroes. > > **Overview** > Applies a Yarn patch to `@metamask/assets-controllers@99.4.0` that tightens `AccountsApiBalanceFetcher` “zero-out” behavior: missing native and ERC-20 balances are only backfilled with `0` when the `chainId` was included in the original request and `supports(chainId)` is true. > > Updates `package.json` and `yarn.lock` to consume the patched dependency via `.yarn/patches/...`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 28f899bbbf389d4d9b7877c467efbb2740d8734a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [ed2fb6c](https://github.com/MetaMask/metamask-mobile/commit/ed2fb6c5ed019e468a9b19410a3a1e783ba831e0) Co-authored-by: Prithpal Sooriya --- ...ts-controllers-npm-99.4.0-13861a06b6.patch | 28 ++++++++ package.json | 2 +- yarn.lock | 64 +++++++++++++++++-- 3 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 .yarn/patches/@metamask-assets-controllers-npm-99.4.0-13861a06b6.patch diff --git a/.yarn/patches/@metamask-assets-controllers-npm-99.4.0-13861a06b6.patch b/.yarn/patches/@metamask-assets-controllers-npm-99.4.0-13861a06b6.patch new file mode 100644 index 00000000000..dc19783ed9a --- /dev/null +++ b/.yarn/patches/@metamask-assets-controllers-npm-99.4.0-13861a06b6.patch @@ -0,0 +1,28 @@ +diff --git a/dist/multi-chain-accounts-service/api-balance-fetcher.cjs b/dist/multi-chain-accounts-service/api-balance-fetcher.cjs +index 6ae7034bd87dfdaa49324fd36a340689df45960e..4719739bf194f5fb8669f382b021b54952294508 100644 +--- a/dist/multi-chain-accounts-service/api-balance-fetcher.cjs ++++ b/dist/multi-chain-accounts-service/api-balance-fetcher.cjs +@@ -150,7 +150,10 @@ class AccountsApiBalanceFetcher { + chains.forEach((chainId) => { + const key = `${address}-${chainId}`; + const existingBalance = nativeBalancesFromAPI.get(key); +- if (!existingBalance) { ++ const isChainIncludedInRequest = chainIds.includes(chainId); ++ const isChainSupported = this.supports(chainId); ++ const shouldZeroOutBalance = !existingBalance && isChainIncludedInRequest && isChainSupported; ++ if (shouldZeroOutBalance) { + // Add zero native balance entry if API succeeded but didn't return one + results.push({ + success: true, +@@ -172,7 +175,10 @@ class AccountsApiBalanceFetcher { + const key = `${account.toLowerCase()}-${tokenLowerCase}-${chainId}`; + const isERC = tokenAddress !== ZERO_ADDRESS; + const existingBalance = nonNativeBalancesFromAPI.get(key); +- if (isERC && !existingBalance) { ++ const isChainIncludedInRequest = chainIds.includes(chainId); ++ const isChainSupported = this.supports(chainId); ++ const shouldZeroOutBalance = !existingBalance && isChainIncludedInRequest && isChainSupported; ++ if (isERC && shouldZeroOutBalance) { + results.push({ + success: true, + value: new bn_js_1.default('0'), diff --git a/package.json b/package.json index 3a2ccf0d2dc..1411b231f28 100644 --- a/package.json +++ b/package.json @@ -199,7 +199,7 @@ "@metamask/app-metadata-controller": "^2.0.0", "@metamask/approval-controller": "^8.0.0", "@metamask/assets-controller": "^2.0.0", - "@metamask/assets-controllers": "^99.4.0", + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A99.4.0#~/.yarn/patches/@metamask-assets-controllers-npm-99.4.0-13861a06b6.patch", "@metamask/base-controller": "^9.0.0", "@metamask/bitcoin-wallet-snap": "^1.10.0", "@metamask/bridge-controller": "patch:@metamask/bridge-controller@npm%3A66.2.0#~/.yarn/patches/@metamask-bridge-controller-npm-66.2.0-8ed148964f.patch", diff --git a/yarn.lock b/yarn.lock index e3e8fd98cf5..e72241965c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7783,6 +7783,62 @@ __metadata: languageName: node linkType: hard +"@metamask/assets-controllers@npm:99.4.0, @metamask/assets-controllers@npm:^99.4.0": + version: 99.4.0 + resolution: "@metamask/assets-controllers@npm:99.4.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/abi-utils": "npm:^2.0.3" + "@metamask/account-tree-controller": "npm:^4.1.1" + "@metamask/accounts-controller": "npm:^36.0.0" + "@metamask/approval-controller": "npm:^8.0.0" + "@metamask/base-controller": "npm:^9.0.0" + "@metamask/contract-metadata": "npm:^2.4.0" + "@metamask/controller-utils": "npm:^11.18.0" + "@metamask/core-backend": "npm:5.0.0" + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/keyring-api": "npm:^21.5.0" + "@metamask/keyring-controller": "npm:^25.1.0" + "@metamask/messenger": "npm:^0.3.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-account-service": "npm:^7.0.0" + "@metamask/network-controller": "npm:^29.0.0" + "@metamask/network-enablement-controller": "npm:^4.1.0" + "@metamask/permission-controller": "npm:^12.2.0" + "@metamask/phishing-controller": "npm:^16.2.0" + "@metamask/polling-controller": "npm:^16.0.2" + "@metamask/preferences-controller": "npm:^22.1.0" + "@metamask/profile-sync-controller": "npm:^27.1.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-controllers": "npm:^17.2.0" + "@metamask/snaps-sdk": "npm:^10.3.0" + "@metamask/snaps-utils": "npm:^11.7.0" + "@metamask/storage-service": "npm:^1.0.0" + "@metamask/transaction-controller": "npm:^62.17.0" + "@metamask/utils": "npm:^11.9.0" + "@types/bn.js": "npm:^5.1.5" + "@types/uuid": "npm:^8.3.0" + async-mutex: "npm:^0.5.0" + bitcoin-address-validation: "npm:^2.2.3" + bn.js: "npm:^5.2.1" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + multiformats: "npm:^9.9.0" + reselect: "npm:^5.1.1" + single-call-balance-checker-abi: "npm:^1.0.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/providers": ^22.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/2329ba8efe5a19ebe836c8ddc492f732461078d3954b713e825e4f0f3f5dc5fb17d55f5dabd30a20bd25e33366e8f2358a23b227c182f36688908f0128c5046c + languageName: node + linkType: hard + "@metamask/assets-controllers@npm:^100.0.2": version: 100.0.3 resolution: "@metamask/assets-controllers@npm:100.0.3" @@ -7839,9 +7895,9 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:^99.4.0": +"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A99.4.0#~/.yarn/patches/@metamask-assets-controllers-npm-99.4.0-13861a06b6.patch": version: 99.4.0 - resolution: "@metamask/assets-controllers@npm:99.4.0" + resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A99.4.0#~/.yarn/patches/@metamask-assets-controllers-npm-99.4.0-13861a06b6.patch::version=99.4.0&hash=7df952" dependencies: "@ethereumjs/util": "npm:^9.1.0" "@ethersproject/abi": "npm:^5.7.0" @@ -7891,7 +7947,7 @@ __metadata: peerDependencies: "@metamask/providers": ^22.0.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/2329ba8efe5a19ebe836c8ddc492f732461078d3954b713e825e4f0f3f5dc5fb17d55f5dabd30a20bd25e33366e8f2358a23b227c182f36688908f0128c5046c + checksum: 10/02c0ec3c99148245ef0860fd544b1c0cf81e41a74c6d9f126a5ae8b79668b3c3a91c62e94c7302134a9f2d48ae11370d9e21f4d933037c77cc664b6c113bbe4d languageName: node linkType: hard @@ -35541,7 +35597,7 @@ __metadata: "@metamask/app-metadata-controller": "npm:^2.0.0" "@metamask/approval-controller": "npm:^8.0.0" "@metamask/assets-controller": "npm:^2.0.0" - "@metamask/assets-controllers": "npm:^99.4.0" + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A99.4.0#~/.yarn/patches/@metamask-assets-controllers-npm-99.4.0-13861a06b6.patch" "@metamask/auto-changelog": "npm:^5.3.0" "@metamask/base-controller": "npm:^9.0.0" "@metamask/bitcoin-wallet-snap": "npm:^1.10.0" From d4a6f2b7b92a384619c4e9e28262cbb3aee6033d Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 26 Feb 2026 19:14:48 +0000 Subject: [PATCH 051/131] [skip ci] Bump version number to 3817 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index c97677d05ec..c010c1a5f77 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.67.0" - versionCode 3814 + versionCode 3817 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 80898010de8..181f0c10011 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3814 + VERSION_NUMBER: 3817 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3814 + FLASK_VERSION_NUMBER: 3817 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 13a6e195b88..0dad3a0f1a4 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3814; + CURRENT_PROJECT_VERSION = 3817; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3814; + CURRENT_PROJECT_VERSION = 3817; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3814; + CURRENT_PROJECT_VERSION = 3817; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3814; + CURRENT_PROJECT_VERSION = 3817; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3814; + CURRENT_PROJECT_VERSION = 3817; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3814; + CURRENT_PROJECT_VERSION = 3817; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 88bcdb760959386a9d6f6b89862b7c21bf35b073 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:24:43 +0000 Subject: [PATCH 052/131] chore(runway): cherry-pick f07db0a (#26647) - chore: remove process.env spread (#26368) ## **Description** We shouldn't be spreading then accessing process.env variables. It doesn't work correctly with the dev watcher Example: ``` console.log({ x: process.env.METAMASK_ENVIRONMENT, y: { ...process.env }?.METAMASK_ENVIRONMENT, z: { ...process.env }?.NODE_ENV, }); Returns { x: 'dev', y: undefined // IT FAILS z: 'development' } ``` ## **Changelog** CHANGELOG entry: chore: remove process.env spread ## **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. ## **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: small change to environment detection logic and test Babel config; main risk is behavior differences in builds/tests that rely on `isProduction()` evaluation timing. > > **Overview** > Fixes `isProduction()` to read `process.env.METAMASK_ENVIRONMENT` directly (removing the `{ ...process.env }` spread that could yield `undefined` under the dev watcher). > > Updates `babel.config.tests.js` to exclude `app/util/environment.ts` (and its test) from `transform-inline-environment-variables`, preventing env vars from being inlined for these files during tests. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 85f54fedb5c751831ac2413563a896e41a7225d1. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [f07db0a](https://github.com/MetaMask/metamask-mobile/commit/f07db0a325ea9a47c9db4de23a86eba9ba59801d) Co-authored-by: Prithpal Sooriya --- app/util/environment.ts | 4 +--- babel.config.tests.js | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/util/environment.ts b/app/util/environment.ts index 7cd325cbea3..01c267532bf 100644 --- a/app/util/environment.ts +++ b/app/util/environment.ts @@ -4,9 +4,7 @@ // This needs to be updated to check for the METAMASK_ENVIRONMENT environment variable instead of NODE_ENV // Once this is updated, verify that e2e smoke tests are working as expected export const isProduction = (): boolean => - // TODO: process.env.NODE_ENV === 'production' doesn't work with tests yet. Once we make it work, - // we can remove the following line and use the code above instead. - ({ ...process.env })?.METAMASK_ENVIRONMENT === 'production'; + process.env.METAMASK_ENVIRONMENT === 'production'; export const isGatorPermissionsFeatureEnabled = (): boolean => process.env.GATOR_PERMISSIONS_ENABLED?.toString() === 'true'; diff --git a/babel.config.tests.js b/babel.config.tests.js index caa1ebde485..f01efe92db2 100644 --- a/babel.config.tests.js +++ b/babel.config.tests.js @@ -35,6 +35,8 @@ const newOverrides = [ 'app/components/UI/Ramp/hooks/useRampTokens.test.ts', 'app/components/Views/confirmations/hooks/pay/useTransactionPayWithdraw.ts', 'app/components/Views/confirmations/hooks/pay/useTransactionPayWithdraw.test.ts', + 'app/util/environment.ts', + 'app/util/environment.test.ts', 'app/store/migrations/**', 'app/util/networks/customNetworks.tsx', ], From 29dcf19bdf0e31d7f31c21497cb8a0c1600258a3 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 26 Feb 2026 20:26:13 +0000 Subject: [PATCH 053/131] [skip ci] Bump version number to 3823 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index c010c1a5f77..c84b3bd7b3f 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.67.0" - versionCode 3817 + versionCode 3823 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 181f0c10011..1e3bcda7097 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3817 + VERSION_NUMBER: 3823 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3817 + FLASK_VERSION_NUMBER: 3823 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 0dad3a0f1a4..ecbeaeda82c 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3817; + CURRENT_PROJECT_VERSION = 3823; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3817; + CURRENT_PROJECT_VERSION = 3823; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3817; + CURRENT_PROJECT_VERSION = 3823; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3817; + CURRENT_PROJECT_VERSION = 3823; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3817; + CURRENT_PROJECT_VERSION = 3823; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3817; + CURRENT_PROJECT_VERSION = 3823; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 588a4fa52e852acb7a6bdc0de9e3092468df3d2c Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Fri, 27 Feb 2026 13:36:08 +0000 Subject: [PATCH 054/131] [skip ci] Bump version number to 3837 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 126abb1a05c..b66555641ef 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.68.0" - versionCode 3607 + versionCode 3837 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index fefd5a029f4..d624b046ece 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.68.0 - opts: is_expand: false - VERSION_NUMBER: 3607 + VERSION_NUMBER: 3837 - opts: is_expand: false FLASK_VERSION_NAME: 7.68.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3607 + FLASK_VERSION_NUMBER: 3837 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index de69b670991..e6d7e243028 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3837; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3837; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3837; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3837; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3837; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3607; + CURRENT_PROJECT_VERSION = 3837; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From acb3b62e73e80a3979a5e33743bbaafe6a06e816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:15:30 +0000 Subject: [PATCH 055/131] update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c777b3f3c..2970de9bbd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [7.66.1] -### OTA fixes +### Fixed -- remove process.env spread +- remove process.env spread (#26528) ## [7.65.0] From 92eb6483a89e8ef97ef2e331c9f77e558a4f37dd Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Fri, 27 Feb 2026 18:23:48 +0000 Subject: [PATCH 056/131] update changelog for 7.66.1 (hotfix - no test plan) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2970de9bbd8..f14c1f57c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10502,8 +10502,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.66.0...HEAD -[7.66.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.65.0...v7.66.0 +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.66.1...HEAD +[7.66.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.65.0...v7.66.1 [7.65.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.1...v7.65.0 [7.64.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.0...v7.64.1 [7.64.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.63.1...v7.64.0 From e66f5d6951e5eabcadfc41c319d80efb51d30e7d Mon Sep 17 00:00:00 2001 From: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:33:54 -0330 Subject: [PATCH 057/131] release: release/7.66.0-Changelog (#26044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates the change log for 7.66.0. (Hotfix - no test plan generated.) Co-authored-by: metamaskbot Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> --- CHANGELOG.md | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f14c1f57c5a..b07aa287c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,125 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - remove process.env spread (#26528) +## [7.66.0] + +### Added + +- Adds a page for changing preferred ramp provider (#25860) +- Add asset overview deeplinks (#25447) +- Restored the previously selected "Pay with" token when returning to the Perps order view within 5 minutes. (#25938) +- Fixed predict transaction toast notifications not appearing when navigating away from the Predict tab (#25863) +- Added new Accounts Menu screen to organize settings navigation with Settings, Manage, and Resources sections (#25611) +- Adds Bridge and Swap feature to `MegaETH` (#25906) +- Adds chiliz.png as network logo and enables it in metamask mobile (#25437) +- Always display learn more about perps link (#25958) +- Created new token list item v2 (#25824) +- Added custom claim transaction request screen for mUSD bonus claims with improved UX flow (#25837) +- Added an "Ending soon" tab to prediction markets feed showing markets sorted by end date (#25868) +- Removed legacy homepage script injection and related RPC methods (#25620) +- Add google/web search inside browser search bar (#25897) +- Homogenize spacing on Explore page for perps items (#25894) +- Added 1st interaction alert to warn users when interacting with an address for the first time. (#25575) +- Added icons to the bridge token selector network pills (#25851) +- Create feature flag for the new unified assets state (#25891) +- Adds Bridge and Swap feature to HyperEVM (#25769) +- Added lightweight position display and one-click Long/Short trading on token details page for perps-enabled assets (#25685) +- Improved browser tab switching performance by keeping tabs mounted (#25702) +- Validation errors from non-EVM transaction snaps will now be displayed to users during send flow. (#25648) +- Added detailed transaction display for mUSD reward claims showing claimed amount, network fee, and received total (#25452) +- Adds functionality for selecting a payment method (#25681) +- Base setup for in-app provisioning (#25669) +- Update the look of the "Earn %" CTA displayed for ETH and Tron staking products to tag style (#25722) +- Added educational bottom sheet explaining that mUSD bonuses are claimed on Linea, and auto-scroll to the resulting token (#25516) + after successful claim +- Added Perps “Pay with” option (Perps balance or other tokens) and info tooltip on the order view. (#25626) +- Updates the "Earn a 3% bonus" text in the mUSD conversion CTA to be clickable. (#25676) +- Added new token details button layout behind a feature flag (#25574) +- Added payment method deeplink support for ramps (#25003) +- Bring back destination asset sync to new swaps asset picker (#25644) +- Add sanitized origin to sentinel metadata (#25612) +- Added a warning message when gas sponsorship is unavailable due to reserve balance requirements. (#25320) +- Added new design of the perps empty state (#25581) +- Added A/B test for homepage featured section (carousel vs list) with variant-specific analytics; replaced empty predictions (#25237) + state with featured markets; hide balance card when no positions exist; + removed dead code +- Added Buy/Sell sticky action bar to Token Details page with smart token selection (#25499) +- Updated the Browser Tabs View with a new top navigation bar, 2-column grid layout, and improved navigation behavior (#25470) +- Allow user to opt-in all accounts at once to Rewards (#24450) +- Improved Perps home screen load time by making price prewarming non-blocking (#25501) +- Add rewards season 2 season status banner (#25522) +- Remove legacy swaps liveness service in favor of new stx hooks (#25506) +- Added points estimate history tracking to state logs for Customer Support diagnostics (#25389) +- When one-click trade transaction creation fails, users now see an error toast ("Could not open position") and the failure is (#25429) + tracked in analytics. +- New retryWithDelay utility - A generic, well-tested retry utility (#24920) + Updated getAuthTokens - Now automatically retries up to + 3 times on transient failures with logging +- Update slippage UI, adding option for users to set a custom slippage (#25405) +- Added deeplinking to the NFT screen (#25426) +- Updated browser URL bar buttons - back button now shows chevron icon and hides when typing, cancel button always shows text (#25418) + instead of X icon +- Added omni-search to browser URL bar - search tokens, perps, and predictions directly from the browser (#25358) +- Fixed malicious alert modal to require checkbox acknowledgment before enabling the Acknowledge button, and added a Close (#24055) + button for easier dismissal +- Replaced transaction details modal with bottom sheet for improved UX consistency (#25400) + +### Fixed + +- Remove deeplink interstitial on dApp deeplinks (#25963) +- Multiple fixes on import token flow (#25962) +- Fixed decimal precision calculation for Tron's staked balance (#25430) +- Fixed intermittent "Failed to fetch market data" errors on Perps by switching market data fetches from WebSocket to HTTP (#26014) + transport +- Fixed `x-us-env` header being incorrectly set to `false` for US Card users when geolocation requests fail (#25971) +- Fix #24546 with human readable message (#25555) +- Removed "Add funds to start trading perps" banner from Perps market details and allow opening trades (Long/Short) when perps (#25960) + balance is zero. +- Fixed long token names pushing balance off screen in Send flow and MM Pay token picker (#25338) +- Fix #25693 styling issue in for ledger devices (#25758) +- Fixed navigation error and token buyability checks when purchasing crypto with cash using unified buy V2 (#25617) +- Fixed Predictions tab not hiding monetary values when privacy mode is enabled (#25887) +- Fixed Perps deposit+order flow so the pending deposit toast auto-dismisses after a few seconds and the "deposit taking longer" (#25939) + message appears after 30 seconds. +- Fixed header height to scale properly with larger accessibility font sizes (#25855) +- Activity header symbol fallback (#25821) +- Fixed the Perps order pay row not appearing until margin was loaded. (#25836) +- When passoword oudated, it navigate to oauthRehydrate screen when reopen app (#25687) +- Fixed notification and transaction display for EIP-7702 transactions without nonces (#25646) +- Adds event for when token details page is opened. (#25780) +- Added error screens when wallet creation fails, allowing users to retry or contact support instead of being redirected (#25564) + to login. +- Remove toggle switch from login screen (#25424) +- Fixed minor button layout issues (#25771) +- Fixed long account names overflowing in the Deposit Buy screen by enabling proper text truncation (#25715) +- Remove subtitle in token details (#25726) +- Fixed flow for "Cash buy X" button on the new token details layout (#25719) +- Pass assetID to the on ramp buy screen. (#25709) +- Fixes padding in add chain approval bottom sheet (#25671) +- Refactored (#25613) +- N/a (#25642) +- Fix rewards end of season scroll issue (#25639) +- Exclude gas fees from swap quotes insufficientBal calculation (#25637) +- Fixed Perps activity tab sometimes showing empty when accessed from perps home or market detail screens (#25695) +- N/a (#25635) +- Fixed perps tutorial animation alignment by removing empty space in carousel (#25664) +- Prevent mUSD conversion initiation from creating duplicate transactions (#25604) +- Fixed inaccurate fill percentages for historical Perps orders and improved price precision for low-priced assets (#24278) +- Fixes incorrect stop lost banner price (#25556) +- Fixed missing localization for "Change" text on the Buy screen (#25641) +- Do not render keyboard when quote reloads after slippage change (#25633) +- Fixed hardware wallet scan screen layout with centered reader, blurred edges, and improved text positioning (#25290) +- Fixed transaction list not automatically scrolling to show latest transactions after send/swap operations (#25467) +- Fixed order book header price not updating in real-time (#25577) +- Disable swap max button on native assets when stx is disabled (#25023) +- Fixed perps market list search to reset category filters when closing search and enabled sort direction toggle for all (#25465) + sort options +- Fixes an issue preventing insufficient funds error when pressing max balance after inputting non-max balance in swaps (#25513) +- Change Rewards season summary icon colors (#25458) +- Strengthen explore portfolio site condition (#25433) +- Fixed a bug where in the Swaps recipient account picker, if the user clicked on the search input bar, the keyboard would (#25393) + push the search input off screen. + ## [7.65.0] ### Added @@ -10503,7 +10622,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.66.1...HEAD -[7.66.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.65.0...v7.66.1 +[7.66.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.66.0...v7.66.1 +[7.66.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.65.0...v7.66.0 [7.65.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.1...v7.65.0 [7.64.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.0...v7.64.1 [7.64.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.63.1...v7.64.0 From a747f378d026f2c4c8e847091137c50a443d6e31 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:09:28 +0000 Subject: [PATCH 058/131] chore(runway): cherry-pick fix(bridge): cp-7.67.0 display block explorer tx link for Popular networks in transaction details (#26708) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(bridge): cp-7.67.0 display block explorer tx link for Popular networks in transaction details (#26659) ## **Description** `getBlockExplorerForChain()` in `TransactionDetails` only resolved block explorer URLs for hardcoded built-in networks (Mainnet, Linea, Sepolia) and user-added custom RPC networks. Popular networks (Arbitrum, Polygon, BNB Chain, etc.) are neither — they aren't stored in `networkConfigurations` — so the method fell through to `NO_RPC_BLOCK_EXPLORER`, hiding the "View on X" link entirely. The fix adds a `PopularList` lookup as a fallback, matching the pattern already used correctly in useBlockExplorer.ts. ## **Changelog** CHANGELOG entry: Fixed a bug where transactions on popular networks (Arbitrum, Polygon, BNB Chain, etc.) were missing the block explorer link in transaction details ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/26419 ## **Manual testing steps** ```gherkin Feature: Block explorer link in transaction details Scenario: user views a transaction from a Popular network Given the user has transactions on Arbitrum, Polygon, or BNB Chain And the activity feed is filtered by "Popular networks" When user taps a confirmed transaction from one of those networks Then a "View on Arbiscan" / "View on Polygonscan" / "View on Bscscan" link appears And tapping it opens the correct block explorer tx URL in the webview Scenario: user views a transaction on Ethereum Mainnet Given the user has transactions on Ethereum Mainnet When user taps a confirmed transaction Then a "View on Etherscan" link still appears (regression check) ``` ## **Screenshots/Recordings** `~` ### **Before** https://github.com/user-attachments/assets/0b15c664-e62e-4811-94f6-e12687454025 ### **After** https://github.com/user-attachments/assets/8c71420a-f95a-4ebe-ba4c-dedd7cfebc0e ## **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 - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Low risk UI logic change that only affects how the block-explorer URL is resolved for transaction details; risk is limited to incorrect/absent explorer links if the PopularList mapping is wrong. > > **Overview** > Transaction details now falls back to `PopularList` when resolving a block explorer URL, so “View on …” links appear for popular networks (e.g., Arbitrum/Polygon/BNB) that aren’t present in `networkConfigurations`. > > Adds unit tests to verify the correct explorer button text and tx URL for Arbitrum (`arbiscan.io`), Polygon (`polygonscan.com`), and BNB Chain (`bscscan.com`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6b204b35ac2916e07a4860d78b146848eb0d1200. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [e7a93ce](https://github.com/MetaMask/metamask-mobile/commit/e7a93ce880fcda04d877b234af9495edb1f3d722) Co-authored-by: Vince Howard --- .../TransactionDetails/index.js | 11 ++++ .../TransactionDetails/index.test.tsx | 51 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/app/components/UI/TransactionElement/TransactionDetails/index.js b/app/components/UI/TransactionElement/TransactionDetails/index.js index 2cc7e06c18b..34ab522617f 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/index.js +++ b/app/components/UI/TransactionElement/TransactionDetails/index.js @@ -66,6 +66,7 @@ import { } from '../../../../constants/urls'; import { CHAIN_IDS } from '@metamask/transaction-controller'; import TagBase from '../../../../component-library/base-components/TagBase'; +import { PopularList } from '../../../../util/networks/customNetworks'; const createStyles = (colors) => StyleSheet.create({ @@ -199,6 +200,16 @@ class TransactionDetails extends PureComponent { blockExplorer = SEPOLIA_BLOCK_EXPLORER; } + // Check PopularList for additional networks (e.g. Arbitrum, Polygon, BNB) + if (blockExplorer === NO_RPC_BLOCK_EXPLORER) { + const popularNetwork = PopularList.find( + (network) => network.chainId === txChainId, + ); + if (popularNetwork?.rpcPrefs?.blockExplorerUrl) { + blockExplorer = popularNetwork.rpcPrefs.blockExplorerUrl; + } + } + // Check for non-EVM chain block explorer if (isNonEvmChainId(chainId)) { blockExplorer = findBlockExplorerForNonEvmChainId(chainId); diff --git a/app/components/UI/TransactionElement/TransactionDetails/index.test.tsx b/app/components/UI/TransactionElement/TransactionDetails/index.test.tsx index 28337099f34..c60953ddbba 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/index.test.tsx +++ b/app/components/UI/TransactionElement/TransactionDetails/index.test.tsx @@ -348,6 +348,57 @@ describe('TransactionDetails', () => { }); }); + it('should display explorer link for arbitrum (popular network not in networkConfigurations)', () => { + arrangeActAssertBlockExplorerTest({ + overrideMocks: (mocks) => { + mocks.mockState.engine.backgroundState.NetworkController = + mockNetworkState({ + chainId: '0x1', + id: 'mainnet', + nickname: 'Ethereum Mainnet', + ticker: 'ETH', + }); + mocks.mockProps.networkId = '0xa4b1'; + }, + buttonText: 'View on Arbiscan', + expectedUrl: 'https://arbiscan.io/tx/0x3', + }); + }); + + it('should display explorer link for polygon (popular network not in networkConfigurations)', () => { + arrangeActAssertBlockExplorerTest({ + overrideMocks: (mocks) => { + mocks.mockState.engine.backgroundState.NetworkController = + mockNetworkState({ + chainId: '0x1', + id: 'mainnet', + nickname: 'Ethereum Mainnet', + ticker: 'ETH', + }); + mocks.mockProps.networkId = '0x89'; + }, + buttonText: 'View on Polygonscan', + expectedUrl: 'https://polygonscan.com/tx/0x3', + }); + }); + + it('should display explorer link for bnb chain (popular network not in networkConfigurations)', () => { + arrangeActAssertBlockExplorerTest({ + overrideMocks: (mocks) => { + mocks.mockState.engine.backgroundState.NetworkController = + mockNetworkState({ + chainId: '0x1', + id: 'mainnet', + nickname: 'Ethereum Mainnet', + ticker: 'ETH', + }); + mocks.mockProps.networkId = '0x38'; + }, + buttonText: 'View on Bscscan', + expectedUrl: 'https://bscscan.com/tx/0x3', + }); + }); + it('should render `Batched transactions` tag if there are nested transactions', async () => { const { getByText } = renderComponent({ state: initialState, From 45d3e53a1d71dcc5c5d3e4c7d0875a0a02afb99e Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:09:39 +0000 Subject: [PATCH 059/131] chore(runway): cherry-pick fix(bridge): cp-7.67.0 fix "View on block explorer" button for swap and bridge transactions (#26709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(bridge): cp-7.67.0 fix "View on block explorer" button for swap and bridge transactions (#26701) ## **Description** The "View on block explorer" button on the Bridge/Swap Transaction Details screen was broken for both swaps and bridge transactions — pressing it did nothing. Root causes: - Swap transactions: `navigation.navigate(Routes.BROWSER.VIEW, ...)` was called directly, but `BROWSER.VIEW` is a nested screen inside the `BROWSER.HOME` tab navigator. React Navigation silently ignores navigation calls to nested screens made from outside their parent. Additionally, navigating to a tab navigator replaces the current stack, so the back button would return to the home screen instead of activities. - Bridge transactions: `navigation.navigate(Routes.BRIDGE.MODALS.TRANSACTION_DETAILS_BLOCK_EXPLORER, ...)` was called directly, but this screen is nested inside `BridgeModalStack` (registered as `Routes.BRIDGE.MODALS.ROOT`). Same silent failure. Fix: - Swap path now uses `Routes.WEBVIEW.MAIN` → `Routes.WEBVIEW.SIMPLE`, which pushes a `WebView` on top of the current stack (back button returns to Transaction Details correctly) - Bridge path now navigates to `Routes.BRIDGE.MODALS.ROOT` with screen: `TRANSACTION_DETAILS_BLOCK_EXPLORER` as a nested param - Tightened the condition from a bare else to else if (isBridge) to prevent a swap with an unresolved explorer URL from accidentally opening the bridge modal ## **Changelog** CHANGELOG entry: Fixed "View on block explorer" button not working on Bridge/Swap Transaction Details screen ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/26628 ## **Manual testing steps** ```gherkin Feature: View on block explorer from Transaction Details Scenario: user views a completed swap on block explorer Given user has a completed swap transaction (same-chain) When user taps the transaction in Activity to open Transaction Details And user taps "View on block explorer" Then a WebView opens showing the block explorer for that transaction And tapping back returns the user to Transaction Details Scenario: user views a completed bridge on block explorer Given user has a completed bridge transaction (cross-chain) When user taps the transaction in Activity to open Transaction Details And user taps "View on block explorer" Then a bottom sheet appears with source and destination chain explorer options When user taps one of the explorer options Then a WebView opens showing that chain's block explorer for the transaction ``` ## **Screenshots/Recordings** `~` ### **Before** https://github.com/user-attachments/assets/9030e42a-32a5-4661-bc00-d4953ae17850 ### **After** https://github.com/user-attachments/assets/26da74e6-353b-4df9-9be2-7f45f00105fe ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Medium risk because it changes React Navigation targets/params for transaction-details flows; mistakes here could break navigation/back behavior, but the change is localized and covered by updated unit tests. > > **Overview** > Fixes the Bridge/Swap Transaction Details "View on block explorer" flow by switching swap explorer navigation to `Routes.WEBVIEW.MAIN` → `Routes.WEBVIEW.SIMPLE` and routing bridge explorer access through the nested modal stack (`Routes.BRIDGE.MODALS.ROOT` → `TRANSACTION_DETAILS_BLOCK_EXPLORER`). > > Updates `BlockExplorersModal` to open explorer links in the WebView (instead of `Routes.BROWSER.VIEW`) and adds/adjusts tests to assert correct navigation for swap (direct WebView) vs bridge (modal stack, then WebView) paths, including an `else if (isBridge)` guard to avoid opening the modal when a swap explorer URL is missing. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 20ebe378e2f8dae5198fb6d401814966465c2620. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [d7b7f79](https://github.com/MetaMask/metamask-mobile/commit/d7b7f792fa447489d6d9849127895eee06cfcddb) Co-authored-by: Vince Howard --- .../BlockExplorersModal.test.tsx | 57 +++++++++++++++++++ .../BlockExplorersModal.tsx | 16 ++++-- .../TransactionDetails.test.tsx | 37 ++++++++++-- .../TransactionDetails/TransactionDetails.tsx | 19 ++++--- 4 files changed, 109 insertions(+), 20 deletions(-) diff --git a/app/components/UI/Bridge/components/TransactionDetails/BlockExplorersModal.test.tsx b/app/components/UI/Bridge/components/TransactionDetails/BlockExplorersModal.test.tsx index 310059d7cb6..104ba6b479c 100644 --- a/app/components/UI/Bridge/components/TransactionDetails/BlockExplorersModal.test.tsx +++ b/app/components/UI/Bridge/components/TransactionDetails/BlockExplorersModal.test.tsx @@ -9,8 +9,25 @@ import { BridgeState } from '../../../../../core/redux/slices/bridge'; import { renderScreen } from '../../../../../util/test/renderWithProvider'; import { initialState } from '../../_mocks_/initialState'; import BlockExplorersModal from './BlockExplorersModal'; +import { fireEvent } from '@testing-library/react-native'; + +const mockNavigate = jest.fn(); +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useNavigation: () => ({ + navigate: mockNavigate, + setOptions: jest.fn(), + }), + }; +}); describe('BlockExplorersModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const mockTx = { id: 'test-tx-id', chainId: '0x1', @@ -115,4 +132,44 @@ describe('BlockExplorersModal', () => { const etherscanButtons = getAllByText('Etherscan'); expect(etherscanButtons).toHaveLength(1); }); + + it('should navigate to webview when source chain explorer button is pressed', () => { + const { getAllByText } = renderScreen( + () => , + { + name: Routes.BRIDGE.MODALS.TRANSACTION_DETAILS_BLOCK_EXPLORER, + }, + { state: mockState }, + ); + + const [srcExplorerButton] = getAllByText('Etherscan'); + fireEvent.press(srcExplorerButton); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.WEBVIEW.MAIN, { + screen: Routes.WEBVIEW.SIMPLE, + params: expect.objectContaining({ + url: expect.stringContaining('etherscan.io'), + }), + }); + }); + + it('should navigate to webview when destination chain explorer button is pressed', () => { + const { getByText } = renderScreen( + () => , + { + name: Routes.BRIDGE.MODALS.TRANSACTION_DETAILS_BLOCK_EXPLORER, + }, + { state: mockState }, + ); + + const destExplorerButton = getByText('Optimistic'); + fireEvent.press(destExplorerButton); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.WEBVIEW.MAIN, { + screen: Routes.WEBVIEW.SIMPLE, + params: expect.objectContaining({ + url: expect.stringContaining('optimistic.etherscan.io'), + }), + }); + }); }); diff --git a/app/components/UI/Bridge/components/TransactionDetails/BlockExplorersModal.tsx b/app/components/UI/Bridge/components/TransactionDetails/BlockExplorersModal.tsx index eb79a0cba8c..27533b1d71d 100644 --- a/app/components/UI/Bridge/components/TransactionDetails/BlockExplorersModal.tsx +++ b/app/components/UI/Bridge/components/TransactionDetails/BlockExplorersModal.tsx @@ -110,9 +110,11 @@ const BlockExplorersModal = (props: BlockExplorersModalProps) => { } onPress={() => { - navigation.navigate(Routes.BROWSER.VIEW, { - newTabUrl: srcExplorerData.explorerTxUrl, - timestamp: Date.now(), + navigation.navigate(Routes.WEBVIEW.MAIN, { + screen: Routes.WEBVIEW.SIMPLE, + params: { + url: srcExplorerData.explorerTxUrl, + }, }); }} /> @@ -139,9 +141,11 @@ const BlockExplorersModal = (props: BlockExplorersModalProps) => { } onPress={() => { - navigation.navigate(Routes.BROWSER.VIEW, { - newTabUrl: bridgeDestExplorerData.explorerTxUrl, - timestamp: Date.now(), + navigation.navigate(Routes.WEBVIEW.MAIN, { + screen: Routes.WEBVIEW.SIMPLE, + params: { + url: bridgeDestExplorerData.explorerTxUrl, + }, }); }} /> diff --git a/app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.test.tsx b/app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.test.tsx index 96bba86e3ad..a37eee9bcb1 100644 --- a/app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.test.tsx +++ b/app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.test.tsx @@ -143,6 +143,31 @@ describe('BridgeTransactionDetails', () => { expect(getByText(/view on block explorer/i)).toBeTruthy(); }); + it('navigates to block explorer modal for cross-chain bridge with evmTxMeta', () => { + const { getByText } = renderScreen( + () => ( + + ), + { + name: Routes.BRIDGE.BRIDGE_TRANSACTION_DETAILS, + }, + { state: mockState }, + ); + + const blockExplorerButton = getByText(/view on block explorer/i); + fireEvent.press(blockExplorerButton); + + // Should navigate to bridge modal stack, not directly to webview + expect(mockNavigate).toHaveBeenCalledWith(Routes.BRIDGE.MODALS.ROOT, { + screen: Routes.BRIDGE.MODALS.TRANSACTION_DETAILS_BLOCK_EXPLORER, + params: expect.objectContaining({ + evmTxMeta: mockEVMTx, + }), + }); + }); + it('navigates directly to browser for same-chain swaps with multiChainTx', () => { const { getByText } = renderScreen( () => ( @@ -159,12 +184,12 @@ describe('BridgeTransactionDetails', () => { const blockExplorerButton = getByText(/view on block explorer/i); fireEvent.press(blockExplorerButton); - // Should navigate to browser, not to the modal - expect(mockNavigate).toHaveBeenCalledWith( - Routes.BROWSER.VIEW, - expect.objectContaining({ - newTabUrl: expect.stringContaining('solana-tx-hash-123'), + // Should navigate to webview, not to the modal + expect(mockNavigate).toHaveBeenCalledWith(Routes.WEBVIEW.MAIN, { + screen: Routes.WEBVIEW.SIMPLE, + params: expect.objectContaining({ + url: expect.stringContaining('solana-tx-hash-123'), }), - ); + }); }); }); diff --git a/app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.tsx b/app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.tsx index 8752c8b465e..33d8799415b 100644 --- a/app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.tsx +++ b/app/components/UI/Bridge/components/TransactionDetails/TransactionDetails.tsx @@ -395,19 +395,22 @@ export const BridgeTransactionDetails = ( onPress={() => { // For swaps, go directly to block explorer web view if (isSwap && swapSrcExplorerData?.explorerTxUrl) { - navigation.navigate(Routes.BROWSER.VIEW, { - newTabUrl: swapSrcExplorerData.explorerTxUrl, - timestamp: Date.now(), + navigation.navigate(Routes.WEBVIEW.MAIN, { + screen: Routes.WEBVIEW.SIMPLE, + params: { + url: swapSrcExplorerData.explorerTxUrl, + }, }); - } else { + } else if (isBridge) { // For bridges, show the modal with both explorers - navigation.navigate( - Routes.BRIDGE.MODALS.TRANSACTION_DETAILS_BLOCK_EXPLORER, - { + navigation.navigate(Routes.BRIDGE.MODALS.ROOT, { + screen: + Routes.BRIDGE.MODALS.TRANSACTION_DETAILS_BLOCK_EXPLORER, + params: { evmTxMeta: props.route.params.evmTxMeta, multiChainTx: props.route.params.multiChainTx, }, - ); + }); } }} /> From f3fc7965cd81a583ea5a38d1eeef9afbb898b9b9 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Fri, 27 Feb 2026 20:11:05 +0000 Subject: [PATCH 060/131] [skip ci] Bump version number to 3840 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index c84b3bd7b3f..a39069bb1fa 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.67.0" - versionCode 3823 + versionCode 3840 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 1e3bcda7097..5277712b999 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3823 + VERSION_NUMBER: 3840 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3823 + FLASK_VERSION_NUMBER: 3840 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index ecbeaeda82c..5113181a447 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3823; + CURRENT_PROJECT_VERSION = 3840; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3823; + CURRENT_PROJECT_VERSION = 3840; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3823; + CURRENT_PROJECT_VERSION = 3840; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3823; + CURRENT_PROJECT_VERSION = 3840; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3823; + CURRENT_PROJECT_VERSION = 3840; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3823; + CURRENT_PROJECT_VERSION = 3840; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 6bd436b246ce47da0107378e75b3eb8b37c26bb4 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Fri, 27 Feb 2026 20:15:07 +0000 Subject: [PATCH 061/131] [skip ci] Bump version number to 3842 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index a39069bb1fa..aa286c4a27f 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.67.0" - versionCode 3840 + versionCode 3842 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 5277712b999..77a2900ec2c 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3840 + VERSION_NUMBER: 3842 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3840 + FLASK_VERSION_NUMBER: 3842 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 5113181a447..07e762cf190 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3840; + CURRENT_PROJECT_VERSION = 3842; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3840; + CURRENT_PROJECT_VERSION = 3842; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3840; + CURRENT_PROJECT_VERSION = 3842; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3840; + CURRENT_PROJECT_VERSION = 3842; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3840; + CURRENT_PROJECT_VERSION = 3842; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3840; + CURRENT_PROJECT_VERSION = 3842; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From c34449ecbc0235ae9bd32d7df38f83e6cbac8857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:54:25 +0000 Subject: [PATCH 062/131] Revert "feat(card): integrate Veriff native SDK with MetaMask branding for KYC verification (#26138)" This reverts commit 69538b5b1db415f5b7a3eaa0dfb0ed0f54f507f8. --- android/app/build.gradle | 37 --- android/build.gradle | 1 - app/components/UI/Card/Card.types.ts | 6 + .../components/Onboarding/KYCWebview.test.tsx | 228 ++++++++++++++++++ .../Card/components/Onboarding/KYCWebview.tsx | 53 ++++ .../Onboarding/VerifyIdentity.test.tsx | 142 ++--------- .../components/Onboarding/VerifyIdentity.tsx | 82 +------ .../Card/routes/OnboardingNavigator.test.tsx | 2 + .../UI/Card/routes/OnboardingNavigator.tsx | 6 + app/constants/navigation/Routes.ts | 1 + app/core/NavigationService/types.ts | 6 +- app/declarations/index.d.ts | 48 ---- .../fox.imageset/Contents.json | 23 -- .../Images.xcassets/fox.imageset/fox@1x.png | Bin 4295 -> 0 bytes .../Images.xcassets/fox.imageset/fox@2x.png | Bin 7894 -> 0 bytes .../Images.xcassets/fox.imageset/fox@3x.png | Bin 11482 -> 0 bytes ios/Podfile.lock | 20 +- jest.config.js | 2 +- package.json | 1 - yarn.lock | 11 - 20 files changed, 342 insertions(+), 327 deletions(-) create mode 100644 app/components/UI/Card/components/Onboarding/KYCWebview.test.tsx create mode 100644 app/components/UI/Card/components/Onboarding/KYCWebview.tsx delete mode 100644 ios/MetaMask/Images.xcassets/fox.imageset/Contents.json delete mode 100644 ios/MetaMask/Images.xcassets/fox.imageset/fox@1x.png delete mode 100644 ios/MetaMask/Images.xcassets/fox.imageset/fox@2x.png delete mode 100644 ios/MetaMask/Images.xcassets/fox.imageset/fox@3x.png diff --git a/android/app/build.gradle b/android/app/build.gradle index aa286c4a27f..1088502c8d5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -197,7 +197,6 @@ android { packagingOptions { exclude 'META-INF/DEPENDENCIES' - exclude 'META-INF/versions/9/OSGI-INF/MANIFEST.MF' pickFirst 'lib/x86/libc++_shared.so' pickFirst 'lib/x86_64/libc++_shared.so' pickFirst 'lib/armeabi-v7a/libc++_shared.so' @@ -368,42 +367,6 @@ android { } } -// Veriff SDK uses BouncyCastle library which conflicts with the BouncyCastle library used by other libraries. -// This resolves modules by substituting the Veriff SDK's BouncyCastle library with the correct one. -configurations.all { - resolutionStrategy.dependencySubstitution { - substitute module('org.bouncycastle:bcprov-jdk15to18') using module('org.bouncycastle:bcprov-jdk18on:1.78.1') - substitute module('org.bouncycastle:bcutil-jdk15to18') using module('org.bouncycastle:bcutil-jdk18on:1.78.1') - } -} - -// Veriff SDK sub-libraries declare minSdkVersion 26; we patch their manifests to use our minSdkVersion -// Fails the build if Veriff is not found. -def VERIFF_SDK_VERSION = '7.14.1' -project.afterEvaluate { - tasks.matching { it.name ==~ /process.*MainManifest/ }.configureEach { task -> - task.doFirst { - def transformsDir = new File(gradle.gradleUserHomeDir, "caches/${gradle.gradleVersion}/transforms") - def veriffManifests = transformsDir.exists() - ? fileTree(dir: transformsDir, includes: ["**/jetified-*-${VERIFF_SDK_VERSION}/AndroidManifest.xml"]).files - : [] - if (veriffManifests.isEmpty()) { - throw new GradleException( - "Veriff SDK (${VERIFF_SDK_VERSION}) manifests not found in Gradle transform cache. " + - "Ensure the Veriff dependency is resolved (e.g. run a clean build). " + - "Searched in: ${transformsDir}" - ) - } - def targetMinSdk = rootProject.ext.minSdkVersion - veriffManifests.each { file -> - def content = file.text - if (content.contains('android:minSdkVersion="26"')) { - file.text = content.replace('android:minSdkVersion="26"', "android:minSdkVersion=\"${targetMinSdk}\"") - } - } - } - } -} dependencies { // The version of react-native is set by the React Native Gradle Plugin diff --git a/android/build.gradle b/android/build.gradle index 01a8c08d81d..57dc4ba770e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -35,7 +35,6 @@ buildscript { url(new File(['node', '--print', "require.resolve('@notifee/react-native/package.json')"].execute(null, rootDir).text.trim(), '../android/libs')) } maven { url "https://jitpack.io" } - maven { url "https://cdn.veriff.me/android/" } } } } diff --git a/app/components/UI/Card/Card.types.ts b/app/components/UI/Card/Card.types.ts index 806213f35ff..16478734773 100644 --- a/app/components/UI/Card/Card.types.ts +++ b/app/components/UI/Card/Card.types.ts @@ -2,6 +2,12 @@ * Card navigation parameters */ +/** Card onboarding webview parameters */ +export interface CardOnboardingWebviewParams { + url?: string; + title?: string; +} + /** Card confirm modal parameters */ export interface CardConfirmModalParams { title?: string; diff --git a/app/components/UI/Card/components/Onboarding/KYCWebview.test.tsx b/app/components/UI/Card/components/Onboarding/KYCWebview.test.tsx new file mode 100644 index 00000000000..b3fadb8e426 --- /dev/null +++ b/app/components/UI/Card/components/Onboarding/KYCWebview.test.tsx @@ -0,0 +1,228 @@ +// Mock dependencies first +jest.mock('@react-navigation/native', () => ({ + useNavigation: jest.fn(), + useRoute: jest.fn(), +})); + +jest.mock('../../../../../util/navigation/navUtils', () => ({ + useParams: jest.fn(), +})); + +jest.mock('@metamask/design-system-twrnc-preset', () => ({ + useTailwind: jest.fn(), +})); + +jest.mock('@metamask/react-native-webview', () => { + const React = jest.requireActual('react'); + const { View } = jest.requireActual('react-native'); + + return { + WebView: ({ + testID, + containerStyle, + source, + ...props + }: { + testID?: string; + containerStyle?: unknown; + source?: { uri: string }; + [key: string]: unknown; + }) => + React.createElement(View, { + testID, + style: containerStyle, + 'data-source': source?.uri, + ...props, + }), + }; +}); + +import React from 'react'; +import { render } from '@testing-library/react-native'; +import { useParams } from '../../../../../util/navigation/navUtils'; +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import KYCWebview from './KYCWebview'; + +describe('KYCWebview', () => { + let mockUseParams: jest.Mock; + let mockUseTailwind: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock useTailwind + mockUseTailwind = jest.fn().mockReturnValue({ + style: jest.fn((styles: string) => ({ testStyle: styles })), + }); + (useTailwind as jest.Mock).mockImplementation(mockUseTailwind); + + // Default mock for useParams + mockUseParams = jest.fn().mockReturnValue({ + url: 'https://example.com/kyc', + }); + (useParams as jest.Mock).mockImplementation(mockUseParams); + }); + + describe('render', () => { + it('renders WebView with correct testID', () => { + const { getByTestId } = render(); + + const webView = getByTestId('kyc-webview'); + expect(webView).toBeTruthy(); + }); + + it('renders WebView with correct URL from params', () => { + const testUrl = 'https://test-kyc-url.com/verification'; + mockUseParams.mockReturnValue({ + url: testUrl, + }); + + const { getByTestId } = render(); + + const webView = getByTestId('kyc-webview'); + expect(webView.props['data-source']).toBe(testUrl); + }); + + it('applies correct container style from Tailwind', () => { + const mockStyle = { flex: 1, backgroundColor: 'white' }; + mockUseTailwind.mockReturnValue({ + style: jest.fn(() => mockStyle), + }); + + const { getByTestId } = render(); + + const webView = getByTestId('kyc-webview'); + expect(webView.props.style).toEqual(mockStyle); + }); + }); + + describe('when URL parameter is provided', () => { + it('passes the URL to WebView source', () => { + const kycUrl = 'https://kyc-provider.com/session/12345'; + mockUseParams.mockReturnValue({ + url: kycUrl, + }); + + const { getByTestId } = render(); + + const webView = getByTestId('kyc-webview'); + expect(webView.props['data-source']).toBe(kycUrl); + }); + + it('handles HTTPS URLs correctly', () => { + const httpsUrl = 'https://secure-kyc.example.com/verify'; + mockUseParams.mockReturnValue({ + url: httpsUrl, + }); + + const { getByTestId } = render(); + + const webView = getByTestId('kyc-webview'); + expect(webView.props['data-source']).toBe(httpsUrl); + }); + + it('handles URLs with query parameters', () => { + const urlWithParams = + 'https://kyc.example.com/verify?session=abc123&redirect=true'; + mockUseParams.mockReturnValue({ + url: urlWithParams, + }); + + const { getByTestId } = render(); + + const webView = getByTestId('kyc-webview'); + expect(webView.props['data-source']).toBe(urlWithParams); + }); + }); + + describe('when URL parameter is missing', () => { + it('handles undefined URL gracefully', () => { + mockUseParams.mockReturnValue({ + url: undefined, + }); + + const { getByTestId } = render(); + + const webView = getByTestId('kyc-webview'); + expect(webView.props['data-source']).toBeUndefined(); + }); + + it('handles empty URL string', () => { + mockUseParams.mockReturnValue({ + url: '', + }); + + const { getByTestId } = render(); + + const webView = getByTestId('kyc-webview'); + expect(webView.props['data-source']).toBe(''); + }); + + it('handles null URL', () => { + mockUseParams.mockReturnValue({ + url: null, + }); + + const { getByTestId } = render(); + + const webView = getByTestId('kyc-webview'); + expect(webView.props['data-source']).toBeNull(); + }); + }); + + describe('when useParams returns no data', () => { + it('handles empty params object', () => { + mockUseParams.mockReturnValue({}); + + const { getByTestId } = render(); + + const webView = getByTestId('kyc-webview'); + expect(webView.props['data-source']).toBeUndefined(); + }); + + it('handles null params', () => { + mockUseParams.mockReturnValue(null); + + // This will throw because the component tries to destructure url from null + expect(() => render()).toThrow(); + }); + }); + + describe('Tailwind integration', () => { + it('calls useTailwind hook', () => { + render(); + + expect(mockUseTailwind).toHaveBeenCalledTimes(1); + }); + + it('applies flex-1 style to WebView container', () => { + const mockTailwindInstance = { + style: jest.fn((className: string) => { + if (className === 'flex-1') { + return { flex: 1 }; + } + return {}; + }), + }; + mockUseTailwind.mockReturnValue(mockTailwindInstance); + + render(); + + expect(mockTailwindInstance.style).toHaveBeenCalledWith('flex-1'); + }); + }); + + describe('component structure', () => { + it('renders only WebView component', () => { + const { getByTestId, queryByTestId } = render(); + + // Should have the WebView + expect(getByTestId('kyc-webview')).toBeTruthy(); + + // Should not have any other test IDs (no additional UI elements) + expect(queryByTestId('loading-indicator')).toBeNull(); + expect(queryByTestId('error-message')).toBeNull(); + expect(queryByTestId('header')).toBeNull(); + }); + }); +}); diff --git a/app/components/UI/Card/components/Onboarding/KYCWebview.tsx b/app/components/UI/Card/components/Onboarding/KYCWebview.tsx new file mode 100644 index 00000000000..210542c6f68 --- /dev/null +++ b/app/components/UI/Card/components/Onboarding/KYCWebview.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import { WebView, WebViewNavigation } from '@metamask/react-native-webview'; +import { useParams } from '../../../../../util/navigation/navUtils'; +import { useNavigation, StackActions } from '@react-navigation/native'; +import Routes from '../../../../../constants/navigation/Routes'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +interface KYCWebviewProps { + url: string; +} + +const KYCWebview: React.FC = () => { + const navigation = useNavigation(); + const { url } = useParams(); + const tw = useTailwind(); + + const handleNavigationStateChange = (navState: WebViewNavigation) => { + if ( + navState.url?.includes('www.veriff.com/get-verified?navigation=slim') || + navState.title?.includes( + 'Get Verified | Personal Data Protection Matters to Us - Veriff', + ) + ) { + navigation.dispatch( + StackActions.replace(Routes.CARD.ONBOARDING.VERIFYING_VERIFF_KYC), + ); + } + }; + + return ( + + + + ); +}; + +export default KYCWebview; diff --git a/app/components/UI/Card/components/Onboarding/VerifyIdentity.test.tsx b/app/components/UI/Card/components/Onboarding/VerifyIdentity.test.tsx index 1171a33c295..0958c9685d8 100644 --- a/app/components/UI/Card/components/Onboarding/VerifyIdentity.test.tsx +++ b/app/components/UI/Card/components/Onboarding/VerifyIdentity.test.tsx @@ -1,10 +1,8 @@ -/* eslint-disable @typescript-eslint/no-shadow */ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react-native'; import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; -import { useNavigation, StackActions } from '@react-navigation/native'; -import VeriffSdk from '@veriff/react-native-sdk'; +import { useNavigation } from '@react-navigation/native'; import VerifyIdentity from './VerifyIdentity'; import Routes from '../../../../../constants/navigation/Routes'; import useStartVerification from '../../hooks/useStartVerification'; @@ -12,32 +10,6 @@ import useStartVerification from '../../hooks/useStartVerification'; // Mock dependencies jest.mock('@react-navigation/native', () => ({ useNavigation: jest.fn(), - StackActions: { - replace: jest.fn((route: string) => ({ - type: 'REPLACE', - payload: { name: route }, - })), - }, -})); - -// Mock Veriff SDK -jest.mock('@veriff/react-native-sdk', () => ({ - __esModule: true, - default: { - launchVeriff: jest.fn(), - statusDone: 'done', - statusCanceled: 'canceled', - statusError: 'error', - }, -})); - -// Mock Logger -jest.mock('../../../../../util/Logger', () => ({ - __esModule: true, - default: { - error: jest.fn(), - log: jest.fn(), - }, })); // Mock useStartVerification hook @@ -256,7 +228,6 @@ const createTestStore = (initialState = {}) => describe('VerifyIdentity Component', () => { const mockNavigate = jest.fn(); - const mockDispatch = jest.fn(); let store: ReturnType; beforeEach(() => { @@ -266,7 +237,6 @@ describe('VerifyIdentity Component', () => { (useNavigation as jest.Mock).mockReturnValue({ navigate: mockNavigate, - dispatch: mockDispatch, }); (useStartVerification as jest.Mock).mockReturnValue({ @@ -276,10 +246,6 @@ describe('VerifyIdentity Component', () => { error: null, }); - (VeriffSdk.launchVeriff as jest.Mock).mockResolvedValue({ - status: VeriffSdk.statusDone, - }); - store = createTestStore(); }); @@ -438,47 +404,8 @@ describe('VerifyIdentity Component', () => { }); }); - describe('Button Interaction and Veriff SDK', () => { - it('launches Veriff SDK with theme-aware branding when continue button is pressed', async () => { - const { getByTestId } = render( - - - , - ); - - const button = getByTestId('verify-identity-continue-button'); - fireEvent.press(button); - - await waitFor(() => { - expect(VeriffSdk.launchVeriff).toHaveBeenCalledWith({ - sessionUrl: 'https://example.com/verify', - branding: expect.objectContaining({ - logo: 'fox', - background: expect.any(String), - primary: expect.any(String), - onPrimary: expect.any(String), - onBackground: expect.any(String), - onBackgroundSecondary: expect.any(String), - onBackgroundTertiary: expect.any(String), - secondary: expect.any(String), - onSecondary: expect.any(String), - outline: expect.any(String), - error: expect.any(String), - success: expect.any(String), - buttonRadius: 12, - // Camera overlay colors are static — always dark + white text - cameraOverlay: '#121314', - onCameraOverlay: '#ffffff', - }), - }); - }); - }); - - it('navigates to VERIFYING_VERIFF_KYC when Veriff status is done', async () => { - (VeriffSdk.launchVeriff as jest.Mock).mockResolvedValue({ - status: VeriffSdk.statusDone, - }); - + describe('Button Interaction and Navigation', () => { + it('navigates to WebView when continue button is pressed', async () => { const { getByTestId } = render( @@ -489,16 +416,21 @@ describe('VerifyIdentity Component', () => { fireEvent.press(button); await waitFor(() => { - expect(StackActions.replace).toHaveBeenCalledWith( - Routes.CARD.ONBOARDING.VERIFYING_VERIFF_KYC, + expect(mockNavigate).toHaveBeenCalledWith( + Routes.CARD.ONBOARDING.WEBVIEW, + { + url: 'https://example.com/verify', + }, ); - expect(mockDispatch).toHaveBeenCalled(); }); }); - it('stays on screen when Veriff status is canceled', async () => { - (VeriffSdk.launchVeriff as jest.Mock).mockResolvedValue({ - status: VeriffSdk.statusCanceled, + it('does not navigate when continue button is pressed without sessionUrl', async () => { + (useStartVerification as jest.Mock).mockReturnValue({ + data: null, + isLoading: false, + isError: false, + error: null, }); const { getByTestId } = render( @@ -511,18 +443,11 @@ describe('VerifyIdentity Component', () => { fireEvent.press(button); await waitFor(() => { - expect(VeriffSdk.launchVeriff).toHaveBeenCalled(); - expect(mockDispatch).not.toHaveBeenCalled(); + expect(mockNavigate).not.toHaveBeenCalled(); }); }); - it('logs error and stays on screen when Veriff status is error', async () => { - const Logger = jest.requireMock('../../../../../util/Logger').default; - (VeriffSdk.launchVeriff as jest.Mock).mockResolvedValue({ - status: VeriffSdk.statusError, - error: 'CAMERA_UNAVAILABLE', - }); - + it('handles multiple button presses', async () => { const { getByTestId } = render( @@ -530,36 +455,19 @@ describe('VerifyIdentity Component', () => { ); const button = getByTestId('verify-identity-continue-button'); - fireEvent.press(button); - - await waitFor(() => { - expect(Logger.error).toHaveBeenCalledWith( - expect.any(Error), - 'Veriff verification failed with error=CAMERA_UNAVAILABLE', - ); - expect(mockDispatch).not.toHaveBeenCalled(); - }); - }); - - it('does not launch Veriff SDK when continue button is pressed without sessionUrl', async () => { - (useStartVerification as jest.Mock).mockReturnValue({ - data: null, - isLoading: false, - isError: false, - error: null, - }); - - const { getByTestId } = render( - - - , - ); - const button = getByTestId('verify-identity-continue-button'); + fireEvent.press(button); + fireEvent.press(button); fireEvent.press(button); await waitFor(() => { - expect(VeriffSdk.launchVeriff).not.toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledTimes(3); + expect(mockNavigate).toHaveBeenCalledWith( + Routes.CARD.ONBOARDING.WEBVIEW, + { + url: 'https://example.com/verify', + }, + ); }); }); }); diff --git a/app/components/UI/Card/components/Onboarding/VerifyIdentity.tsx b/app/components/UI/Card/components/Onboarding/VerifyIdentity.tsx index 4280f510de8..986b4a1b9a5 100644 --- a/app/components/UI/Card/components/Onboarding/VerifyIdentity.tsx +++ b/app/components/UI/Card/components/Onboarding/VerifyIdentity.tsx @@ -1,8 +1,7 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Image } from 'react-native'; -import { useNavigation, StackActions } from '@react-navigation/native'; +import { useNavigation } from '@react-navigation/native'; import { useSelector } from 'react-redux'; -import VeriffSdk, { type VeriffBranding } from '@veriff/react-native-sdk'; import OnboardingStep from './OnboardingStep'; import { strings } from '../../../../../../locales/i18n'; import { @@ -26,48 +25,12 @@ import { CardActions, CardScreens } from '../../util/metrics'; import MM_CARD_VERIFY_IDENTITY from '../../../../../images/card-fingerprint-kyc-image.png'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; import { selectSelectedCountry } from '../../../../../core/redux/slices/card'; -import Logger from '../../../../../util/Logger'; -import { useTheme } from '../../../../../util/theme'; -import { brandColor } from '@metamask/design-tokens'; const VerifyIdentity = () => { const navigation = useNavigation(); const tw = useTailwind(); - const { colors } = useTheme(); const { trackEvent, createEventBuilder } = useMetrics(); const selectedCountry = useSelector(selectSelectedCountry); - const [isLaunchingVeriff, setIsLaunchingVeriff] = useState(false); - - const veriffBranding: VeriffBranding = useMemo( - () => ({ - logo: 'fox', - background: colors.background.default, - onBackground: colors.text.default, - onBackgroundSecondary: colors.text.alternative, - onBackgroundTertiary: colors.text.muted, - primary: colors.icon.default, - onPrimary: colors.icon.inverse, - secondary: colors.primary.default, - onSecondary: colors.primary.inverse, - outline: colors.border.default, - cameraOverlay: brandColor.grey900, - onCameraOverlay: brandColor.grey000, - error: colors.error.default, - success: colors.success.default, - buttonRadius: 12, - iOSFont: { - regular: 'Geist-Regular', - medium: 'Geist-Medium', - bold: 'Geist-Bold', - }, - androidFont: { - regular: 'Geist-Regular', - medium: 'Geist-Medium', - bold: 'Geist-Bold', - }, - }), - [colors], - ); const { data: verificationResponse, isLoading: startVerificationIsLoading, @@ -78,6 +41,12 @@ const VerifyIdentity = () => { const { sessionUrl } = verificationResponse || {}; const handleContinue = useCallback(async () => { + if (sessionUrl) { + navigation.navigate(Routes.CARD.ONBOARDING.WEBVIEW, { + url: sessionUrl, + }); + } + trackEvent( createEventBuilder(MetaMetricsEvents.CARD_BUTTON_CLICKED) .addProperties({ @@ -85,37 +54,7 @@ const VerifyIdentity = () => { }) .build(), ); - - if (sessionUrl) { - setIsLaunchingVeriff(true); - try { - const result = await VeriffSdk.launchVeriff({ - sessionUrl, - branding: veriffBranding, - }); - - switch (result.status) { - case VeriffSdk.statusDone: - navigation.dispatch( - StackActions.replace(Routes.CARD.ONBOARDING.VERIFYING_VERIFF_KYC), - ); - break; - case VeriffSdk.statusCanceled: - break; - case VeriffSdk.statusError: - Logger.error( - new Error('Veriff verification failed'), - `Veriff verification failed with error=${result.error}`, - ); - break; - } - } catch (error) { - Logger.error(error as Error, 'Veriff SDK launch failed unexpectedly'); - } finally { - setIsLaunchingVeriff(false); - } - } - }, [navigation, sessionUrl, trackEvent, createEventBuilder, veriffBranding]); + }, [navigation, sessionUrl, trackEvent, createEventBuilder]); useEffect(() => { trackEvent( @@ -196,8 +135,7 @@ const VerifyIdentity = () => { size={ButtonSize.Lg} onPress={handleContinue} width={ButtonWidthTypes.Full} - isDisabled={!sessionUrl || isLaunchingVeriff} - loading={isLaunchingVeriff} + isDisabled={!sessionUrl} testID="verify-identity-continue-button" /> ); diff --git a/app/components/UI/Card/routes/OnboardingNavigator.test.tsx b/app/components/UI/Card/routes/OnboardingNavigator.test.tsx index da89064d4fb..4f63efe1ce2 100644 --- a/app/components/UI/Card/routes/OnboardingNavigator.test.tsx +++ b/app/components/UI/Card/routes/OnboardingNavigator.test.tsx @@ -120,6 +120,7 @@ jest.mock('../components/Onboarding/KYCPending', () => 'KYCPending'); jest.mock('../components/Onboarding/PersonalDetails', () => 'PersonalDetails'); jest.mock('../components/Onboarding/PhysicalAddress', () => 'PhysicalAddress'); jest.mock('../components/Onboarding/Complete', () => 'Complete'); +jest.mock('../components/Onboarding/KYCWebview', () => 'KYCWebview'); // Mock navigation options jest.mock('.', () => ({ @@ -186,6 +187,7 @@ jest.mock('../../../../constants/navigation/Routes', () => ({ PERSONAL_DETAILS: 'PERSONAL_DETAILS', PHYSICAL_ADDRESS: 'PHYSICAL_ADDRESS', COMPLETE: 'COMPLETE', + WEBVIEW: 'WEBVIEW', }, MODALS: { ID: 'CARD_MODALS', diff --git a/app/components/UI/Card/routes/OnboardingNavigator.tsx b/app/components/UI/Card/routes/OnboardingNavigator.tsx index 50484a356b8..20e8ea17e03 100644 --- a/app/components/UI/Card/routes/OnboardingNavigator.tsx +++ b/app/components/UI/Card/routes/OnboardingNavigator.tsx @@ -22,6 +22,7 @@ import ButtonIcon, { ButtonIconSizes, } from '../../../../component-library/components/Buttons/ButtonIcon'; import { IconName } from '../../../../component-library/components/Icons/Icon'; +import KYCWebview from '../components/Onboarding/KYCWebview'; import { NavigationProp, ParamListBase, @@ -294,6 +295,11 @@ const OnboardingNavigator: React.FC = () => { component={VerifyIdentity} options={PostEmailNavigationOptions} /> + ; - statusDone: string; - statusCanceled: string; - statusError: string; - } - - const VeriffSdk: VeriffSdkModule; - export default VeriffSdk; -} diff --git a/ios/MetaMask/Images.xcassets/fox.imageset/Contents.json b/ios/MetaMask/Images.xcassets/fox.imageset/Contents.json deleted file mode 100644 index 821f5f18add..00000000000 --- a/ios/MetaMask/Images.xcassets/fox.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "fox@1x.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "fox@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "fox@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ios/MetaMask/Images.xcassets/fox.imageset/fox@1x.png b/ios/MetaMask/Images.xcassets/fox.imageset/fox@1x.png deleted file mode 100644 index 97f613e5aedb7c09b85e6884f1c38fce6673d099..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4295 zcmaJ__ct4i_oh}+qk)3t@u(69pjM_QVUEWW=@TDZB62F+KjAnC86cT+b~r=h7zWIcbuKtsdnpr@s7 z8A7|64e=7RKKc1&3W5yw?ybt4t^~JO^QOk%>_P; zv;Q|*z{fN-vWLhJ_=eRoN!Oi$N7$t_H)bTMmOJAUuP?uNP!>m}YVc-udUvRsGIn}C zF&#dCaVPsrZPi42cD6F^DJ~R;#AY#E`Twxq|M@iOcq(TS;9Mf}hQ_%(h_-aVA{*Ah9mlUG!yZ9OrhZ_=^ zZ}dBB8DJ<|({LzDUOrSzJs`;pITtPflsYZ|C=) ztT_Kl?1OSZ`lpPWnxcU0YCyLRtQcb6&R#(8c)Lx3;q=a47od885d)3pTfZR!D)tw8 z0qLxCsd&`ue?<1mZ@D<}SfhJ#Tcp7fu6G{XxT5%+>W6!g_EYafS`Sqw@}zUBvL^nX z>3qcaRcJ`3;>h0T3I{%>*$ykfY-qL@iHU3N3LPU~E_JrnuX_5m0O3WLw~74Y9*wd} z;RL_6h?gNs31seS|FpbrpTVGA;YPG{-5E;u1NXxh?xdnkaG%5!A)Y+BXJFfe2JH8l zeE@bc3n+C8qSr?59U+u4DtE$7PrrLMVx^&s?uixk{-1dY1QL%WiBd#75q**N@7gHN zlW|;Iu*?_zsVVyM#USwP?Bm|T+cNtgxrgkim;Tq=Ws}^|&gA5W_lMP53Kf^(ORC>< z`M2Kd7}SLjP{V&05U>ysS;DJxVwaq3KweR6cvY9X5g`?i9t^9LGtGS5Xo3<9=79i{kgcTLRgUD!0S zHY{QTVMk=Ud^m2Ot36s=_uQ1~0xk%vG&}L|5DHm}IyeoiP{}S+mHE-@472rz^ADlj{5&rBlMYuh1oQGb|^%Fv;irORsLo^Ym+^_8Ocl(+Ix zwWpEBbys}T-uJQGD+i)sHdC$ofZd38ye*)k1moqb0sGWZy_U}oo?Foq_fDq-eZyh; z1689QuCMQt_4DDC-A7S1!y|+5EgPvvj%k3(f<*)ALvp(rS2i+iiveemJPU5q z4>t1`dfCP~!JB`ou6OXG#PIFoNFG-3AfBHN#3Pmb#Y(2`j>tM+8^_>o-nBpGFdA3u zg+{*_WIARZ*6At|zRt9)t4VvPhW$PY-vmXD&t#T*pgI7p5MuZ=qJ15h9!l?#P^IP0IHU=oJBn{0Hop zB^szzY_?i2o2kp2tXE zE_@6O3(5Vllo#FFxH`!n9qZNHHTtxC+Hg!N$#38@b34W--KY-uJ_zKEdA{Xr?*$Qu z-f`pG5a8x4@_vbkpp9_POjq{`m$9_%bCr`PFMt(h^5ddCs-wi2n<+W8*fnM5SGqfc z7I0bcrRRbM+&rM9MpilgBk#2ZAM>nGTAYXL5SsHHPV)Mp5qv@x6+oVSWlJ3@x08MKm4z@cg^>HY z18r=(nJs@LJU;h!e9N)+>*(XoF&M|(fj01b87?J(FcobyQ@K3u5eb4e##xU7Uq7d2 zWr4EGLrEW-%nxqVIZ3OwRS$+mw4Mvc)!TsTeYa8)9XFCxnGkiK3$33OH3yyr{yko& z@ZbDWx`GK=^lb+IDm{SX&VR%EP~%WiTb1tLy*6hyVVLzyi$Sr%4r4Mds)@ikBUA&X*plB>L_(4I)3|{p^V$~KiuGW^k6%Bv37go^j5HB$ydG8r-_wJXS5Un$ZfyjBZ`%84 zr&uhoy>UfV6IwJfq_t zHhU&5|9XDE2kp7}>9G~dqBLM1sADE$=xHX&QTgxeZ&J#QwN=OP;rc2QY{|x6+pDnb zp|#{SM{&p5c+3%%K1|bdoeJl&mU&+g!Kd%Jl)4eV4{zeWW;S|;w^jq$UtY*NZGfRN z1!_ZeAu@lm<-%LaU6_xZ3DSP2G1Hr&7A$l(beI@^_mYGyFCm3L7Xs{3zH}9yw@thd zFsiK$IM}mfza~YG$pY?tP9GMl5`X#l>#( zk|sS<@4UOBP^QnGBW5RcL^PH?;qoI%^M81yRQPBT%eoBHD)JbH>2h+lqXdTG`x%#F z=*zPccZZ1Z@(szy0rkt_Ed&2TDji^9q%L+ce&i1dfA# zQbMfaDi^1Kjui+NyZ5_`ZDs90KVhe?RUnzCIYSoZ;dXQDfmK2~EcYq|AKX^lZ5Hoj zR@j;xtOVvXZsE0}fUBv=i z%G)iRv*uqO-?UpeW$m&if3~QM=J7T-q5fR3`Zknd8ib8JzQP~bL}LGg(JIoBi*MQ1 zEU#g0az~M(+iwOd@JNV*IF;l2PEbN(AhnIUg{K|zalgk4Zfwq8ov^gkS6LTUh+ksK zMpBBih^e}*WV$C8|F|vkG~}1Z&ciXc&e)$+%)h3!rCoEAus`!P6QjR|f!wv#YH)5U zNOm}2{(ZH0Z0%M6hPOg6k~_) zs`V5Z6uazc3a?tOz9-fh3n(1z3M2PJKXON5QsglI40U-^Hwh4@NVq6H^QUO3PraYrAp?6W1-nZ;;ntNk zkid8;Vv#v>FXF*VO5K~ zD=>x@#+iw{s=5O_(==0zch13~uw-q*PKuDwhm5L8Ng4HlNt?%SzJ=?qMk@Dmx?x+l z4ozo;wSC=pTgd?|MZ3Bvl+xyk6Dd|Ny^o(oedF-U@ck|8Y*i>+OjfuCes@fD=+Vsh z3Fk||lu#uGcklFLs&A|0e^~{5;kmBEaCtRWv51!ML7r@64S8`z@tw(cstTi(&-5c2 ziz{q4BzlwHu5Bl9XyW&*@+9UyRN8y5=wCY*Gl4`F`+RpYj;yK=sFn%E_e!dI-N-Ax zn5<_Xlm1d#cvkIyvqaFlJ48Zy!z2klkT-qTK4*~aE%S4{&MggVX&`wdojjJ8wc;6n zufpMZker^M!%Tgz2!S z?r%xk?y4d~!;t%`OS;8NUMq-cv25AGD#P^4(lQrw-OEe=6G z-tT|-&D`CcyU%m(oH=*)nLWGFS{h0OxRkg60DwS6SzhO9y#23XV?FgzhsKgm1CFb* zkp}>PNAh1o17u`TJWZl`=qNz|)nnAirxy%cS#?;ThhULI>;w`nK43${=(N7DI2_no4 zW5q=*5cae29kak71U28UdX+#@NMg~amn;}Y%9lzu+ik$ z!+U2%P;eJmX;-7{)|&O0^(c9j<$*@=(ei)sppX0br?B16AxaEaSve8E&~|F>Wo_5PAH1!aLXR(gDUI29KDhEf1i z@cBaJbt#dYhBQ+?s<(>|m+yG-hJH(TbLv;WT%@Cb`3JL>@lUsKlBj$%?ap>QEDv8% z-MghrLyArxHFBqfkAG7%qkOKuUta&=u%JleKR$?#l~xGh-`Ep)T;KjClvD!>larPv zVy;oD14)+1b)2_04(81KBU|9jv62{XiLstSJD*A&jAioW*EQG{*;$E$F&ZQY%X zrr{4cdcBF8st%MB^ahoqgRd!B`yUk^sKPcG`Ub zWN$+DjZ0tlQ-9X5E)RnFgWKZOWu8aL3gmCzc4Q2KIx04nNl)q=zh~QK$9Q|HA}c-V;aG<_tT_7YctI}%iSX&q{I z{4hk;Bt-2=@f`ntQq5W>lSe)tbZ00=e)H!fb)T&D=H!JdFLA^cC1U2bgBo~i#TQ_z zw5>T|fzbn3YDkJD>B69GKqi{|wU}Tu%OpMOyrzuY5YIM|f7D3$E~U5`3FOT&uO^$H z9}{%ct4ES~E5vgZg_?+QiNOzy;gn;9Rl*5Mc!BL#vY?$7sybT@c1Bax21F)K#QX?Qloroy3Vx^_=Iw z_UdjtM&wN8paHT(|4Ku3&wF-vyA*neIX8y-9HxFErTeQGnm4E!1$$MJ*QCM5;z^UW z94=qG*@obGN(s9VMo9UH`iX6p4ki)Tx*O_1W=%eL@R2-1o}u!bb`fe!!pm0f{6Mx9 zBR~m%i2gc$S>U%-`DJEZ8;hUqQc z#^KJa$zJ{i)W)-}?t6IgzK^VTp907&;i9j{Kg^$|?|c8b<%LMrIcHSB%-bZUuQSN^ z5!EUmbR8~8X1hVPSg(8MeZA7>J!|FAuiL-Z%Ds#3SX#p_fj4Nd)hqgV1ey)ECFwOy zYRJkb&^ z>6GuOQW%x+89}Bulz}aD;?OBRJX|-MQL~6*n(*(J@63LPd~C%SE}{h+Y|CkUg-9d@ zcwyikcy5CvGxS$~fAT>o_024POw#k_|NQA^#_6lYn_K=^v!BngGa8V6Ss!f8S_eV@ z`K9@H6IbP(HNQ-R9j+QnhtR zA-|tgMM;+N0(svx3^+Yaa>HdEDV5?Zzg${M{P;*EyqrI28LvYuT_DzD!k`+tqUwny zJeEu41}`WpCG~zCHMz{4>Ae2alcKrQC!!U~PTD4WU0I&d%bJPC>pnFGEVx=$&vceG z+KR$LBNH#W@D6AD_k*Lt)_(kZyNOd3*GH@Z* zc$CBO7}95+#Q9vX7iv7t;--H zv%70SvXPu7BofvK@kTztzGI)H%qTBE<5O&*GRW29Q$?(f%09iM+?Y5h5O`u(H;dGX zJRW%BPE8}MB9;LaJ*O*Ki!1v1t z;gLy#pN-ORtLK&FLFa4?Yk#=Cd9i8A{)r<*MIq9a7WVguK{aujOXdWkyEnIJpL1PeR z0qemfto~~#Z<}P$k6pH`qtEF%HH`6nsE90e4gsFjRivN|42)MW3%%5DlvQu^8aR|# zyn5a=(DQdWvkCn@Vybh#+V-d83VOT5>jqtV9QAOv1oct^yWvP$;V@H_*>KurQ}60Z z6M?r2*<`@HoC52-^&Blh@0H6zn1A&O#t5li%{XWqVv8~Z6kK09Bg0${SMiUMrsbT^ zXfty4J$2il*!RHThUL3&6kjmd6UpLbU^e4Wf@s)6vEgjed-D?LCC^IuQkU z4&Z9dBX+mx8P4;_#v7U=gcbi;ohEBIdZAk`{{o%wRfC3PlZgF@mEI6|H+M_NL^U(S zkcZD8!a@eMS#m=rX=w1o(*Vf;^u(6Ycvgn{)=^jT>uWGM-sg=^m$2vB9nXv*#z(w+ zb6CVI&cSKq04Xu(h3;QlM3Dc>09ENIZetWP?q+PQ;xlIlYo(6694mcQgj&ei?7fdF*iRkGXK2}qZs&yD5*0Trhr5vw~4 zB^$?bQM{(bH*Z#0I&Zr6JF2#Nug5aj@vGuel2EGtgre_pf2XRasVS7(HLv&5R3-zd z0u?EgqyY&UgrEA7}5x?=?=ik@|biKbX5lqEfeSk2ZnNY`4; zFgwg0G8Q?z6V8~4K^&enfiPq-sEwHBU>9j_`6~C<9fv!`p~Z@tP;%_=5iJFfXwK&i zt)#0d5dYY9UjN-p;0b|Dvd0Pfn?MZbFgaGqse@|S(_QjByeL_rQI$@mWll>k(=F`n zoz%}L1>U*3gOumS4l1H}$&ixQR>Pb@H^0W6y@I&SaE3>@lbas@BKohg{gUDoUk%9X z?CJUcslCQ*4k=V3`!9S3G=#)9;s9^+Ws=w@aW(6j`=$3n%-noBXs|7W^6!w1G&^L8 z&)J2W|4fc-U|maev5+)ky*QM79Adu1$A&b4qlcr)mT5pj+8y8+km*0ix_k+7Zd|RA zCTJq5I3_9{DzQqgLa!8Seh6{n&F*sSStW?cR>py$M?ujP$YW-*=bS@^8L5VwXcz(^UCqbQY|YNe%?}Q_KLG9Z zX(7z%z5~D_Ihv@{oqQ6B?>~r&C4e$ywKJ}#C6A}Cv?3cOTo5+n^n+U)j9r-d5GB6y z0r3!wow!fO4-c1BnU<)WY!C^0td+ zIJoo9V=V?B_r$Y{CkV2gbmYXqjN)t?~}tC|jJl#?(E4O8}hv8 zlF}$y&EC*tECiqNm(dU=jWDMR;J*F6fzar+_-DtnAbP?WYTnmRdrf_^X@V6aBds%p zB`R(?+}}w;l+lK8o+bnY%SH> zipo(5<#f0AT3x(*SOH0td5H_b2SV9DSbifnG`q)4%{Cx^YXEM;!PAV&HeBR<0oQd_ zD`Iw+H=uay#i(n|Ra>)#ojagdN}Z}9&(_8L79ReYk9MC~VSYXzDVC@%33a?a>kMdhdl`2!eZPTa zYj8Mth`tgP6^;0=A&nISw_L_)EaI`&w{Y;*{x+Dj%RAEq*Q-LEksnp1Wd@FOX&s(% z__7Rn5d+b+j0(Oe8}Gp~8K&!AWOPU4UH-1)ISjg}8RuXR7ushJytkVSlIrECwatfG z6e;|JryJ0_!CpF$wD&kb_eF`B1pSR(7vs{|7VqCumhk9py=-T6U5wx7&IpwIL=R4~ z)g5Qk(690ccG396)}Of=B!_y43niO-QE_tWv^Yb+ohiXEgTONu7cc5$^MX^!l`rZ~ z*GxvUtaB~2d8ZS_8u$3^|r8$*Nj!foSf~Gn9q$zxvuMfNNp|!2_NejTCKJ_mNZA0 z;)XW8#f2n<13Ac~9&UDRdAsM!UQp0vMyUMbPjT+7Ke})2Aoklf2MISfT!n!hte2#X zJ$G1VXVe$pDFVHfsI~7BlcYWsJ2n1f}VlW zpsA+Q_B?^+xpJZU4k=wR9CAZT7kQ+(Y2(Buu@)!J<9)sEhMSxEH3heuCL%d-U?)|1 zqVU(+H-dk-h9Od0h`;Wh?$86kTvAo=?O#q$=EAz|IR5LBoyZ)kE|2K#DHSS~h zA2!!_N;jW{^#@zI&mBnJ=n8vm^p~CsE5eG=Bi-jfS|M#w)dy}CoOSK(zXJ09x)Gu7 zkYB8&H5EP78f{X?SKWa4q%aAAx3VZHbX+Y1bTwCIp0}>6EmalmhpR(&&SedaWYI8V z18CPDX2KXPMo%g3MmJ&e#vg}g0vS+FqbqI$bD}qL>=LlIZ{>HVbf_?xY)h@*DWrcM z@h`GTox$Y^FAK~6r{fSYR#GI4zi@Tg8A>poDYVCvyQFjcK6NhMRHyDzMX@2K7krzq+fRQX3kyWm7w5suF>OR7{!+YB_+iZljRF8ueMooiEs>z($G zfW`KOxlNoR`KT7|x2)Tjf0o5tiyFSM49|PpCI&2&I~~xbH|aIcn@~=hmL|TeiV{3~ zW=6FsPhNK5S*Fe{=+Y^G&9EJAYYO+UrHhba;az}F>`DjtA3ZMHIdi~l=9&G~U~XhK zQk2hf8K;+3N&WVJ|NO!T`bU4A$c?neE+Pl`UEx4?9u^&h+SWo;f5z>kZd3^&3Ao6m zFj_>h$~3ofm6@JBTT@AlGC6RzE9n|HdB9#OWE=_thPnxi=2>VzrWk#Lvh*@bF+qgA`V!Fgo^o=9%x@A$M%8G3h1`7gA#LSgKyC#9V z%|fbf1+ia9-Sq`yD+suL;YC_&04i_A!CU2pcu%$|kA+@2Z!XxUU+ATaP@Cxdl9lz6 zDKt}aNZmw7)`>^+(3dqEFjTs)l;&)z9HLKK(B4blJiWJ3=+&HIL_OY=c@x`=ZZjRL zO|qqjBZ(%92`lgYW?eDar+vV)6uL>kmHIYU3@EEG>&YU<$pR6nid}7nr)Ta@S>x={^+%g)ta7;N{B9-=MRISX*`=vnix;R$7A_O6XC|aH_u{`CT1LZ z_ySgrFTTRKUy}IImv8N3VuqrQ)Qx?0_XuA#TL`L65pYfI_W9lQ^|WR21TF6#B{L6Hjj_ra8%}=}bv6~bCD^;t__?VV| zKDZw=?OY*XHKSGH!A<*AeBcFdd1UR-%bsERE>YYOKdx?zUkC9Ol()Z2%u#@#n;xnO ze4z97EBxS#GlIal`NXhjh4_ta?W`|4l#{@S6j`-rBZ`$yLQ?w;r<(V-7sU$k9oI9T z{{A_unSR8)rsPNVGx8>2sgV9hgj76#;df%m812=r8LEnjvH5!Y*Galae7WHTz-hX| ziemqzYzkg>^Zb6EVgd?9^5yT($9t?SEEnqW`<>?PM*$hc=4inKr2)@**OQiNk+fIH zdFI!be`>n9b42f{C<3p14s!8zPFkKd5VPOt(@mmPsYf08Bfu9en&Z6P2@jjE3V$29 zejyp}#CY1a2CuXCERy2B)qM2P+Ks6PXr*{7k}DLM zAQEaJXPf-|1svF@TAjsGlV&%d51Kv+i8jauiXfM~NY70?bWL;HFFmMlC3N>-kaO9_ z>yLx)&_RR$ac4bo$m9zMTHsW)S6fQq@?|;?K8~%%qxtcPhHs$&F#t67o7%uzZw|WF zq0dL`KRK}{7=Oi9z=!uFW`AFpIL_J+QK4~rZScFMJV1|d5ELFp?(fzmWw>G!@&~qZ zRucoabhwpq5Lzs@*N{DmM*gIablOYKp)-7r!v4{Cm0EqKgBI>ire3)<$$z*4V9YWjYK6#-H5iJRgX%bC=c=+zivdB zmGuD=XzP^Wl+_eE5MHPHbv|5|r~XpK3{Ucfz-Qg5$8+x=AC`$?*-)|qlCuFbLIrj{ zfzIT=Hf){m+;xCQpqAjp(rSC4T;3|j-S=@HSh zpM3o(h{oED$EjsD~?45+6( z{x$-5a@}6$m{pq8g*x5IBC;+%P8i^M-Q#I}IGFbRDm4H;WVZdmMLDU1ia^y~H zm>!d#i3xg1x4M(zsg3L6m_J`WThGU!mUjR8gT^(w;KqwQu2>hlemFwYc8EZif#Zpk z0o0ae)n>{tn{pg*NyT*DbTOhU+hAiu z4EDT_+KHP9*GbZqe<>6cfKRH)G!}d}AZa#JnP@d)mPenCj02uExEu$vN}^-HGd??h zR~>s3Vu8+EyNi6YF??p9_r}G=2@*h&ug z#Lt^cs`0-Sq3Bt-Aq-7oF}IJ6(~4h+3-LC)gV2JL;FfqQy!liKh2)C~+O^10t%Sak zwkjK0;e4ip@~s$dUuflEas}PgCKw?hOat-`lJ~BS>t&`d39N5!4nz;=kzr^Sth+T_ ztSA4K57VOk!4i`B7Rt8w`MR*;ITDioG80Irh z6cepEzE7I^7in6T_Tho6G4|=Wr*lst-Ykw;X?r&Z>eYO`oMglpXuAu=c3L5g& I5R0(?1I1h$`v3p{ diff --git a/ios/MetaMask/Images.xcassets/fox.imageset/fox@3x.png b/ios/MetaMask/Images.xcassets/fox.imageset/fox@3x.png deleted file mode 100644 index 64ea262104a9220c5ff669d3a46bd84ef5a8fb9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11482 zcmc(Fg;!Kv*e?i*lys+r(jego2uOo;cXvrMG%qRLIY@VRcMKpYB{d8m>VG&D4PMFkm6G&FSJzw6}-)DzP|+(*1CmI?q$-fI74fKr?^$^`tQ(g+KewOwSb%SXqsUnGn)|7;MZ}A)rjmuS0MpD}s z{d6-R-uNox?U`rI)aJ5F$EM4oONW`4z$kXIxe<$!f=?lpkG_7&kOHR<@R^c;VG+6m zMZnruIZ@_#x$ll-uLD{+CuSwSyqV{ilo`geXEd16jPZFLD)T-fB1^hsYb@ckeG^e% z-GhW2Ic06=EMIjdZ3zz`>qn-?gfP)9bB6NKF%$4Uy?pr&a2QgP6 z(X&l0!O!H4w!4p`FV@LPecBttDysZC3XJUvkUUA1FFx`0;{X}pYdg;y8?rc0xLoIt zHbJm_PUDBdKV|DJ10wQT!ica02pyE-bu~I6TeNv^1`zB$v;}k9dGlrkq;|G8)|4aBbkEVw+2LV_ zn|hGvo>x+J`NQz1yGD1cMqsdenVv=kRKMuR{s53iHY%WtErSfX7HI*oQWrWZyU@IvUL%sU! zVc4{}6GFzTIaWg!Z?3?7`MrJL^r}yCxSI%p4rGy3!6EavZ-yg5F6Ope#$*ar$T3tcz6VFEjnteRy?T4~b;`^}Xm$umlKn zPR?^ujed69W8Pb12%}dOf?LKFH{NZ<{ehTx@6qc*`}O75&jW^BPn1`#=5GB0m5x+V zI&a+QH3$P4I$(AC1##7lSiQ@qd0S$#2AT?|lVX#8F%Zdxkl~r7oCg#Pr*0~g!e5Rx z4$p$tI9x>9z+w#NuOp*yKT2z3ke>mEepmDsn+9N#bYA|AZES3@?f9Il?BcEl{y`gE z;HyIFi_$OS-h?kW>1y3)h>Kj$!mb{c(K9(6R9Vn%Xm>yh{=M+=r!om5;}w2@-@U-Z zjwHvxxc`?UMMPI`OG+j~=)gUA?eBYTK}9Ecy~l2TP7HFhvx+PbO#}bsPi0K4*!}!5 zgVmg$5EC2sxwk(^4;BVbjY(*{2$%okn~~!yo5XKT4F4ah8tYR0!1TwTW9EAJl=V3# ze7oAz_H?feN2H5_URV2G;QV=TR1Q_p ze0( zJ=H;QcZ6%ha?lEQg{v<^(T=ImtqsA4r{m7`{H5f0xqzXi;nrdLAMINv%afZCC@iz0 z^uf`h zyMp?i#k@U)PM!O<&rYn-lBtX~hEj}381@-taeqrpjo5x2QD5Fkq3JeBBOi*&K?arhbF-(hcJ|OfbtA>UaL}hkks>*NM$J5to<;xQq>3)^D7_ibVH-zxJ z_jP1fThq5jEW>f8BGhR0v@WHIjj3O{Lq^?M8eBhgR}F(jq4&QRO*N8s17&DY&M#&K z>NR;}rFLX#AqP)wNlQRPGD1FGq(g;`D>6#(`r^+0=Lj|Y^4U?yNH^tjA+XocU5*d^ zFfz~K`=$;lGX3nKN@d2%WoZ|BzN%9aKY+XUD_<<#Q|k123n_kNUf1XAo*FiIeOQ`J z45@Ear5u@S*`xJ0B4x(eu8I^-@5>yb5b2Byp9b2*Wa6S}D;H9R|Co?e`vF!Xn8i0@ zKfecw%x3sA7TLcuWcS~0P)j}t{|}}IPrbPIkjfcfXOj_v8ONG;j*H*~*wacIeg#Me zx@YBgWTn~v1B#3SM;QCn2mx<3=ZUg3oJTy<&pD6b`(>IGjp$;q%wme_nT3?L)i)OA zY)wTJ`r|Gi;Kh1yHRm&A$#t4$(JV@zVrnZEx}o&X9);edP=k-{WV2rTqg)=WJ4%hcT&Fc7HmU`F@jp*79Nc6e^2Okx$jJM6+BV^li$6H=kJ0v=w$UpxtnHxn;LD`_) zShH8u#%rmk5)|8G?vKoA#I=2whC+#?2b|})CDsEL7a-^}Oonf6EltQ_@(VEMQ61 zx%tiIu^#kw1>V#(VFrCJEp5%_fKj)@*jCm#F-_+e;FknxP3`}g^}57hwWfR(x4iKy zo=Fu}E8a$Q*eJxr*nPiOEUN+o(*`Vp5;xWJY9Z5zAk}9ail}H}4!8b5dVS5nlJQPaFjN8lIgeP!dw=D8Qgxr*+_lKh~<~uOT zHJ+V=63AKKKRbV)z1XtH%X_WIzW#4z7j&K9f%_OQUp6Yn=ZrfpR_9TjU99u{5P6kY zD8f?SSyG_CxdAtI2}73ulV$ zDd>={B+eU;)qg|Ib~2xw+7rhHpV%l#(GTsY>wN2WK^Z9~l2^yg)NcL1gtUo4ms$vO z0*oTuL?zXjz{1<&u(`Pd-i0+kI$gLsLCQ;a$*jS~R@g(!jkPCHvSH}+Re;m1N;OvwB1#;MyL{oz*NS8B0>`E{iDp=;51+YCnV*1f zRO&(t=rHPLe!c8Lyshr^6x?|T0~?b&ENh2$+hRa;+$GF^++yC3UZnyO`)#Cnhi`xgM+ z1P#5=4i~;_OCi%X^TIZZ-n+mBl=aQKyv0;4OCQ}f_7tL)-FCy+KkudZFbgp@cUHI= zE`Jc3v1${`G_dTz!|BO^q)&rOCt$OEbc8e6cHf`59>1UXlmP&c`gnS`PSy%pzJ2G1 zjZnjm*Ye(OKjlQ?RA_6g&g}XUok2Bn?3_l)k)*otvFZ=?4d0n>U>RK=L!X>l6lXSa zR$+Rp9H)x zdFQrqU6(xFCoR5gEn(YHh(ztG)3BbpZ;Z9DC=viIumg^}K3W`?QcR7uY-kPT1J-LcaF%?kHh zKG2P9$<6<0c%+|Mj4GMRu{vrC6V1y+04hIod7v_VbDZ$4$8o3o8fw0z(tlh-Z0Tq# za*T9;;6tA_<;){o`dls%eZ&1Ujc@T04nYKeu8)P_}#WHj1AwWANB>xk1)a9640_=qLy+Tu|N}t0`@M4$DD+w8LhiwV2 zxx%V9Q{CzRKLbPM6;BUq7IBeIDY)B)@p2+7(vr&T=84 zHqWE^X{L(!skK@u9J6U{HK+V-dwsD`4)w~*_6CU4{jKZq)D5Sf_~zXnwz5^ML1wx- z(G*|&Rvd-v?j0_#TxC}ymNU}TZj`F zC1*L0^NPjAcwPo%cRU$Xf9@NpcdoX%5E-sUwCKSsR9KBdhpC`%TkvvdsT|7roRN+A zq*w*Ttlr;DS*!C|2fh`$%Lx2}E&m<3U8t@hU=i8@saWToYssX|0K-OyDh|9_=XWHK zFcVFw<;gsYd(#iTaXULkdXX)Lyf#i;0%UM9T;20usUKAvh`qsdJ4nztR%-v%mC$4U z@elV4J{p>6*|YL7gO8x(NNFSJEGmJH-*PhkA^x8$)LUjP^Jn|A=s=F_jX+6|BwKSS zDjA<`bSe)PY9n|V=K|2N@)g*TR=Y2czkYs|CXSMk0j3j!I_Y&AHr##EEOB;XBwx(H zc_l6+w>VU$$q^?yhFFf;z#45wX=lVma*=`4!8^#&&x5LjY~1ZA2lVJDE~D15A}l7G z_1}aRba;`?>Dj4=hN19V8MH(+ldk8_$o{Uq4nlJ8=l72msP=qPv*)F@5#(Jk!ZB+v z#~-joY^N2fc4H@d7d9#Q@7+8#p@`q=iPt+;b3QHJ0JPtUjU<>WVKlvhqTmpL1B&E$mmMGkEA$JQI!tU3n0qoIzDT;k)xfkd3NU!A6N5+;1>sKL%D z|7QkOi(ZV%4idHITc70b?|#x)C~;Zcw5#=3@6?FGnFa4RaJ_8TL7|KWnyL>k^o_<4 z;B|3EJR+pgyQt)w@9o#m4b{prH5ob`7zW1w-y1V9ODkrxhaAdQ~;`2X>#sG**Ex>(jqR#vXQ1 zQpz<=6*}iEs}t=2MJoZJ(Q3~13roK)0DJiLbAsT0Et9)vE<2YTO~sv_h?nx;@kyCc zD<&JTS8v|4Z<4$I5Fkt=iofNj^EFXdbap_0Q9}XcQ7aU7KL=pEW0hcNfISU!o>9LN zcc87%ixC2w{JH5{Snf{2N~H#K0%w?!=_Wb$A8xFbptH58L}DD_a_q9$W@x7D#?!|V zi6kX$<()`nt#!QQa=17BA-;W{t9NKv$NSzrQCkygDhcB-Jw_Q@yNW}#O^?{D(fz0x zWt0XgDasW|qbel=)S+a!FFp$n{-d+YS)jZybg|b!qE)q|j$^3TnK>PEwLuZs^p(E%(1*WZ@v5%kG0NFu<(|8vF?^6q>vLS8%VIf; z8$@)1sU2Uh^=ODp(ut_mMYR@_$Hfzg=bgfOvbNuES6N`Z^0!KPY<~UIbY}2XvzG>s zoeF-V-LB~b0dcmXk^4S?YsTm!tCOMu$;>+G6l3bUjP@6n`fa(>AQ5vdcLo-|wDvq! z)csVPt;_e#IyN{o{JXG`L}*Scm|wpuw=TJZ3DjhT;+Yel4r&byZRRe^zlTT#zn6dMy4Hu5Y|erF-Uo z9I;1J9d6welOwDVnKz?0ktn{;Gx_(iDeUbs_kTL%zzw7OKe#@DQqb8n+6Kt;g0tzl z34Z|_wzJZ9ve4O#9iVAvy0u@LGY(&Z8-r$D=N+lA5=ZO z%QF#gTPn*Rr{ec6zwNnr!N{aZvSf37)bMO+J;da-s7>{+XA)zgH4qV?u3~}2k5u@= z>N9;4_l_noqAPBZ14lHin@)2D}0`P>;xG2G+^x^Lh|&hw~{;P8XC85peGU3jc;eVqDzb?(U-M&W#0F7Ov>P zSu2^(Sj9)r#Xq`37lv;XGbVO%-;?=zbN}aE2f65y)Z}41>1|%%knqcA`2{E)?k%Qj zWUb{mn>0i_=73M$30&ZwT;4<+4bT2rBw_j9d*+g?)Kosm8Rykz19!ae3AVW9ZR~v{ zOK+tU(4>wMVrDzL08x;emey2}g@VQ`Kt}gtI;-RR#($wjeovm1U(B+~MSr%b_mv6} zI(gCj{uT4=VW#K|O8IE-32Lvee?ppY20xyQ-{k8|IT(m^KdBazwxRuO@z7iM_~d9| zXWUEux-L||q(W;;mPh@t4-uY?R)aO}nC>oKVxV4vkU(2f(hcU3KEjekwHW z=W&LJ61!RuxCv^_9cg^VX!56tI@eLw1~k-E>252<`IQ61pCHYq?|@rMVc*TVjDEs8 zQXRqh$b7#!#j9s$eegJW5& z8HXBj@mwRn#m|i_=g4zZb{`^94IXJc9s&nXmA5@&?F@Ud?OD!ojz1nd znX!VPpK75|7GK}O3?iM4dqC~Z#&MQV+JWbd7YqXhtVPxhkS=h8*4<}99;M>div*e3 zI|1%CG233NvESpW?2zCK!zY1QKZRHm+ZLAe6WHuP;D)W3T&ZT$)7e4A zWeU>$_f16+B>DlYe+tl5ab5FQMKY~=FG9Zl^Ve13GxPIPnTpY_kS%c$-nhqq{LfrkOe$W-##oUH-$4I zuKG8pHl4bfCg-R5G2#Nm@JGlIF7odvWS#SfdX`6nu743}E@3ackXQXyo}`vvL?D+q z?_Gs^SvFfna9MlbmutW44M=gYl@OJ*!g@rcnvDZALOwQ8gN>WDl{!#8px=|5uIEM* zNRsGgzo3RE_Z4oyO@-Zf@a^B!C%ghpq4~fBfL6msFsE2sf5E3lOU^J0VmVt0;O|pd zn>SM3acu$cS;d7hGxwv};!2wjk~sP;KPbfja^Z<$*+b3RCRcdw$c{qlyTzpiK4)PVCHFn~AWM8F4Bl~b6}*NCJAb@tr?$jh zEY2~cj=FX63IGtYy>`3I@68Z3ysvJCty350P__II^|h1U8wp+>oJ>7PBr@PX&Sej) z%g(tm@zd2mH}Y!>*2Vdl87j@|Y{S za@d4nmuH-r`d46vwv{19QyFy>H{$rMdL=6C;2D57-5Z~4zD~5Q-qgpZj?O7I9O3IH z9!k(Ii;x{&h!t)B#IX$R=|9>6u!E|8@^LNp5D6%@8QP;OOGUH8AH}s+WZm^*J&}$m ze8$*jI#NdUolcS&p~W%U?IvboRG&L`c*od}VJwg+e!Et7nUpl19 zf(6D#`8_>to@iBuxtL1HLgO<;|M)m*bHK|532T=4@Bo^1OiWCf8W6hSM;|3^r#G2i zf?8D;l%NO$EKcN)&Is;g@~S0euEQ6W8SAeYojMYu$p^uGf>TzJY{l0lH3pX^JF3Um+Z@3{7*seREGZu z7pHwaSiM6K3(m2Q--ND>H9!D;@6%Yv$pC&3g+PI`osO7t-qgcMArhs(>5gAuM_pA+ zpokfjfTTMk8LO|@!@o@QY`?qlw@Y8qiYID^{v^P~HXZNnE}>Q?qjAM6q9Bh#{S$nh`W*e#`Ds{`#TWov^csCW z#q6SoSTg%tK`I(kY=F2xO95lxz8ZxUwKQ&Em#w9utiUdH6je`$Sf_|I9tr*mkw%gX zDKhlocVF{2F3Fot!uvftxiP_APzlP=xnr{MzV(aqNxEFt6;xy_0#SB1|Xhdd=sM*mD)$-~sG^^IPOQPW2$B%U& zNBF57r60lc$11~>9=~LF#JT<;ta_Fb-J6H%M`A!X9lqM9GaA0wYb$%OC62TCj8b)( zg=-Zm;-1Oqf2;h06Uo;3>NO|^LtdbFF-!ta-4K%I$f&!{H=RvWRY5>uMHvqK$eu>G zD#5ciolr|v9c>E(*8uz1KyK&y5M?g7eR-=zHj&Ek%}kcw@%W7IMO!4Zvpair=RUsx zmb;;lt)#N&H709SYmPnyfAlj=6l;Fl1rKHLN5YZOIYm1~LJcsT5fjNUCwOE))vL$) zFk!jVD(5zpGg1cl-3`xb72nn9?4ds)M8Yo_Glgs_`ALb~v%`=MnvkNdOd?2Pct^ithz=x%{ zD*2B9lBt;)4O(9sYpx9j6bogk7oO?N9i2O^)j5_2L~}I<_tqadfr&OuVx1AtV_TAs z&sCk>#~pOS!9SS$gEnnrrN;u8W1L+@{TpcJ6x*UrWMaADkC_Sp)}zsEZ>AHXIe_lY zhpO({mRzQxcX+5#Dlk-W??4=n<=e@FPn;b`uyv*inhT0VmDt0>0NpL$eGEr;+SrAk zG#%=dH`P&FXFq7<(AM@HP|=@$JLL_KAapIvxC`N5obSYr$NMw@Jjeh*%PL)N4=}~9 zcoF65Zg?YN{%&lF5n)<{o0i6BUvgk*1c-I)`FPASF<5TA1B~P3q@<)^;jG40eep+t z$+tiw`Akd_6G%{fI68f5ywJ+p8AI?@uydr?*kLu(|%SuX0UWs&tM6%TfaWcQxFy>s#=4iW0!PeRWUb@+Ym{ZBp8Z&PlH(Q z>?7%0kxldGXiJaE5bX*hpw@4QqCdCeQC{%8YY`M8nX6iIG(GzRoc+Zqh48+`{MRZg z?j$LuTC2W{-XDHEAqbYTn^j9@&kuR0WH+1HrH^Mxd1*>9t1<7bE9*XIXZs`eQ#ZdD ztipCbf%C-K(I+wgVgC4$W{&mWdrLH8Us&1}6AMd3r;km)j1GB}f7<9Arr$OM0@)L3qfUM|0x?0o+LOp!*KYy(v3>=wwiopt=?$ z2X)f$dm#;T?ANL#9NF(*Rk&igN%7?k{|D#er#}e2hr7qQUc!|#j{LZwyA6py65%PM z|6Gnb1wwfAu)$w!>zvf;O>`Ar?0>GgaQt!;?)t;&=~N=6>gKM7%&gAMEBt26IMM2l zW^r(JtSG^m!d!b|fNXypqfeQ;5+#6A_|L($_-r1Z=jVA%PUL6uaRBX_TVR?W;%&5R z>hIDPnU>PXYS^q1Qxf#p74@+KAubOrP#|4z-xxA-?dR%!e}Q-+;8%Il`%*DI>GqS5 zR!ikBs$TIX}y$G%+8*-w;5s!Z<|sv(8lUaA|?S?+|)X3Cr|9xl0SMzdS8SVdu}Q zIhccfP*ijj>=(XX^9z%`(LSnaA;O{$kKax4YS-0633Q;lZJ*phQe7ZV5PH>QIJRV1wx$`eu)2`gy9a1oTBau%c zHfC3u{V(eAG_J~l)<@{ps#)q+=guqPk@_8RzQ^B>@^j+n;qz6Zk22a8c97UX%s8W0 z*@+BxiC=i`&w*nS57#|Cy>CbJK1wBaJQ6k5qKnu_dRLA(2embNoS4 z5F;!RwJldB*vR>)>-o2PkH*?Z*-dm;g+s1=`|E-_Gao76s;MHilq>+kv&<}mH!&lZ zD+F7zigzVX&Er!LCs)zr##-cW-|~hEv7RN0L`d49>uc)Ed}*+ht77S4>B2@EoheQw zH(dEDQ=y@dqH;Gf{TM!H!;eMd;_LP2@-i7I>c1ZF<}&S<`2AVX=k;=x+^P~Q;hgE$ z!CyYi)glg8mGJ+VikcRw;iu#dNIO;NJZT>}wP2K{L-nI#F+Lij4pS7KNa@VvOsP5q z=Zjk#I)Ya31Oq*sJvv$|U)1k)M-G5;jQe|7!W<@s1(_bt^sc68fnWP?J;6 z9Q3(hO_aH?$SSxi2`DExszVycm;!l6Jx)IP<9FJ z11Hec=9k#K>Dx3n2#m}NZvNe>PdPX>hjiI|H(2p~;>&WX@^JuyzFWnll5Wk4%7fm+<1{G^0oPxy;|BVL&{&kz1i#r**-U^)b@e%9$i5T=# zt;0m_+CHYF#ydLmz-fo3-_Q<4?8b^+eR&q{O^e(v*V}JT^Y|-(EatiuHXpsI9LQZa zJNx6gxRWV&b;Y%HDl0Vddr}yU)i{$m`>Kz#xpefCdyzoznWQ}2Ws}G?ump)pG&ptP zj}FT@?VhBe3!OH<_ELpliH^>OT}p}OAR;v-%zafF(;8h;AUWJYCkW-+`%2p*SXyLa zMhbMLv8>YxIjpmcD%w#hO@-kvSU{{}Gl#X4qyJZKv#GPd zl5t?k9ZIMZDy!_6R88xt*b6Dtv;#+6=jYZpGz2Z(33|a1*QW)yM+2+OMS2PBAHp1{ zX#C3e$9N6NhL&o))PhRRRYJW~O*qCu`cZVK=uiq14gRhXGRi$eb9Xkf89808UP7JF zgvXSsjK6Fqa&%I&UZ1MMoO zA61jFz;y9D2=pwXWyUactD?^El=wu)UWDD+FEi#a^panMB++g+tG9W~B?CPL#O*#V sTWSJ_-T_UTSpT0ts{LQ_|Lxs}2(~ai8&s{l`u9XpR#m26$~^4<0KxdH^Z)<= diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 01a9c526cf6..a9da799146a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -414,8 +414,8 @@ PODS: - hermes-engine (0.76.9): - hermes-engine/Pre-built (= 0.76.9) - hermes-engine/Pre-built (0.76.9) - - libavif/core (1.0.0) - - libavif/libdav1d (1.0.0): + - libavif/core (0.11.1) + - libavif/libdav1d (0.11.1): - libavif/core - libdav1d (>= 0.6.0) - libdav1d (1.2.0) @@ -2097,9 +2097,6 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-sdk (11.2.0): - - React - - VeriffSDK (= 8.12.0) - react-native-slider (4.5.6): - DoubleConversion - glog @@ -2945,7 +2942,7 @@ PODS: - SDWebImage (5.19.7): - SDWebImage/Core (= 5.19.7) - SDWebImage/Core (5.19.7) - - SDWebImageAVIFCoder (0.11.1): + - SDWebImageAVIFCoder (0.11.0): - libavif/core (>= 0.11.0) - SDWebImage (~> 5.10) - SDWebImageSVGCoder (1.7.0): @@ -2957,7 +2954,6 @@ PODS: - SocketRocket (0.7.1) - sovran-react-native (1.0.4): - React-Core - - VeriffSDK (8.12.0) - VisionCamera (4.6.4): - VisionCamera/Core (= 4.6.4) - VisionCamera/React (= 4.6.4) @@ -3067,7 +3063,6 @@ DEPENDENCIES: - react-native-release-profiler (from `../node_modules/react-native-release-profiler`) - react-native-render-html (from `../node_modules/react-native-render-html`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - - "react-native-sdk (from `../node_modules/@veriff/react-native-sdk`)" - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - react-native-tcp-socket (from `../node_modules/react-native-tcp-socket`) - react-native-video (from `../node_modules/react-native-video`) @@ -3162,7 +3157,6 @@ SPEC REPOS: - SDWebImageSVGCoder - Sentry - SocketRocket - - VeriffSDK - YttriumWrapper EXTERNAL SOURCES: @@ -3353,8 +3347,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-render-html" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" - react-native-sdk: - :path: "../node_modules/@veriff/react-native-sdk" react-native-slider: :path: "../node_modules/@react-native-community/slider" react-native-tcp-socket: @@ -3531,7 +3523,7 @@ SPEC CHECKSUMS: GZIP: 3c0abf794bfce8c7cb34ea05a1837752416c8868 GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 - libavif: 5f8e715bea24debec477006f21ef9e95432e254d + libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f lottie-ios: e047b1d2e6239b787cc5e9755b988869cf190494 lottie-react-native: 7f3fc3f396b1d6c7b1454b77596bd2ad3151871e @@ -3597,7 +3589,6 @@ SPEC CHECKSUMS: react-native-release-profiler: fdca7c73a6e6a03fa2a343f5088fce4787e8d4ee react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd react-native-safe-area-context: c68127652d8b9a26a28ac9597167a3ad90bcd713 - react-native-sdk: 6eb6c9de60e510a9ea93ddc09a3822cddf04f8cb react-native-slider: e4b7f9d0616032ec2909ba073731eabcde242256 react-native-tcp-socket: 120072c8020262032773f80f0daaf3964aaa08a1 react-native-video: 3bb92b90b2774144fac7d43d52d11b936e8a14ec @@ -3658,13 +3649,12 @@ SPEC CHECKSUMS: RNSVG: 3a1cce2e940268a7d3554e3cf2bbd2195871f4fe RNVectorIcons: 3bf5f38dcb1aaf587c4101e9f3fcad5c8f5a88b2 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 - SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57 + SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c segment-analytics-react-native: 6f98edf18246782ee7428c5380c6519a3d2acf5e Sentry: 2cbbe3592f30050c60e916c63c7f5a2fa584005e SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 sovran-react-native: e4721a564ee6ef5b5a0d901bc677018cf371ea01 - VeriffSDK: 4323cc7d0152c107f40795881c92a41cf5a80f29 VisionCamera: f56eaedde0d3fa095143b78374d29e89e71735f9 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a YttriumWrapper: cbddb60c835ebc4232d9f57064084ab30686a18e diff --git a/jest.config.js b/jest.config.js index 9eaa1b8901b..de7a67804c0 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))', + '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))', ], transform: { '^.+\\.[jt]sx?$': ['babel-jest', { configFile: './babel.config.tests.js' }], diff --git a/package.json b/package.json index 1411b231f28..90abdc8bcc8 100644 --- a/package.json +++ b/package.json @@ -338,7 +338,6 @@ "@types/he": "^1.2.3", "@types/react-test-renderer": "^18.0.0", "@types/readable-stream": "^2.3.9", - "@veriff/react-native-sdk": "^11.2.0", "@viem/anvil": "^0.0.10", "@walletconnect/core": "^2.23.0", "@walletconnect/react-native-compat": "^2.23.0", diff --git a/yarn.lock b/yarn.lock index e72241965c2..cb7ebdcdf5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19747,16 +19747,6 @@ __metadata: languageName: node linkType: hard -"@veriff/react-native-sdk@npm:^11.2.0": - version: 11.2.0 - resolution: "@veriff/react-native-sdk@npm:11.2.0" - peerDependencies: - react: ^19.1.0 - react-native: ^0.81.0 - checksum: 10/39f2f546db15a1360cef131d868143462610555413410df669b6266c1d28f42f82c675c1ff1f3ce060f2ed9fba6166a2332e2c2e64c9dc976ff7b28feec8434b - languageName: node - linkType: hard - "@viem/anvil@npm:^0.0.10": version: 0.0.10 resolution: "@viem/anvil@npm:0.0.10" @@ -35789,7 +35779,6 @@ __metadata: "@types/valid-url": "npm:^1.0.4" "@typescript-eslint/eslint-plugin": "npm:^7.10.0" "@typescript-eslint/parser": "npm:^7.10.0" - "@veriff/react-native-sdk": "npm:^11.2.0" "@viem/anvil": "npm:^0.0.10" "@walletconnect/core": "npm:^2.23.0" "@walletconnect/react-native-compat": "npm:^2.23.0" From 2757e5591ffd86c57b08148a28672b1838e9ab0d Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Fri, 27 Feb 2026 21:56:10 +0000 Subject: [PATCH 063/131] [skip ci] Bump version number to 3843 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1088502c8d5..7ab2b94773b 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.67.0" - versionCode 3842 + versionCode 3843 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 77a2900ec2c..9c11a62bb77 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3842 + VERSION_NUMBER: 3843 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3842 + FLASK_VERSION_NUMBER: 3843 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 07e762cf190..dd4cc9b8838 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3842; + CURRENT_PROJECT_VERSION = 3843; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3842; + CURRENT_PROJECT_VERSION = 3843; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3842; + CURRENT_PROJECT_VERSION = 3843; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3842; + CURRENT_PROJECT_VERSION = 3843; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3842; + CURRENT_PROJECT_VERSION = 3843; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3842; + CURRENT_PROJECT_VERSION = 3843; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From b1c3e3e9b847fe9890063dac400db8686ddc9818 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Fri, 27 Feb 2026 22:03:50 +0000 Subject: [PATCH 064/131] [skip ci] Bump version number to 3844 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7ab2b94773b..532cbb43739 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.67.0" - versionCode 3843 + versionCode 3844 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 9c11a62bb77..f0f5aa5dccf 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.0 - opts: is_expand: false - VERSION_NUMBER: 3843 + VERSION_NUMBER: 3844 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3843 + FLASK_VERSION_NUMBER: 3844 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index dd4cc9b8838..71640427dcf 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3843; + CURRENT_PROJECT_VERSION = 3844; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3843; + CURRENT_PROJECT_VERSION = 3844; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3843; + CURRENT_PROJECT_VERSION = 3844; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3843; + CURRENT_PROJECT_VERSION = 3844; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3843; + CURRENT_PROJECT_VERSION = 3844; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3843; + CURRENT_PROJECT_VERSION = 3844; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 0c239919032de027a049fd35a1f8234f1840ccc2 Mon Sep 17 00:00:00 2001 From: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:22:23 -0330 Subject: [PATCH 065/131] release: release-changelog/7.67.0 (#26313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates the change log for 7.67.0. (Hotfix - no test plan generated.) --------- Co-authored-by: metamaskbot Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> --- CHANGELOG.md | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b07aa287c25..77836eec4e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,138 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.67.0] + +### Uncategorized + +- chore: merge stable into release 7.67.0 branch (#26496) +- chore: make OTA Version Display more robust (#26295) +- Bump new assets controller to v2.0.0 (#26166) +- Updated assets controllers to 99.4.0 (#26261) +- Added code fencing for gh actions defined at builds.yml (#26159) +- Fixed OTA version display (#26204) +- Remove npx in favor of yarn in sync script (#26233) +- Remove opt out button from Rewards settings (#26189) +- Removed notifications for all swap/bridge txs (#25919) +- Use chain-agnostic gas fee estimates source for bridging (#26047) +- Stop using portfolio API to fetch contentful sites (#26003) +- chore(release): sync stable to main for version 7.66.0 (#25916) +- Updated OTA modal user interface (#25867) +- Adds FAST_NETWORKS filter for gas-speed component and returns " < 1 Sec" when speed is < 1000ms (#25825) + Modifies toHumanEstimatedTimeRange in utils/time.ts to + handle "fast network" filter and " < 1 Sec" display +- Use `StorageService` in Snap Controller (#25672) +- Fixed the limit price row in Perps order form so it no longer shows rounded bottom borders when the Pay with row is visible (#25834) + below it. +- Replace modal with bottom sheet on 'Account added' click (#25770) +- chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#25696) +- chore(release): sync stable to main for version 7.66.0 (#25802) + +### Added + +- Gas sponsorship UI (#26252) +- Replaced webview-based Veriff KYC flow with native Veriff SDK integration, featuring MetaMask-branded UI with dynamic (#26138) + light/dark theme support, custom fonts, and fox logo +- Add market close bottom sheet to stop user perform trade. (#25157) +- Added trust signal icons to address displays in confirmations, showing verified, warning, or malicious indicators based (#25154) + on address scan results. +- Moved notifications and QR scanner from home screen header to Account Menu and added Deposit quick action (#26100) +- Added card freeze/unfreeze toggle to the Card Home screen, allowing users to temporarily disable and re-enable their card. (#26246) +- `[ADDED]` Native Transak v2 purchase flow with in-app email/OTP authentication, KYC handling, order creation, and payment (#26033) + processing. +- Force enable explore feature (#26128) +- Add support for wallet connect verify api (#26070) +- Increased the browser tab limit from 5 to 20 and improved tab switching performance by keeping only the 5 most recently (#26143) + used tabs live in memory +- Prefill country of residence from geolocation on Card onboarding SignUp and extract reusable SelectField component across (#26136) + onboarding screens +- Predict withdrawal to any token (happy path) (#25441) +- Display amount row when simulation fails (#25716) +- Improved claim bonus responsiveness by caching Merkl API responses and fixed claim bonus button in token list V2 layout (#26016) +- Preloaded Perps market and user data at startup for instant rendering (#26061) +- Added network pill overflow with "+X more" button that opens a full network list in the bridge token selector (#25893) +- Revamp swaps keypad (#25845) +- Init the new assets controller under a feature flag (#25957) +- Adds a page for changing preferred ramp provider (#25860) +- Add asset overview deeplinks (#25447) +- Restored the previously selected "Pay with" token when returning to the Perps order view within 5 minutes. (#25938) +- Fixed predict transaction toast notifications not appearing when navigating away from the Predict tab (#25863) +- Added new Accounts Menu screen to organize settings navigation with Settings, Manage, and Resources sections (#25611) +- Adds Bridge and Swap feature to `MegaETH` (#25906) +- Adds chiliz.png as network logo and enables it in metamask mobile (#25437) +- Always display learn more about perps link (#25958) +- Created new token list item v2 (#25824) +- Added custom claim transaction request screen for mUSD bonus claims with improved UX flow (#25837) +- Added an "Ending soon" tab to prediction markets feed showing markets sorted by end date (#25868) +- Removed legacy homepage script injection and related RPC methods (#25620) +- Add google/web search inside browser search bar (#25897) +- Homogenize spacing on Explore page for perps items (#25894) +- Added 1st interaction alert to warn users when interacting with an address for the first time. (#25575) +- Added icons to the bridge token selector network pills (#25851) +- Create feature flag for the new unified assets state (#25891) +- Adds Bridge and Swap feature to HyperEVM (#25769) +- Added lightweight position display and one-click Long/Short trading on token details page for perps-enabled assets (#25685) +- Improved browser tab switching performance by keeping tabs mounted (#25702) +- Validation errors from non-EVM transaction snaps will now be displayed to users during send flow. (#25648) +- Added detailed transaction display for mUSD reward claims showing claimed amount, network fee, and received total (#25452) +- Adds functionality for selecting a payment method (#25681) +- Base setup for in-app provisioning (#25669) + +### Fixed + +- Adds analytics instrumentation for Token Details V2 layout A/B test (#25844) +- Fix issues with balance rounding, localization formatting and decimal representation on source swap asset balance. (#26267) +- Keypad bottom border is visible occasionally on Android (#26229) +- Adds location property to swap events. (#26067) +- Fixed a bug where add/remove network confirmation toasts appeared during Bridge flows. (#26239) +- Fixed an iOS bug where scanning a MetaMask universal link QR code opened Safari and redirected to the App Store instead of (#25739) + handling the link in-app. +- Fixed DeFi tab not appearing when switching from non-EVM networks to "All Popular Networks" (#26193) +- Set height of quick pick buttons the same as confirm cta (#26170) +- Fixed Bridge token selectors to show all supported networks, persist selected network pills, and auto-add missing networks (#26174) + on token selection. +- Fixed token prices not displaying for non-EVM tokens in the V2 token list layout. (#26132) +- Prevent full app reload when editing Trending files (#26135) +- Fixed excessive ENS API calls when opening the bridge/swaps flow that scaled with the number of accounts (#26126) +- Fixed a bug where tapping a token’s info icon in Swaps could open the wrong asset details page. (#26123) +- Fixed perpetual trading margin display showing $0 when placing orders from the Token Details page (#26105) +- Start rendering confirm button loading state on input change (#26107) +- Fixed issue that triggered account creation during onboarding using pre BIP-44 flow when switching networks (#26088) +- Fixed a UI issue where buttons in the signature message details view were overlapping. (#26040) +- Keep keypad state on flip and close it when dest token input is pressed (#26068) +- Updated mUSD claim bonus subtitle copy (#26019) +- Fixed intermittent placeholder text alignment and clipping in text inputs on iOS. (#26049) +- Fall back to priceImpact or destTokenAmount for swap quote sorting (#25928) +- Remove deeplink interstitial on dApp deeplinks (#25963) +- Multiple fixes on import token flow (#25962) +- Fixed decimal precision calculation for Tron's staked balance (#25430) +- Fixed intermittent "Failed to fetch market data" errors on Perps by switching market data fetches from WebSocket to HTTP (#26014) + transport +- Fixed `x-us-env` header being incorrectly set to `false` for US Card users when geolocation requests fail (#25971) +- Fix #24546 with human readable message (#25555) +- Removed "Add funds to start trading perps" banner from Perps market details and allow opening trades (Long/Short) when perps (#25960) + balance is zero. +- Fixed long token names pushing balance off screen in Send flow and MM Pay token picker (#25338) +- Fix #25693 styling issue in for ledger devices (#25758) +- Fixed navigation error and token buyability checks when purchasing crypto with cash using unified buy V2 (#25617) +- Fixed Predictions tab not hiding monetary values when privacy mode is enabled (#25887) +- Fixed Perps deposit+order flow so the pending deposit toast auto-dismisses after a few seconds and the "deposit taking longer" (#25939) + message appears after 30 seconds. +- Fixed header height to scale properly with larger accessibility font sizes (#25855) +- Activity header symbol fallback (#25821) +- Fixed the Perps order pay row not appearing until margin was loaded. (#25836) +- When passoword oudated, it navigate to oauthRehydrate screen when reopen app (#25687) +- Fixed notification and transaction display for EIP-7702 transactions without nonces (#25646) +- Adds event for when token details page is opened. (#25780) +- Added error screens when wallet creation fails, allowing users to retry or contact support instead of being redirected (#25564) + to login. +- Remove toggle switch from login screen (#25424) +- Fixed minor button layout issues (#25771) +- Fixed long account names overflowing in the Deposit Buy screen by enabling proper text truncation (#25715) +- Remove subtitle in token details (#25726) +- Fixed flow for "Cash buy X" button on the new token details layout (#25719) +- Pass assetID to the on ramp buy screen. (#25709) + ## [7.66.1] ### Fixed @@ -10621,7 +10753,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.66.1...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.67.0...HEAD +[7.67.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.66.1...v7.67.0 [7.66.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.66.0...v7.66.1 [7.66.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.65.0...v7.66.0 [7.65.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.64.1...v7.65.0 From c4acb0b62642314c4553bf2bbe9334782d65be4a Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Mon, 2 Mar 2026 16:03:19 +0000 Subject: [PATCH 066/131] bump semvar version to 7.67.1 && build version to 3854 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 532cbb43739..264825108e8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -187,7 +187,7 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionName "7.67.0" + versionName "7.67.1" versionCode 3844 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bitrise.yml b/bitrise.yml index f0f5aa5dccf..9072b94fa78 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3516,13 +3516,13 @@ app: PROJECT_LOCATION_IOS: ios - opts: is_expand: false - VERSION_NAME: 7.67.0 + VERSION_NAME: 7.67.1 - opts: is_expand: false VERSION_NUMBER: 3844 - opts: is_expand: false - FLASK_VERSION_NAME: 7.67.0 + FLASK_VERSION_NAME: 7.67.1 - opts: is_expand: false FLASK_VERSION_NUMBER: 3844 diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 71640427dcf..12e75ad47a4 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1319,7 +1319,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1385,7 +1385,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1454,7 +1454,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1518,7 +1518,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1684,7 +1684,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.1; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -1751,7 +1751,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.1; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index 90abdc8bcc8..b6a461aeea4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "7.67.0", + "version": "7.67.1", "private": true, "scripts": { "install:foundryup": "yarn mm-foundryup", From 35d793b7e1d2e4f30eac824eb2833396a3c78be9 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:29:56 +0000 Subject: [PATCH 067/131] chore(runway): cherry-pick chore: Bump `json-rpc-engine` cp-7.68.0 (#26796) - chore: Bump `json-rpc-engine` cp-7.68.0 (#26777) ## **Description** Bump `json-rpc-engine` to the latest version which includes a fix for a bug that caused issues when creating Snap accounts. ## **Changelog** CHANGELOG entry: null --- > [!NOTE] > **Low Risk** > Low risk dependency bump limited to `@metamask/json-rpc-engine`; behavior changes are confined to upstream library updates and could only surface via JSON-RPC request/response handling regressions. > > **Overview** > Updates the `@metamask/json-rpc-engine` dependency from `^10.2.1` to `^10.2.3` and refreshes `yarn.lock` to resolve to `10.2.3`. > > No application code changes; this is a dependency-only update intended to pick up upstream fixes (notably around Snap account creation per PR description). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9c848f4aca54efb9b1a40291f530644272d583cf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [67269fe](https://github.com/MetaMask/metamask-mobile/commit/67269fe0bdf9a653f48b6ab92085a8dc1a6facac) Co-authored-by: Frederik Bolding --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 93a15f5fe2a..eb696f97f17 100644 --- a/package.json +++ b/package.json @@ -234,7 +234,7 @@ "@metamask/gas-fee-controller": "^25.0.0", "@metamask/gator-permissions-controller": "^0.3.0", "@metamask/hw-wallet-sdk": "^0.4.0", - "@metamask/json-rpc-engine": "^10.2.1", + "@metamask/json-rpc-engine": "^10.2.3", "@metamask/json-rpc-middleware-stream": "^8.0.7", "@metamask/key-tree": "patch:@metamask/key-tree@npm%3A10.1.1#~/.yarn/patches/@metamask-key-tree-npm-10.1.1-0bfab435ac.patch", "@metamask/keyring-api": "^21.5.0", diff --git a/yarn.lock b/yarn.lock index 7cd9637d686..849ccebbbbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8713,9 +8713,9 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@npm:^10.2.0, @metamask/json-rpc-engine@npm:^10.2.1, @metamask/json-rpc-engine@npm:^10.2.2": - version: 10.2.2 - resolution: "@metamask/json-rpc-engine@npm:10.2.2" +"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@npm:^10.2.0, @metamask/json-rpc-engine@npm:^10.2.1, @metamask/json-rpc-engine@npm:^10.2.2, @metamask/json-rpc-engine@npm:^10.2.3": + version: 10.2.3 + resolution: "@metamask/json-rpc-engine@npm:10.2.3" dependencies: "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/safe-event-emitter": "npm:^3.0.0" @@ -8723,7 +8723,7 @@ __metadata: "@types/deep-freeze-strict": "npm:^1.1.0" deep-freeze-strict: "npm:^1.1.1" klona: "npm:^2.0.6" - checksum: 10/e2449e80f8ca3aed58d0778c220eba6c98e0848359da2703bcb68879c1b315774a1a8a90b2a7cd8d3eb3e0f022f9d0e30503e75a2645ec32cbfc5ba2e537f807 + checksum: 10/8895ffcfc0dbf5542476dfd9771cb288feaf6fd7e9628e02c10232b3b8f0feabe3a0ad3e3480e3260a69aaafcf8f58d1d89410e7f43e97a08350b3ec3e767b1d languageName: node linkType: hard @@ -35411,7 +35411,7 @@ __metadata: "@metamask/gas-fee-controller": "npm:^25.0.0" "@metamask/gator-permissions-controller": "npm:^0.3.0" "@metamask/hw-wallet-sdk": "npm:^0.4.0" - "@metamask/json-rpc-engine": "npm:^10.2.1" + "@metamask/json-rpc-engine": "npm:^10.2.3" "@metamask/json-rpc-middleware-stream": "npm:^8.0.7" "@metamask/key-tree": "patch:@metamask/key-tree@npm%3A10.1.1#~/.yarn/patches/@metamask-key-tree-npm-10.1.1-0bfab435ac.patch" "@metamask/keyring-api": "npm:^21.5.0" From eefde10780f06a5b9b0098c81fc6e448783d934f Mon Sep 17 00:00:00 2001 From: Wei Sun Date: Mon, 2 Mar 2026 09:30:37 -0800 Subject: [PATCH 068/131] revert version number back to 7.67.0 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 264825108e8..532cbb43739 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -187,7 +187,7 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionName "7.67.1" + versionName "7.67.0" versionCode 3844 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bitrise.yml b/bitrise.yml index 9072b94fa78..f0f5aa5dccf 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3516,13 +3516,13 @@ app: PROJECT_LOCATION_IOS: ios - opts: is_expand: false - VERSION_NAME: 7.67.1 + VERSION_NAME: 7.67.0 - opts: is_expand: false VERSION_NUMBER: 3844 - opts: is_expand: false - FLASK_VERSION_NAME: 7.67.1 + FLASK_VERSION_NAME: 7.67.0 - opts: is_expand: false FLASK_VERSION_NUMBER: 3844 diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 12e75ad47a4..71640427dcf 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1319,7 +1319,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.1; + MARKETING_VERSION = 7.67.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1385,7 +1385,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.1; + MARKETING_VERSION = 7.67.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1454,7 +1454,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.1; + MARKETING_VERSION = 7.67.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1518,7 +1518,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.1; + MARKETING_VERSION = 7.67.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1684,7 +1684,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.1; + MARKETING_VERSION = 7.67.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -1751,7 +1751,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.1; + MARKETING_VERSION = 7.67.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index b6a461aeea4..90abdc8bcc8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "7.67.1", + "version": "7.67.0", "private": true, "scripts": { "install:foundryup": "yarn mm-foundryup", From 6e7c0ff39854a1f0d400058d22ecfc432a8d2125 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:30:45 +0000 Subject: [PATCH 069/131] chore(runway): cherry-pick fix: recipient list display in send flow cp-7.68.0 (#26788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: recipient list display in send flow cp-7.68.0 (#26771) ## **Description** Fix recipient list display in send flow. ## **Changelog** CHANGELOG entry: ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/26684 ## **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** Screenshot 2026-03-02 at 5 04 59 PM ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I've included tests if applicable - [X] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Low risk UI-only change that swaps the row wrapper component and tweaks padding/accessibility; main risk is minor touch/spacing regressions in the recipient list. > > **Overview** > Fixes the recipient list row rendering in the send/confirmation flow by replacing the design-system `ButtonBase` wrapper with React Native `Pressable`. > > Updates the row styling to include horizontal padding (`px-4`) and explicitly sets `accessibilityRole="button"` while preserving the existing pressed/selected background behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e5b33b4e7cd874d52ee1358712480dcc9d57cbd4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [c859ce0](https://github.com/MetaMask/metamask-mobile/commit/c859ce032042e2c23e70c9d7159fa4235cd1bd73) Co-authored-by: Jyoti Puri --- .../confirmations/components/UI/recipient/recipient.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/components/Views/confirmations/components/UI/recipient/recipient.tsx b/app/components/Views/confirmations/components/UI/recipient/recipient.tsx index f26cdda96f0..13b7d4ca873 100644 --- a/app/components/Views/confirmations/components/UI/recipient/recipient.tsx +++ b/app/components/Views/confirmations/components/UI/recipient/recipient.tsx @@ -1,11 +1,11 @@ import React, { useCallback } from 'react'; +import { Pressable } from 'react-native'; import { KeyringAccountType } from '@metamask/keyring-api'; import { Box, FontWeight, Text, TextVariant, - ButtonBase, } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; @@ -73,7 +73,7 @@ export function Recipient({ ACCOUNT_TYPE_LABELS[recipient.accountType as KeyringAccountType]; return ( - tw.style( - 'w-full flex-row items-center justify-between h-18 rounded-none', + 'w-full flex-row items-center justify-between h-18 rounded-none px-4', pressed || isSelected ? 'bg-pressed' : 'bg-transparent', ) } onPress={handlePressRecipient} + accessibilityRole="button" > @@ -120,6 +121,6 @@ export function Recipient({ - + ); } From d0be88d9a5c804e347fb1b59e288c959a5628863 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Mon, 2 Mar 2026 17:32:42 +0000 Subject: [PATCH 070/131] [skip ci] Bump version number to 3855 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index b66555641ef..1ed86b4b054 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.68.0" - versionCode 3837 + versionCode 3855 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index d624b046ece..bcf72eb99c8 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.68.0 - opts: is_expand: false - VERSION_NUMBER: 3837 + VERSION_NUMBER: 3855 - opts: is_expand: false FLASK_VERSION_NAME: 7.68.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3837 + FLASK_VERSION_NUMBER: 3855 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index e6d7e243028..096fbcd4845 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3837; + CURRENT_PROJECT_VERSION = 3855; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3837; + CURRENT_PROJECT_VERSION = 3855; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3837; + CURRENT_PROJECT_VERSION = 3855; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3837; + CURRENT_PROJECT_VERSION = 3855; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3837; + CURRENT_PROJECT_VERSION = 3855; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3837; + CURRENT_PROJECT_VERSION = 3855; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From d34662285a800d720b891f5ba25cee2872b5ee19 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:55:09 +0000 Subject: [PATCH 071/131] chore(runway): cherry-pick fix(perps): recover connection after app state changes (#26809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(perps): recover connection after app state changes cp-7.67.1 (#26780) ## **Description** Fixes Perps WebSocket connectivity issues when: 1. **App returns from background** — after a few minutes in background, the OS silently kills the WebSocket but `PerpsConnectionManager` still reports `isConnected = true`. No code path detected the stale connection or triggered reconnection. 2. **WiFi/network drops and restores** — toggling WiFi, airplane mode, or losing cellular signal kills the WebSocket, but since the app stays in `active` state, the existing `AppState`-based recovery (if any) never fires. ### Root Cause `PerpsConnectionManager` had no lifecycle awareness of: - **React Native `AppState` transitions** (background → foreground) - **Network connectivity changes** (offline → online via `@react-native-community/netinfo`) Arthur's prior fix ([#26334](https://github.com/MetaMask/metamask-mobile/pull/26334)) made `StreamChannel.ensureReady()` connection-aware to avoid blind polling on slow connections, but it only helps when a reconnection is **already in progress** (`isConnecting = true`). After background resume or WiFi restore, nobody triggers the reconnection in the first place. ### Fix - **`AppState` listener** — on `active`, cancels any pending grace period and runs `validateAndReconnect()` - **`NetInfo` listener** — tracks `wasOffline` state; on offline → online transition, runs `validateAndReconnect()` - **`validateAndReconnect(context)`** — shared method that sends a lightweight `ping()` health check to the active provider. If the ping fails (stale WebSocket), marks the connection as lost and triggers `reconnectWithNewContext({ force: true })` which reinitializes the controller, validates with a fresh health check, and preloads all stream subscriptions. - **Cleanup** — both listeners are properly removed in `cleanupStateMonitoring()` ## **Changelog** CHANGELOG entry: Fixed Perps WebSocket not reconnecting after app resume from background or WiFi/network toggle ## **Related issues** Fixes: connectivity loss after backgrounding app, WiFi off/on not recovering Perps data ## **Manual testing steps** ```gherkin Feature: Perps connection recovery Scenario: App returns from background after several minutes Given the user has navigated to the Perps trading screen And the user has an open position When the user backgrounds the app for 3+ minutes And the user returns to the app Then the Perps WebSocket reconnects automatically And positions, prices, and account data resume updating Scenario: WiFi is toggled off and back on Given the user is viewing live Perps positions And WiFi is connected When the user turns WiFi off And waits a few seconds And turns WiFi back on Then the Perps WebSocket reconnects after network is restored And live data resumes without requiring navigation away Scenario: Airplane mode is toggled Given the user is on the Perps trading screen When the user enables airplane mode And then disables airplane mode Then the connection recovers and live data resumes ``` ## **Screenshots/Recordings** ### **Before** After backgrounding or WiFi toggle, Perps shows stale data with no automatic recovery. User must navigate away and back to restore the connection. ### **After** Connection automatically recovers via health-check ping and force reconnection. Live data resumes within seconds. ## **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 - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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 Perps connection lifecycle and reconnection paths; regressions could cause reconnect loops or delayed/stuck loading during flaky connectivity. > > **Overview** > Improves Perps WebSocket resilience by adding AppState and NetInfo listeners in `PerpsConnectionManager` to detect background→foreground and offline→online transitions, validate the connection via provider `ping()`, and force a reconnect when stale. > > Adds network-restore retry/backoff knobs (`NetworkRestoreMaxRetries`, `NetworkRestoreRetryBaseMs`) and ensures cleanup of new subscriptions/timers on teardown; reconnection now explicitly calls `PerpsController.disconnect()` before `init()` to avoid skipping re-init on a dead socket. > > Updates `usePerpsHomeData` to treat WebSocket-backed sections (positions/orders/activity) as loading while `isConnecting`, preventing brief empty-state flashes during reconnection. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b3aab14cd5c66e00f6cb80762f5f7b67f2ccbfe6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [c4f83e4](https://github.com/MetaMask/metamask-mobile/commit/c4f83e4148417219ccde8dc0f59442d46aca07de) Co-authored-by: Alejandro Garcia Anglada --- .../UI/Perps/hooks/usePerpsHomeData.ts | 10 +- .../Perps/services/PerpsConnectionManager.ts | 197 ++++++++++++++++++ .../perps/constants/perpsConfig.ts | 2 + 3 files changed, 205 insertions(+), 4 deletions(-) diff --git a/app/components/UI/Perps/hooks/usePerpsHomeData.ts b/app/components/UI/Perps/hooks/usePerpsHomeData.ts index c1060a82c6e..e8fabae5051 100644 --- a/app/components/UI/Perps/hooks/usePerpsHomeData.ts +++ b/app/components/UI/Perps/hooks/usePerpsHomeData.ts @@ -63,7 +63,7 @@ export const usePerpsHomeData = ({ searchQuery = '', }: UsePerpsHomeDataParams = {}): UsePerpsHomeDataReturn => { // Get connection state to guard REST calls that require an initialized controller - const { isConnected, isInitialized } = usePerpsConnection(); + const { isConnected, isInitialized, isConnecting } = usePerpsConnection(); // Fetch positions via WebSocket with throttling for performance const { positions, isInitialLoading: isPositionsLoading } = @@ -365,12 +365,14 @@ export const usePerpsHomeData = ({ recentActivity: limitedActivity, sortBy, isLoading: { - positions: isPositionsLoading, - orders: isOrdersLoading, + // During reconnection, treat WebSocket-backed data as loading so the UI + // shows skeletons instead of briefly flashing "no positions" → positions. + positions: isPositionsLoading || isConnecting, + orders: isOrdersLoading || isConnecting, markets: isMarketsLoading, // Only wait for WebSocket fills (fast ~100ms), not REST fills (slow 3s+) // REST fills merge in background via mergedFills without blocking initial render - activity: isFillsLoading, + activity: isFillsLoading || isConnecting, }, refresh, }; diff --git a/app/components/UI/Perps/services/PerpsConnectionManager.ts b/app/components/UI/Perps/services/PerpsConnectionManager.ts index dff4bf9695b..4632db78eb2 100644 --- a/app/components/UI/Perps/services/PerpsConnectionManager.ts +++ b/app/components/UI/Perps/services/PerpsConnectionManager.ts @@ -1,3 +1,8 @@ +import { AppState, type AppStateStatus } from 'react-native'; +import { + addEventListener as netInfoAddEventListener, + type NetInfoState, +} from '@react-native-community/netinfo'; import { captureException, setMeasurement } from '@sentry/react-native'; import BackgroundTimer from 'react-native-background-timer'; import performance from 'react-native-performance'; @@ -57,6 +62,13 @@ class PerpsConnectionManagerClass { private isInGracePeriod = false; private pendingReconnectPromise: Promise | null = null; private connectionTimeoutRef: ReturnType | null = null; + private appStateSubscription: ReturnType< + typeof AppState.addEventListener + > | null = null; + private netInfoUnsubscribe: (() => void) | null = null; + private wasOffline = false; + private networkRestoreRetryTimer: ReturnType | null = null; + private networkRestoreRetryCount = 0; private constructor() { // Private constructor to enforce singleton pattern @@ -178,13 +190,188 @@ class PerpsConnectionManagerClass { this.previousHip3Version = currentHip3Version; }); + // Listen for app state changes to reconnect after background + if (!this.appStateSubscription) { + this.appStateSubscription = AppState.addEventListener( + 'change', + (nextAppState: AppStateStatus) => { + this.handleAppStateChange(nextAppState).catch((error) => { + Logger.error( + ensureError(error, 'PerpsConnectionManager.appStateListener'), + { + tags: { feature: PERPS_CONSTANTS.FeatureName }, + context: { + name: 'PerpsConnectionManager.appStateListener', + data: { message: 'Error handling app state change' }, + }, + }, + ); + }); + }, + ); + } + + // Listen for connectivity changes (WiFi off/on, airplane mode, etc.) + if (!this.netInfoUnsubscribe) { + this.netInfoUnsubscribe = netInfoAddEventListener( + (netState: NetInfoState) => { + const isOnline = netState.isInternetReachable === true; + + if (isOnline && this.wasOffline) { + DevLogger.log( + 'PerpsConnectionManager: Network restored - validating connection', + ); + this.reconnectAfterNetworkRestore(); + } + + if (!isOnline) { + this.wasOffline = true; + } else if (isOnline && this.isConnected) { + this.wasOffline = false; + } + }, + ); + } + DevLogger.log('PerpsConnectionManager: State monitoring set up'); } + /** + * Validate the WebSocket connection and force-reconnect if it is stale. + * Shared by both AppState (background→foreground) and NetInfo (offline→online) + * recovery paths. + * + * @param context - Caller identifier for error reporting + * @param skipPing - Skip ping and force reconnect after known network loss + */ + private async validateAndReconnect( + context: string, + skipPing = false, + ): Promise { + if (this.connectionRefCount <= 0 || this.isConnecting) { + return; + } + + if (this.isConnected && !skipPing) { + try { + const provider = Engine.context.PerpsController.getActiveProvider(); + await provider.ping(); + DevLogger.log( + `PerpsConnectionManager: ${context} - connection healthy`, + ); + return; + } catch { + DevLogger.log( + `PerpsConnectionManager: ${context} - connection stale, triggering reconnection`, + ); + } + } + + this.isConnected = false; + this.isInitialized = false; + + await this.reconnectWithNewContext({ force: true }); + DevLogger.log( + `PerpsConnectionManager: ${context} - reconnection successful`, + ); + } + + /** + * Attempt reconnection after network restore with exponential backoff. + * WebSocket endpoints may not be reachable immediately even though + * NetInfo reports isInternetReachable: true. + */ + private reconnectAfterNetworkRestore(): void { + this.cancelNetworkRestoreRetry(); + this.networkRestoreRetryCount = 0; + this.attemptNetworkRestoreReconnect(); + } + + private attemptNetworkRestoreReconnect(): void { + this.validateAndReconnect('PerpsConnectionManager.netInfoListener', true) + .then(() => { + this.wasOffline = false; + this.networkRestoreRetryCount = 0; + }) + .catch((error) => { + this.networkRestoreRetryCount++; + if ( + this.networkRestoreRetryCount < + PERPS_CONSTANTS.NetworkRestoreMaxRetries + ) { + const delay = + PERPS_CONSTANTS.NetworkRestoreRetryBaseMs * + this.networkRestoreRetryCount; + DevLogger.log( + `PerpsConnectionManager: Network restore retry ${this.networkRestoreRetryCount} in ${delay}ms`, + ); + this.networkRestoreRetryTimer = setTimeout(() => { + this.networkRestoreRetryTimer = null; + this.attemptNetworkRestoreReconnect(); + }, delay); + } else { + Logger.error( + ensureError(error, 'PerpsConnectionManager.netInfoListener'), + { + tags: { feature: PERPS_CONSTANTS.FeatureName }, + context: { + name: 'PerpsConnectionManager.netInfoListener', + data: { + message: + 'Network restore reconnection failed after max retries', + }, + }, + }, + ); + } + }); + } + + private cancelNetworkRestoreRetry(): void { + if (this.networkRestoreRetryTimer) { + clearTimeout(this.networkRestoreRetryTimer); + this.networkRestoreRetryTimer = null; + } + this.networkRestoreRetryCount = 0; + } + + /** + * Handle app state transitions to recover from stale WebSocket connections. + * When the app returns from background, the OS may have silently killed the + * WebSocket. A health-check ping detects this and triggers reconnection. + */ + private async handleAppStateChange( + nextAppState: AppStateStatus, + ): Promise { + if (nextAppState !== 'active') { + return; + } + + // Cancel any pending grace period — user is back + if (this.isInGracePeriod) { + DevLogger.log( + 'PerpsConnectionManager: App resumed - cancelling grace period', + ); + this.cancelGracePeriod(); + } + + await this.validateAndReconnect('appResume'); + } + /** * Clean up state monitoring */ private cleanupStateMonitoring(): void { + if (this.appStateSubscription) { + this.appStateSubscription.remove(); + this.appStateSubscription = null; + } + if (this.netInfoUnsubscribe) { + this.netInfoUnsubscribe(); + this.netInfoUnsubscribe = null; + this.wasOffline = false; + } + this.cancelNetworkRestoreRetry(); if (this.unsubscribeFromStore) { this.unsubscribeFromStore(); this.unsubscribeFromStore = null; @@ -747,7 +934,17 @@ class PerpsConnectionManagerClass { this.clearError(); // Stage 2: Force the controller to reinitialize with new context + // Disconnect first so the controller resets its isInitialized flag — + // without this, init() silently skips when the controller thinks it's + // already initialized (even though the underlying WebSocket is dead). const reinitStart = performance.now(); + try { + await Engine.context.PerpsController.disconnect(); + } catch { + DevLogger.log( + 'PerpsConnectionManager: disconnect before reinit failed (continuing)', + ); + } await Engine.context.PerpsController.init(); setMeasurement( PerpsMeasurementName.PerpsControllerReinit, diff --git a/app/controllers/perps/constants/perpsConfig.ts b/app/controllers/perps/constants/perpsConfig.ts index c12fe232d0e..d19c3184b20 100644 --- a/app/controllers/perps/constants/perpsConfig.ts +++ b/app/controllers/perps/constants/perpsConfig.ts @@ -30,6 +30,8 @@ export const PERPS_CONSTANTS = { ReconnectionDelayAndroidMs: 300, // Android-specific reconnection delay for better reliability on slower devices ReconnectionDelayIosMs: 100, // iOS-specific reconnection delay for optimal performance ReconnectionRetryDelayMs: 5_000, // 5 seconds delay between reconnection attempts + NetworkRestoreMaxRetries: 8, // Max retry attempts when reconnecting after WiFi/network restore + NetworkRestoreRetryBaseMs: 1_500, // Base delay (ms) between network restore retries (multiplied by attempt number) // Connection manager timing constants BalanceUpdateThrottleMs: 15000, // Update at most every 15 seconds to reduce state updates in PerpsConnectionManager From 8155e01575330e09c3a6f161f5ace1e83f005a22 Mon Sep 17 00:00:00 2001 From: Wei Sun Date: Mon, 2 Mar 2026 12:24:09 -0800 Subject: [PATCH 072/131] update OTA version --- app/constants/ota.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/constants/ota.ts b/app/constants/ota.ts index 70e0dd691f3..e43c9c02236 100644 --- a/app/constants/ota.ts +++ b/app/constants/ota.ts @@ -6,7 +6,7 @@ import otaConfig from '../../ota.config.js'; * Reset to v0 when releasing a new native build * We keep this OTA_VERSION here to because changes in ota.config.js will affect the fingerprint and break the workflow in Github Actions */ -export const OTA_VERSION: string = 'v7.65.1'; +export const OTA_VERSION: string = 'v7.67.1'; export const RUNTIME_VERSION = otaConfig.RUNTIME_VERSION; export const PROJECT_ID = otaConfig.PROJECT_ID; export const UPDATE_URL = otaConfig.UPDATE_URL; From dec6a1d5984ed0aaa01024ef2330fe7697b22fb6 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 03:39:49 +0000 Subject: [PATCH 073/131] chore(runway): cherry-pick feat(card): cp-7.68.0 Add View PIN option (#26767) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - feat(card): cp-7.68.0 Add View PIN option (#26646) ## **Description** Adds a "View PIN" option to the Card Home manage card section, allowing users to securely view their card PIN through a PCI-compliant image-based display. **Why**: Users need to retrieve their card PIN (e.g. for ATM use or in-store transactions). The PIN is never transmitted as plain text — it is rendered as an image via a time-limited, single-use secure token from the `POST /v1/card/pin/token` endpoint, ensuring PCI compliance. **What changed**: - **New SDK method** (`CardSDK.generateCardPinToken`): Calls `POST /v1/card/pin/token` with optional `customCss` for theming the PIN image. Mirrors the existing `generateCardDetailsToken` pattern with proper error handling. - **React Query integration**: New `cardQueries.pin` key factory and `pinTokenMutationFn` following the established React Query patterns from the codebase. - **`useCardPinToken` hook**: Wraps `useMutation` for PIN token generation. Automatically applies dark/light theme-aware `customCss` (background and text colors) so the PIN image matches the app appearance. - **`ViewPinBottomSheet` component**: Displays the PIN image in a bottom sheet with a skeleton loader and `CardScreenshotDeterrent` enabled to prevent screenshots of sensitive data. - **`CardHome` integration**: New `ManageCardListItem` for "View PIN" with biometric authentication gating (matching the "View Card Details" flow). Falls back to password bottom sheet with a PIN-specific description when biometrics are not configured. Visible for US users (all card types) and international users with non-virtual cards. - **Analytics**: Added `VIEW_PIN_BUTTON` action to `CardActions` enum, tracked via `CARD_BUTTON_CLICKED` event. - **Navigation**: Registered `CardViewPinModal` route and added the `ViewPinBottomSheet` screen to `CardModalsRoutes`. - **Tests**: Added tests across 5 files — SDK method tests, query layer tests, hook tests, bottom sheet snapshot/render tests, and 11 new CardHome integration tests covering visibility conditions, biometric auth flow, password fallback, and loading guards. ## **Changelog** CHANGELOG entry: Added "View PIN" option to the Card Home screen, allowing users to securely view their card PIN via biometric or password authentication. ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: View card PIN Scenario: View PIN button is visible for eligible users Given the user is authenticated with an active card And the user is a US user OR has a non-virtual (metal) card When the user navigates to the Card Home screen Then a "View PIN" option is displayed in the manage card section Scenario: View PIN button is hidden for international virtual card users Given the user is an international user with a virtual card When the user navigates to the Card Home screen Then the "View PIN" option is NOT displayed Scenario: View PIN with biometric authentication Given the user has biometric authentication configured When the user taps "View PIN" Then a biometric prompt is displayed And upon successful authentication, the card PIN is shown as an image in a bottom sheet And the PIN image matches the current theme (light/dark background) Scenario: View PIN with password fallback Given the user does NOT have biometric authentication configured When the user taps "View PIN" Then a password bottom sheet appears with the message "Enter your wallet password to view your card PIN." And upon entering the correct password, the card PIN is shown in a bottom sheet Scenario: View PIN biometric cancellation Given the user has biometric authentication configured When the user taps "View PIN" and cancels the biometric prompt Then no PIN is displayed and the user returns to Card Home Scenario: View PIN error handling Given the user taps "View PIN" and authentication succeeds When the PIN token request fails Then an error toast is shown with "Failed to load PIN. Please try again." Scenario: Screenshot prevention Given the card PIN bottom sheet is displayed When the user attempts to take a screenshot Then the screenshot deterrent is active and prevents capture of the PIN ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Adds a new authenticated flow to fetch and display a sensitive card PIN image via a new SDK endpoint and modal UI, with biometric/password gating and error handling. Risk is mainly around auth/error-state handling and the new network call/token lifecycle. > > **Overview** > Adds a new **“View PIN”** manage-card action on `CardHome`, shown only for eligible users (authenticated, has a card, not loading; US users or non-virtual cards), gated by `reauthenticate()` with a password-bottom-sheet fallback when biometrics aren’t configured and guarded against concurrent loads. > > Introduces PIN-token generation plumbing: `CardSDK.generateCardPinToken` calling `POST /v1/card/pin/token`, React Query `cardQueries.pin` + `useCardPinToken` (theme-aware `customCss`), plus a new `ViewPinBottomSheet` modal route (`Routes.CARD.MODALS.VIEW_PIN`) that renders the PIN image with a skeleton loader and `CardScreenshotDeterrent` enabled. > > Updates analytics (`CardActions.VIEW_PIN_BUTTON`), test IDs, and English strings, and adds comprehensive tests for the SDK/query/hook, the new bottom sheet (snapshot/render), and CardHome visibility/auth/error flows. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 514ae0c60c2eebb4d3d7e1d2aaef334797c503b5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [ab07f46](https://github.com/MetaMask/metamask-mobile/commit/ab07f46934834c3a794b92e17f567b99ca8dffcd) Co-authored-by: Bruno Nascimento --- .../UI/Card/Views/CardHome/CardHome.test.tsx | 330 +++++++++ .../Card/Views/CardHome/CardHome.testIds.ts | 1 + .../UI/Card/Views/CardHome/CardHome.tsx | 106 +++ .../ViewPinBottomSheet.test.tsx | 140 ++++ .../ViewPinBottomSheet.testIds.ts | 5 + .../ViewPinBottomSheet/ViewPinBottomSheet.tsx | 80 +++ .../ViewPinBottomSheet.test.tsx.snap | 626 ++++++++++++++++++ .../components/ViewPinBottomSheet/index.ts | 2 + .../UI/Card/hooks/useCardPinToken.test.ts | 168 +++++ .../UI/Card/hooks/useCardPinToken.ts | 56 ++ app/components/UI/Card/queries/index.ts | 6 +- app/components/UI/Card/queries/pin.test.ts | 56 ++ app/components/UI/Card/queries/pin.ts | 14 + app/components/UI/Card/routes/index.tsx | 5 + app/components/UI/Card/sdk/CardSDK.test.ts | 135 ++++ app/components/UI/Card/sdk/CardSDK.ts | 41 ++ app/components/UI/Card/types.ts | 19 + app/components/UI/Card/util/metrics.ts | 1 + app/constants/navigation/Routes.ts | 1 + locales/languages/en.json | 7 + 20 files changed, 1798 insertions(+), 1 deletion(-) create mode 100644 app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.test.tsx create mode 100644 app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.testIds.ts create mode 100644 app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.tsx create mode 100644 app/components/UI/Card/components/ViewPinBottomSheet/__snapshots__/ViewPinBottomSheet.test.tsx.snap create mode 100644 app/components/UI/Card/components/ViewPinBottomSheet/index.ts create mode 100644 app/components/UI/Card/hooks/useCardPinToken.test.ts create mode 100644 app/components/UI/Card/hooks/useCardPinToken.ts create mode 100644 app/components/UI/Card/queries/pin.test.ts create mode 100644 app/components/UI/Card/queries/pin.ts diff --git a/app/components/UI/Card/Views/CardHome/CardHome.test.tsx b/app/components/UI/Card/Views/CardHome/CardHome.test.tsx index a29a56f4770..f5d80659410 100644 --- a/app/components/UI/Card/Views/CardHome/CardHome.test.tsx +++ b/app/components/UI/Card/Views/CardHome/CardHome.test.tsx @@ -99,6 +99,7 @@ import { } from '../../../../../core/redux/slices/card'; import { useIsSwapEnabledForPriorityToken } from '../../hooks/useIsSwapEnabledForPriorityToken'; import useCardDetailsToken from '../../hooks/useCardDetailsToken'; +import useCardPinToken from '../../hooks/useCardPinToken'; const mockNavigate = jest.fn(); const mockGoBack = jest.fn(); @@ -274,6 +275,26 @@ jest.mock('../../hooks/useCardDetailsToken', () => ({ })), })); +const mockGeneratePinToken = jest.fn(); +const mockResetPinToken = jest.fn(); +jest.mock('../../hooks/useCardPinToken', () => ({ + __esModule: true, + default: jest.fn(() => ({ + generatePinToken: mockGeneratePinToken, + isLoading: false, + error: null, + imageUrl: null, + reset: mockResetPinToken, + })), +})); + +jest.mock('../../components/ViewPinBottomSheet', () => ({ + createViewPinBottomSheetNavigationDetails: jest.fn((params) => [ + 'CardModals', + { screen: 'CardViewPinModal', params }, + ]), +})); + // Mock useAuthentication for biometric verification const mockReauthenticate = jest.fn(); jest.mock('../../../../../core/Authentication/hooks/useAuthentication', () => @@ -503,6 +524,13 @@ jest.mock('../../../../../../locales/i18n', () => ({ 'Authentication required to resume spending on your card.', 'card.password_bottomsheet.description_unfreeze': 'Enter your wallet password to unfreeze your card.', + 'card.card_home.manage_card_options.view_pin': 'View PIN', + 'card.card_home.manage_card_options.view_pin_description': + 'View your card PIN securely', + 'card.card_home.manage_card_options.view_pin_error': + 'Failed to load PIN. Please try again.', + 'card.password_bottomsheet.description_view_pin': + 'Enter your wallet password to view your card PIN.', }; return strings[key] || key; }, @@ -3938,6 +3966,308 @@ describe('CardHome Component', () => { }); }); }); + + describe('View PIN Button', () => { + beforeEach(() => { + mockGeneratePinToken.mockClear(); + mockResetPinToken.mockClear(); + mockReauthenticate.mockClear(); + mockReauthenticate.mockResolvedValue(undefined); + }); + + it('does not show view pin button when user is not authenticated', () => { + // Given: User is not authenticated + setupMockSelectors({ isAuthenticated: false }); + setupLoadCardDataMock({ + isAuthenticated: false, + isBaanxLoginEnabled: true, + cardDetails: { type: CardType.VIRTUAL }, + isLoading: false, + }); + + // When: component renders + render(); + + // Then: view pin button is not shown + expect( + screen.queryByTestId(CardHomeSelectors.VIEW_PIN_BUTTON), + ).toBeNull(); + }); + + it('does not show view pin button when user has no card', () => { + // Given: Authenticated user without card + setupMockSelectors({ isAuthenticated: true }); + setupLoadCardDataMock({ + isAuthenticated: true, + isBaanxLoginEnabled: true, + cardDetails: null, + warning: CardStateWarning.NoCard, + isLoading: false, + kycStatus: { verificationState: 'VERIFIED', userId: 'user-123' }, + }); + + // When: component renders + render(); + + // Then: view pin button is not shown + expect( + screen.queryByTestId(CardHomeSelectors.VIEW_PIN_BUTTON), + ).toBeNull(); + }); + + it('does not show view pin button while loading', () => { + // Given: Loading state + setupMockSelectors({ isAuthenticated: true }); + setupLoadCardDataMock({ + isAuthenticated: true, + isBaanxLoginEnabled: true, + cardDetails: { type: CardType.VIRTUAL }, + isLoading: true, + }); + + // When: component renders + render(); + + // Then: view pin button is not shown + expect( + screen.queryByTestId(CardHomeSelectors.VIEW_PIN_BUTTON), + ).toBeNull(); + }); + + it('does not show view pin button for international virtual card', () => { + // Given: International user with virtual card + setupMockSelectors({ + isAuthenticated: true, + userLocation: 'international', + }); + setupLoadCardDataMock({ + isAuthenticated: true, + isBaanxLoginEnabled: true, + cardDetails: { type: CardType.VIRTUAL }, + isLoading: false, + kycStatus: { verificationState: 'VERIFIED', userId: 'user-123' }, + }); + + // When: component renders + render(); + + // Then: view pin button is not shown + expect( + screen.queryByTestId(CardHomeSelectors.VIEW_PIN_BUTTON), + ).toBeNull(); + }); + + it('shows view pin button for US user with virtual card', () => { + // Given: US user with virtual card + setupMockSelectors({ isAuthenticated: true, userLocation: 'us' }); + setupLoadCardDataMock({ + isAuthenticated: true, + isBaanxLoginEnabled: true, + cardDetails: { type: CardType.VIRTUAL }, + isLoading: false, + kycStatus: { verificationState: 'VERIFIED', userId: 'user-123' }, + }); + + // When: component renders + render(); + + // Then: view pin button is shown + expect( + screen.getByTestId(CardHomeSelectors.VIEW_PIN_BUTTON), + ).toBeOnTheScreen(); + }); + + it('shows view pin button for international user with metal card', () => { + // Given: International user with metal card + setupMockSelectors({ + isAuthenticated: true, + userLocation: 'international', + }); + setupLoadCardDataMock({ + isAuthenticated: true, + isBaanxLoginEnabled: true, + cardDetails: { type: CardType.METAL }, + isLoading: false, + kycStatus: { verificationState: 'VERIFIED', userId: 'user-123' }, + }); + + // When: component renders + render(); + + // Then: view pin button is shown (non-virtual card) + expect( + screen.getByTestId(CardHomeSelectors.VIEW_PIN_BUTTON), + ).toBeOnTheScreen(); + }); + + it('calls generatePinToken and navigates to ViewPinBottomSheet after biometric auth', async () => { + // Given: Authenticated US user with card and biometric auth succeeds + setupMockSelectors({ isAuthenticated: true, userLocation: 'us' }); + setupLoadCardDataMock({ + isAuthenticated: true, + isBaanxLoginEnabled: true, + cardDetails: { type: CardType.VIRTUAL }, + isLoading: false, + kycStatus: { verificationState: 'VERIFIED', userId: 'user-123' }, + }); + + mockReauthenticate.mockResolvedValueOnce(undefined); + mockGeneratePinToken.mockResolvedValueOnce({ + token: 'pin-token-123', + imageUrl: 'https://cards.baanx.com/pin-image?token=pin-token-123', + }); + + // When: component renders and button is pressed + render(); + const button = screen.getByTestId(CardHomeSelectors.VIEW_PIN_BUTTON); + fireEvent.press(button); + + // Then: reauthenticate is called first, then generatePinToken, then navigation + await waitFor(() => { + expect(mockReauthenticate).toHaveBeenCalled(); + }); + await waitFor(() => { + expect(mockGeneratePinToken).toHaveBeenCalled(); + }); + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('CardModals', { + screen: 'CardViewPinModal', + params: { + imageUrl: 'https://cards.baanx.com/pin-image?token=pin-token-123', + }, + }); + }); + }); + + it('resets pin token after successful navigation', async () => { + // Given: Authenticated US user with card + setupMockSelectors({ isAuthenticated: true, userLocation: 'us' }); + setupLoadCardDataMock({ + isAuthenticated: true, + isBaanxLoginEnabled: true, + cardDetails: { type: CardType.VIRTUAL }, + isLoading: false, + kycStatus: { verificationState: 'VERIFIED', userId: 'user-123' }, + }); + + mockReauthenticate.mockResolvedValueOnce(undefined); + mockGeneratePinToken.mockResolvedValueOnce({ + token: 'pin-token-123', + imageUrl: 'https://cards.baanx.com/pin-image?token=pin-token-123', + }); + + // When: button is pressed + render(); + fireEvent.press(screen.getByTestId(CardHomeSelectors.VIEW_PIN_BUTTON)); + + // Then: resetPinToken is called after navigation + await waitFor(() => { + expect(mockResetPinToken).toHaveBeenCalled(); + }); + }); + + it('does not call generatePinToken when already loading', async () => { + // Given: Hook reports loading + setupMockSelectors({ isAuthenticated: true, userLocation: 'us' }); + setupLoadCardDataMock({ + isAuthenticated: true, + isBaanxLoginEnabled: true, + cardDetails: { type: CardType.VIRTUAL }, + isLoading: false, + kycStatus: { verificationState: 'VERIFIED', userId: 'user-123' }, + }); + + (useCardPinToken as jest.Mock).mockReturnValueOnce({ + generatePinToken: mockGeneratePinToken, + isLoading: true, + error: null, + imageUrl: null, + reset: mockResetPinToken, + }); + + // When: button is pressed while loading + render(); + fireEvent.press(screen.getByTestId(CardHomeSelectors.VIEW_PIN_BUTTON)); + + // Then: reauthenticate is not called + await waitFor(() => { + expect(mockReauthenticate).not.toHaveBeenCalled(); + }); + expect(mockGeneratePinToken).not.toHaveBeenCalled(); + }); + + describe('Biometric Authentication', () => { + it('does not fetch pin when biometric authentication fails', async () => { + // Given: Authenticated US user with card but biometric auth fails + setupMockSelectors({ isAuthenticated: true, userLocation: 'us' }); + setupLoadCardDataMock({ + isAuthenticated: true, + isBaanxLoginEnabled: true, + cardDetails: { type: CardType.VIRTUAL }, + isLoading: false, + kycStatus: { verificationState: 'VERIFIED', userId: 'user-123' }, + }); + + mockReauthenticate.mockRejectedValueOnce( + new Error('BIOMETRIC_ERROR: User cancelled'), + ); + + // When: component renders and button is pressed + render(); + fireEvent.press( + screen.getByTestId(CardHomeSelectors.VIEW_PIN_BUTTON), + ); + + // Then: reauthenticate is called but generatePinToken is NOT called + await waitFor(() => { + expect(mockReauthenticate).toHaveBeenCalled(); + }); + expect(mockGeneratePinToken).not.toHaveBeenCalled(); + }); + + it('navigates to password bottom sheet with view pin description when biometrics not configured', async () => { + // Given: Authenticated US user with card but biometrics not configured + setupMockSelectors({ isAuthenticated: true, userLocation: 'us' }); + setupLoadCardDataMock({ + isAuthenticated: true, + isBaanxLoginEnabled: true, + cardDetails: { type: CardType.VIRTUAL }, + isLoading: false, + kycStatus: { verificationState: 'VERIFIED', userId: 'user-123' }, + }); + + mockReauthenticate.mockRejectedValueOnce( + new Error( + 'PASSWORD_NOT_SET_WITH_BIOMETRICS: Biometrics not configured', + ), + ); + + // When: component renders and button is pressed + render(); + fireEvent.press( + screen.getByTestId(CardHomeSelectors.VIEW_PIN_BUTTON), + ); + + // Then: navigation to password bottom sheet is triggered with view pin description + await waitFor(() => { + expect(mockReauthenticate).toHaveBeenCalled(); + }); + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith( + Routes.CARD.MODALS.ID, + expect.objectContaining({ + screen: Routes.CARD.MODALS.PASSWORD, + params: expect.objectContaining({ + onSuccess: expect.any(Function), + description: + 'Enter your wallet password to view your card PIN.', + }), + }), + ); + }); + }); + }); + }); }); describe('Freeze Card Toggle', () => { diff --git a/app/components/UI/Card/Views/CardHome/CardHome.testIds.ts b/app/components/UI/Card/Views/CardHome/CardHome.testIds.ts index 528adfdb857..94fd58f65e6 100644 --- a/app/components/UI/Card/Views/CardHome/CardHome.testIds.ts +++ b/app/components/UI/Card/Views/CardHome/CardHome.testIds.ts @@ -30,4 +30,5 @@ export const CardHomeSelectors = { ORDER_METAL_CARD_ITEM: 'order-metal-card-item', CASHBACK_ITEM: 'cashback-item', FREEZE_CARD_TOGGLE: 'freeze-card-toggle', + VIEW_PIN_BUTTON: 'view-pin-button', }; diff --git a/app/components/UI/Card/Views/CardHome/CardHome.tsx b/app/components/UI/Card/Views/CardHome/CardHome.tsx index 814a21b1e98..8ddfdbb1168 100644 --- a/app/components/UI/Card/Views/CardHome/CardHome.tsx +++ b/app/components/UI/Card/Views/CardHome/CardHome.tsx @@ -84,6 +84,7 @@ import { removeCardBaanxToken } from '../../util/cardTokenVault'; import useLoadCardData from '../../hooks/useLoadCardData'; import useCardFreeze from '../../hooks/useCardFreeze'; import useCardDetailsToken from '../../hooks/useCardDetailsToken'; +import useCardPinToken from '../../hooks/useCardPinToken'; import useAuthentication from '../../../../../core/Authentication/hooks/useAuthentication'; import { ReauthenticateErrorType } from '../../../../../core/Authentication/types'; import { CardActions } from '../../util/metrics'; @@ -104,6 +105,7 @@ import { import { AddToWalletButton } from '@expensify/react-native-wallet'; import { CardScreenshotDeterrent } from '../../components/CardScreenshotDeterrent'; import { createPasswordBottomSheetNavigationDetails } from '../../components/PasswordBottomSheet'; +import { createViewPinBottomSheetNavigationDetails } from '../../components/ViewPinBottomSheet'; import { buildProvisioningUserAddress, buildShippingAddress, @@ -149,6 +151,11 @@ const CardHome = () => { imageUrl: cardDetailsImageUrl, clearImageUrl: clearCardDetailsImageUrl, } = useCardDetailsToken(); + const { + generatePinToken, + isLoading: isPinLoading, + reset: resetPinToken, + } = useCardPinToken(); const { reauthenticate } = useAuthentication(); const hasTrackedCardHomeView = useRef(false); const hasLoadedCardHomeView = useRef(false); @@ -830,6 +837,91 @@ const CardHome = () => { createEventBuilder, ]); + const fetchAndShowPin = useCallback(async () => { + trackEvent( + createEventBuilder(MetaMetricsEvents.CARD_BUTTON_CLICKED) + .addProperties({ + action: CardActions.VIEW_PIN_BUTTON, + }) + .build(), + ); + + try { + const response = await generatePinToken(); + navigation.navigate( + ...createViewPinBottomSheetNavigationDetails({ + imageUrl: response.imageUrl, + }), + ); + } catch { + toastRef?.current?.showToast({ + variant: ToastVariants.Icon, + labelOptions: [ + { + label: strings('card.card_home.manage_card_options.view_pin_error'), + }, + ], + hasNoTimeout: false, + iconName: IconName.Warning, + }); + } finally { + resetPinToken(); + } + }, [ + generatePinToken, + navigation, + toastRef, + resetPinToken, + trackEvent, + createEventBuilder, + ]); + + const viewPinAction = useCallback(async () => { + if (isPinLoading) { + return; + } + + try { + await reauthenticate(); + await fetchAndShowPin(); + } catch (error) { + const errorMessage = (error as Error).message; + + if ( + errorMessage.includes( + ReauthenticateErrorType.PASSWORD_NOT_SET_WITH_BIOMETRICS, + ) + ) { + navigation.navigate( + ...createPasswordBottomSheetNavigationDetails({ + onSuccess: () => { + fetchAndShowPin(); + }, + description: strings( + 'card.password_bottomsheet.description_view_pin', + ), + }), + ); + return; + } + + if (errorMessage.includes(ReauthenticateErrorType.BIOMETRIC_ERROR)) { + return; + } + + toastRef?.current?.showToast({ + variant: ToastVariants.Icon, + labelOptions: [ + { + label: strings('card.card_home.biometric_verification_required'), + }, + ], + hasNoTimeout: false, + iconName: IconName.Warning, + }); + } + }, [isPinLoading, reauthenticate, fetchAndShowPin, navigation, toastRef]); + const cardSetupState = useMemo(() => { const needsSetup = isBaanxLoginEnabled && @@ -1350,6 +1442,20 @@ const CardHome = () => { testID={CardHomeSelectors.VIEW_CARD_DETAILS_BUTTON} /> )} + {isAuthenticated && + !isLoading && + cardDetails && + (userLocation === 'us' || cardDetails.type !== CardType.VIRTUAL) && ( + + )} {isAuthenticated && !isLoading && cardDetails?.isFreezable && diff --git a/app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.test.tsx b/app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.test.tsx new file mode 100644 index 00000000000..365643eb250 --- /dev/null +++ b/app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.test.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { Linking } from 'react-native'; +import { fireEvent, waitFor } from '@testing-library/react-native'; +import ViewPinBottomSheet from './ViewPinBottomSheet'; +import { renderScreen } from '../../../../../util/test/renderWithProvider'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; +import { ViewPinBottomSheetSelectors } from './ViewPinBottomSheet.testIds'; + +const mockUseParams = jest.fn(); +const mockGoBack = jest.fn(); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(Linking as any).removeEventListener = jest.fn(); + +jest.mock('../../../../../util/navigation/navUtils', () => ({ + useParams: () => mockUseParams(), + createNavigationDetails: jest.fn((stackId, screenName) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (params?: any) => [stackId, { screen: screenName, params }], + ), +})); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + goBack: mockGoBack, + }), +})); + +jest.mock('../CardScreenshotDeterrent/CardScreenshotDeterrent', () => { + const { View } = jest.requireActual('react-native'); + return (props: Record) => ( + + ); +}); + +const TEST_IMAGE_URL = + 'https://cards.baanx.com/details-image?token=test-pin-token'; + +const renderWithProvider = (component: React.ComponentType) => + renderScreen( + component, + { + name: 'ViewPinBottomSheet', + }, + { + state: { + engine: { + backgroundState, + }, + }, + }, + ); + +describe('ViewPinBottomSheet', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseParams.mockReturnValue({ + imageUrl: TEST_IMAGE_URL, + }); + }); + + it('renders correctly and matches snapshot', () => { + const { toJSON } = renderWithProvider(() => ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('displays the title', () => { + const { getByText } = renderWithProvider(() => ); + + expect(getByText('Your Card PIN')).toBeOnTheScreen(); + }); + + it('renders the PIN image with correct source', () => { + const { getByTestId } = renderWithProvider(() => ); + + const pinImage = getByTestId(ViewPinBottomSheetSelectors.PIN_IMAGE); + + expect(pinImage).toBeOnTheScreen(); + expect(pinImage.props.source).toEqual({ uri: TEST_IMAGE_URL }); + }); + + it('displays skeleton while image is loading', () => { + const { getByTestId } = renderWithProvider(() => ); + + expect( + getByTestId(ViewPinBottomSheetSelectors.PIN_IMAGE_SKELETON), + ).toBeOnTheScreen(); + }); + + it('hides skeleton after image loads', async () => { + const { getByTestId, queryByTestId } = renderWithProvider(() => ( + + )); + + const pinImage = getByTestId(ViewPinBottomSheetSelectors.PIN_IMAGE); + + fireEvent(pinImage, 'load'); + + await waitFor(() => { + expect( + queryByTestId(ViewPinBottomSheetSelectors.PIN_IMAGE_SKELETON), + ).toBeNull(); + }); + }); + + it('hides skeleton on image error', async () => { + const { getByTestId, queryByTestId } = renderWithProvider(() => ( + + )); + + const pinImage = getByTestId(ViewPinBottomSheetSelectors.PIN_IMAGE); + + fireEvent(pinImage, 'error'); + + await waitFor(() => { + expect( + queryByTestId(ViewPinBottomSheetSelectors.PIN_IMAGE_SKELETON), + ).toBeNull(); + }); + }); + + it('renders CardScreenshotDeterrent as enabled', () => { + const { getByTestId } = renderWithProvider(() => ); + + const deterrent = getByTestId('card-screenshot-deterrent'); + + expect(deterrent.props.enabled).toBe(true); + }); + + it('renders the bottom sheet with correct testID', () => { + const { getByTestId } = renderWithProvider(() => ); + + expect( + getByTestId(ViewPinBottomSheetSelectors.BOTTOM_SHEET), + ).toBeOnTheScreen(); + }); +}); diff --git a/app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.testIds.ts b/app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.testIds.ts new file mode 100644 index 00000000000..758fc8893f7 --- /dev/null +++ b/app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.testIds.ts @@ -0,0 +1,5 @@ +export const ViewPinBottomSheetSelectors = { + BOTTOM_SHEET: 'view-pin-bottom-sheet', + PIN_IMAGE: 'view-pin-image', + PIN_IMAGE_SKELETON: 'view-pin-image-skeleton', +}; diff --git a/app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.tsx b/app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.tsx new file mode 100644 index 00000000000..fc37ff080f4 --- /dev/null +++ b/app/components/UI/Card/components/ViewPinBottomSheet/ViewPinBottomSheet.tsx @@ -0,0 +1,80 @@ +import React, { useCallback, useRef, useState } from 'react'; +import { Image } from 'react-native'; +import { Box, Text, TextVariant } from '@metamask/design-system-react-native'; +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import BottomSheet, { + BottomSheetRef, +} from '../../../../../component-library/components/BottomSheets/BottomSheet'; +import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; +import { Skeleton } from '../../../../../component-library/components/Skeleton'; +import { + createNavigationDetails, + useParams, +} from '../../../../../util/navigation/navUtils'; +import Routes from '../../../../../constants/navigation/Routes'; +import { strings } from '../../../../../../locales/i18n'; +import CardScreenshotDeterrent from '../CardScreenshotDeterrent/CardScreenshotDeterrent'; +import { ViewPinBottomSheetSelectors } from './ViewPinBottomSheet.testIds'; + +interface ViewPinBottomSheetParams { + imageUrl: string; +} + +export const createViewPinBottomSheetNavigationDetails = + createNavigationDetails( + Routes.CARD.MODALS.ID, + Routes.CARD.MODALS.VIEW_PIN, + ); + +const ViewPinBottomSheet: React.FC = () => { + const sheetRef = useRef(null); + const { imageUrl } = useParams(); + const tw = useTailwind(); + const [isImageLoading, setIsImageLoading] = useState(true); + + const handleClose = useCallback(() => { + sheetRef.current?.onCloseBottomSheet(); + }, []); + + return ( + + + + {strings('card.view_pin_bottomsheet.title')} + + + + + + {isImageLoading && ( + + )} + setIsImageLoading(false)} + onError={() => setIsImageLoading(false)} + testID={ViewPinBottomSheetSelectors.PIN_IMAGE} + /> + + + + + + ); +}; + +export default ViewPinBottomSheet; diff --git a/app/components/UI/Card/components/ViewPinBottomSheet/__snapshots__/ViewPinBottomSheet.test.tsx.snap b/app/components/UI/Card/components/ViewPinBottomSheet/__snapshots__/ViewPinBottomSheet.test.tsx.snap new file mode 100644 index 00000000000..6e4be5d814c --- /dev/null +++ b/app/components/UI/Card/components/ViewPinBottomSheet/__snapshots__/ViewPinBottomSheet.test.tsx.snap @@ -0,0 +1,626 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ViewPinBottomSheet renders correctly and matches snapshot 1`] = ` + + + + + + + + + + + + + ViewPinBottomSheet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Your Card PIN + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/app/components/UI/Card/components/ViewPinBottomSheet/index.ts b/app/components/UI/Card/components/ViewPinBottomSheet/index.ts new file mode 100644 index 00000000000..06a370940cf --- /dev/null +++ b/app/components/UI/Card/components/ViewPinBottomSheet/index.ts @@ -0,0 +1,2 @@ +export { default } from './ViewPinBottomSheet'; +export { createViewPinBottomSheetNavigationDetails } from './ViewPinBottomSheet'; diff --git a/app/components/UI/Card/hooks/useCardPinToken.test.ts b/app/components/UI/Card/hooks/useCardPinToken.test.ts new file mode 100644 index 00000000000..6558b661310 --- /dev/null +++ b/app/components/UI/Card/hooks/useCardPinToken.test.ts @@ -0,0 +1,168 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useMutation } from '@tanstack/react-query'; +import { useCardSDK } from '../sdk'; +import useCardPinToken from './useCardPinToken'; + +jest.mock('@tanstack/react-query', () => ({ + useMutation: jest.fn(), +})); + +jest.mock('../sdk', () => ({ + useCardSDK: jest.fn(), +})); + +const mockUseTheme = jest.fn(); +jest.mock('../../../../util/theme', () => ({ + useTheme: (...args: unknown[]) => mockUseTheme(...args), +})); + +jest.mock('../queries', () => ({ + cardQueries: { + pin: { + keys: { token: () => ['card', 'pin', 'token'] }, + tokenMutationFn: jest.fn(() => jest.fn()), + }, + }, +})); + +const mockUseCardSDK = useCardSDK as jest.MockedFunction; + +describe('useCardPinToken', () => { + const mockMutateAsync = jest.fn(); + const mockReset = jest.fn(); + + const mockTokenResponse = { + token: 'pin-token-123', + imageUrl: 'https://cards.baanx.com/details-image?token=pin-token-123', + }; + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseTheme.mockReturnValue({ themeAppearance: 'light' }); + + mockUseCardSDK.mockReturnValue({ + sdk: {} as never, + isLoading: false, + user: null, + setUser: jest.fn(), + logoutFromProvider: jest.fn(), + fetchUserData: jest.fn(), + isReturningSession: false, + }); + + mockMutateAsync.mockResolvedValue(mockTokenResponse); + + (useMutation as jest.Mock).mockReturnValue({ + mutateAsync: mockMutateAsync, + isPending: false, + error: null, + data: null, + reset: mockReset, + }); + }); + + describe('Initial State', () => { + it('initializes with correct default values', () => { + const { result } = renderHook(() => useCardPinToken()); + + expect(result.current.isLoading).toBe(false); + expect(result.current.error).toBeNull(); + expect(result.current.imageUrl).toBeNull(); + expect(typeof result.current.generatePinToken).toBe('function'); + expect(typeof result.current.reset).toBe('function'); + }); + }); + + describe('generatePinToken', () => { + it('calls mutateAsync with light theme CSS', async () => { + const { result } = renderHook(() => useCardPinToken()); + + await act(async () => { + await result.current.generatePinToken(); + }); + + expect(mockMutateAsync).toHaveBeenCalledWith({ + customCss: { backgroundColor: '#FFF', textColor: '#000' }, + }); + }); + + it('returns the mutation response', async () => { + const { result } = renderHook(() => useCardPinToken()); + + let response; + await act(async () => { + response = await result.current.generatePinToken(); + }); + + expect(response).toEqual(mockTokenResponse); + }); + }); + + describe('Loading State', () => { + it('reflects isPending from mutation', () => { + (useMutation as jest.Mock).mockReturnValue({ + mutateAsync: mockMutateAsync, + isPending: true, + error: null, + data: null, + reset: mockReset, + }); + + const { result } = renderHook(() => useCardPinToken()); + + expect(result.current.isLoading).toBe(true); + }); + }); + + describe('Error Handling', () => { + it('reflects error from mutation', () => { + const testError = new Error('Network error'); + (useMutation as jest.Mock).mockReturnValue({ + mutateAsync: mockMutateAsync, + isPending: false, + error: testError, + data: null, + reset: mockReset, + }); + + const { result } = renderHook(() => useCardPinToken()); + + expect(result.current.error).toBe(testError); + }); + }); + + describe('imageUrl', () => { + it('returns imageUrl from successful mutation data', () => { + (useMutation as jest.Mock).mockReturnValue({ + mutateAsync: mockMutateAsync, + isPending: false, + error: null, + data: mockTokenResponse, + reset: mockReset, + }); + + const { result } = renderHook(() => useCardPinToken()); + + expect(result.current.imageUrl).toBe(mockTokenResponse.imageUrl); + }); + + it('returns null when no data', () => { + const { result } = renderHook(() => useCardPinToken()); + + expect(result.current.imageUrl).toBeNull(); + }); + }); + + describe('reset', () => { + it('delegates to mutation reset', () => { + const { result } = renderHook(() => useCardPinToken()); + + act(() => { + result.current.reset(); + }); + + expect(mockReset).toHaveBeenCalled(); + }); + }); +}); diff --git a/app/components/UI/Card/hooks/useCardPinToken.ts b/app/components/UI/Card/hooks/useCardPinToken.ts new file mode 100644 index 00000000000..f1117a4ebf7 --- /dev/null +++ b/app/components/UI/Card/hooks/useCardPinToken.ts @@ -0,0 +1,56 @@ +import { useCallback, useMemo } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { useCardSDK } from '../sdk'; +import { cardQueries } from '../queries'; +import { useTheme } from '../../../../util/theme'; +import type { CardPinTokenResponse } from '../types'; + +/* eslint-disable @metamask/design-tokens/color-no-hex */ +const PIN_CSS = { + dark: { backgroundColor: '#121314', textColor: '#FFF' }, + light: { backgroundColor: '#FFF', textColor: '#000' }, +} as const; + +interface UseCardPinTokenResult { + generatePinToken: () => Promise; + isLoading: boolean; + error: Error | null; + imageUrl: string | null; + reset: () => void; +} + +/** + * Hook to generate a secure token for viewing the card PIN as an image. + * Uses React Query useMutation since this is a one-off POST (not cached data). + * The token is time-limited (~10 minutes) and single-use. + * Automatically applies dark/light theme styling to the PIN image. + */ +const useCardPinToken = (): UseCardPinTokenResult => { + const { sdk } = useCardSDK(); + const theme = useTheme(); + + const customCss = useMemo( + () => (theme.themeAppearance === 'dark' ? PIN_CSS.dark : PIN_CSS.light), + [theme.themeAppearance], + ); + + const { mutateAsync, isPending, error, data, reset } = useMutation({ + mutationKey: cardQueries.pin.keys.token(), + mutationFn: cardQueries.pin.tokenMutationFn(sdk), + }); + + const generatePinToken = useCallback( + () => mutateAsync({ customCss }), + [mutateAsync, customCss], + ); + + return { + generatePinToken, + isLoading: isPending, + error, + imageUrl: data?.imageUrl ?? null, + reset, + }; +}; + +export default useCardPinToken; diff --git a/app/components/UI/Card/queries/index.ts b/app/components/UI/Card/queries/index.ts index c8c77d04628..5ba4e742b2c 100644 --- a/app/components/UI/Card/queries/index.ts +++ b/app/components/UI/Card/queries/index.ts @@ -1,10 +1,14 @@ +import { pinKeys, pinTokenMutationFn } from './pin'; import { cashbackKeys, cashbackWalletOptions, cashbackWithdrawEstimationOptions, } from './cashback'; - export const cardQueries = { + pin: { + keys: pinKeys, + tokenMutationFn: pinTokenMutationFn, + }, cashback: { keys: cashbackKeys, walletOptions: cashbackWalletOptions, diff --git a/app/components/UI/Card/queries/pin.test.ts b/app/components/UI/Card/queries/pin.test.ts new file mode 100644 index 00000000000..175983e1362 --- /dev/null +++ b/app/components/UI/Card/queries/pin.test.ts @@ -0,0 +1,56 @@ +import { CardSDK } from '../sdk/CardSDK'; +import { pinKeys, pinTokenMutationFn } from './pin'; + +describe('pinKeys', () => { + it('returns the base key for all pin queries', () => { + expect(pinKeys.all()).toEqual(['card', 'pin']); + }); + + it('returns the token mutation key', () => { + expect(pinKeys.token()).toEqual(['card', 'pin', 'token']); + }); +}); + +describe('pinTokenMutationFn', () => { + const mockSdk = { + generateCardPinToken: jest.fn(), + } as unknown as CardSDK; + + it('calls sdk.generateCardPinToken with request', async () => { + const mockResponse = { + token: 'test-token-uuid', + imageUrl: 'https://cards.baanx.com/details-image?token=test-token-uuid', + }; + (mockSdk.generateCardPinToken as jest.Mock).mockResolvedValue(mockResponse); + + const mutationFn = pinTokenMutationFn(mockSdk); + const result = await mutationFn({ + customCss: { backgroundColor: '#FFFFFF', textColor: '#000000' }, + }); + + expect(mockSdk.generateCardPinToken).toHaveBeenCalledWith({ + customCss: { backgroundColor: '#FFFFFF', textColor: '#000000' }, + }); + expect(result).toEqual(mockResponse); + }); + + it('calls sdk.generateCardPinToken without request', async () => { + const mockResponse = { + token: 'test-token-uuid', + imageUrl: 'https://cards.baanx.com/details-image?token=test-token-uuid', + }; + (mockSdk.generateCardPinToken as jest.Mock).mockResolvedValue(mockResponse); + + const mutationFn = pinTokenMutationFn(mockSdk); + const result = await mutationFn(); + + expect(mockSdk.generateCardPinToken).toHaveBeenCalledWith(undefined); + expect(result).toEqual(mockResponse); + }); + + it('throws when sdk is null', async () => { + const mutationFn = pinTokenMutationFn(null); + + await expect(mutationFn()).rejects.toThrow('CardSDK not available'); + }); +}); diff --git a/app/components/UI/Card/queries/pin.ts b/app/components/UI/Card/queries/pin.ts new file mode 100644 index 00000000000..ac53ca5dbdb --- /dev/null +++ b/app/components/UI/Card/queries/pin.ts @@ -0,0 +1,14 @@ +import { CardSDK } from '../sdk/CardSDK'; +import type { CardPinTokenRequest, CardPinTokenResponse } from '../types'; + +export const pinKeys = { + all: () => ['card', 'pin'] as const, + token: () => [...pinKeys.all(), 'token'] as const, +}; + +export const pinTokenMutationFn = + (sdk: CardSDK | null) => + async (request?: CardPinTokenRequest): Promise => { + if (!sdk) throw new Error('CardSDK not available'); + return sdk.generateCardPinToken(request); + }; diff --git a/app/components/UI/Card/routes/index.tsx b/app/components/UI/Card/routes/index.tsx index 92d37921177..f76776460ee 100644 --- a/app/components/UI/Card/routes/index.tsx +++ b/app/components/UI/Card/routes/index.tsx @@ -27,6 +27,7 @@ import RegionSelectorModal from '../components/Onboarding/RegionSelectorModal'; import ConfirmModal from '../components/Onboarding/ConfirmModal'; import RecurringFeeModal from '../components/RecurringFeeModal/RecurringFeeModal'; import DaimoPayModal from '../components/DaimoPayModal/DaimoPayModal'; +import ViewPinBottomSheet from '../components/ViewPinBottomSheet'; import OrderCompleted from '../Views/OrderCompleted/OrderCompleted'; import Cashback from '../Views/Cashback/Cashback'; import { @@ -231,6 +232,10 @@ const CardModalsRoutes = () => ( name={Routes.CARD.MODALS.DAIMO_PAY} component={DaimoPayModal} /> + ); diff --git a/app/components/UI/Card/sdk/CardSDK.test.ts b/app/components/UI/Card/sdk/CardSDK.test.ts index 3670b98b91a..1e3b1864a32 100644 --- a/app/components/UI/Card/sdk/CardSDK.test.ts +++ b/app/components/UI/Card/sdk/CardSDK.test.ts @@ -4105,6 +4105,141 @@ describe('CardSDK', () => { }); }); + describe('generateCardPinToken', () => { + const mockPinTokenResponse = { + token: 'pin-token-uuid-123', + imageUrl: + 'https://cards.baanx.com/details-image?token=pin-token-uuid-123', + }; + + beforeEach(() => { + (getCardBaanxToken as jest.Mock).mockResolvedValue({ + success: true, + tokenData: { accessToken: 'test-access-token' }, + }); + }); + + it('generates card PIN token successfully', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValue(mockPinTokenResponse), + }); + + const result = await cardSDK.generateCardPinToken(); + + expect(result).toEqual(mockPinTokenResponse); + }); + + it('generates card PIN token with custom CSS', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValue(mockPinTokenResponse), + }); + + const customCss = { + backgroundColor: '#FFFFFF', + textColor: '#000000', + }; + + const result = await cardSDK.generateCardPinToken({ customCss }); + + expect(result).toEqual(mockPinTokenResponse); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/card/pin/token'), + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ customCss }), + }), + ); + }); + + it('throws INVALID_CREDENTIALS error on 401 response', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 401, + statusText: 'Unauthorized', + }); + + try { + await cardSDK.generateCardPinToken(); + fail('Expected error to be thrown'); + } catch (error) { + expect(error).toBeInstanceOf(CardError); + expect((error as CardError).type).toBe( + CardErrorType.INVALID_CREDENTIALS, + ); + } + }); + + it('throws INVALID_CREDENTIALS error on 403 response', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 403, + statusText: 'Forbidden', + }); + + try { + await cardSDK.generateCardPinToken(); + fail('Expected error to be thrown'); + } catch (error) { + expect(error).toBeInstanceOf(CardError); + expect((error as CardError).type).toBe( + CardErrorType.INVALID_CREDENTIALS, + ); + } + }); + + it('throws NO_CARD error on 404 response', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + try { + await cardSDK.generateCardPinToken(); + fail('Expected error to be thrown'); + } catch (error) { + expect(error).toBeInstanceOf(CardError); + expect((error as CardError).type).toBe(CardErrorType.NO_CARD); + } + }); + + it('throws SERVER_ERROR on 500 response', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + }); + + try { + await cardSDK.generateCardPinToken(); + fail('Expected error to be thrown'); + } catch (error) { + expect(error).toBeInstanceOf(CardError); + expect((error as CardError).type).toBe(CardErrorType.SERVER_ERROR); + } + }); + + it('sends authenticated request with bearer token', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValue(mockPinTokenResponse), + }); + + await cardSDK.generateCardPinToken(); + + expect(global.fetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer test-access-token', + }), + }), + ); + }); + }); + describe('createOrder', () => { const mockOrderResponse = { orderId: 'order-123', diff --git a/app/components/UI/Card/sdk/CardSDK.ts b/app/components/UI/Card/sdk/CardSDK.ts index 0e6811d0ea8..fc9a8db8a23 100644 --- a/app/components/UI/Card/sdk/CardSDK.ts +++ b/app/components/UI/Card/sdk/CardSDK.ts @@ -49,6 +49,8 @@ import { GetOnboardingConsentResponse, CardDetailsTokenRequest, CardDetailsTokenResponse, + CardPinTokenRequest, + CardPinTokenResponse, CreateOrderRequest, CreateOrderResponse, GetOrderStatusResponse, @@ -1141,6 +1143,45 @@ export class CardSDK { return (await response.json()) as CardDetailsTokenResponse; }; + /** + * Generate a secure token for viewing the card PIN through an image-based display. + * The token is time-limited (~10 minutes) and single-use. + * The PIN is never transmitted as plain text, ensuring PCI compliance. + * + * @param request - Optional customization for the PIN image appearance + * @returns Promise containing the token and imageUrl for displaying the card PIN + */ + generateCardPinToken = async ( + request?: CardPinTokenRequest, + ): Promise => { + const response = await this.makeRequest('/v1/card/pin/token', { + fetchOptions: { + method: 'POST', + ...(request && { body: JSON.stringify(request) }), + }, + authenticated: true, + }); + + if (!response.ok) { + const errorType = + response.status === 401 || response.status === 403 + ? CardErrorType.INVALID_CREDENTIALS + : response.status === 404 + ? CardErrorType.NO_CARD + : CardErrorType.SERVER_ERROR; + + throw this.logAndCreateError( + errorType, + 'Failed to generate card PIN token. Please try again.', + 'generateCardPinToken', + 'card/pin/token', + response.status, + ); + } + + return (await response.json()) as CardPinTokenResponse; + }; + getCardExternalWalletDetails = async ( delegationSettings: DelegationSettingsNetwork[], ): Promise => { diff --git a/app/components/UI/Card/types.ts b/app/components/UI/Card/types.ts index 22417f66212..ec70bdbac89 100644 --- a/app/components/UI/Card/types.ts +++ b/app/components/UI/Card/types.ts @@ -464,6 +464,25 @@ export interface CardDetailsTokenResponse { imageUrl: string; } +/** + * Request body for generating card PIN token + * Used to customize the visual appearance of the PIN image + */ +export interface CardPinTokenRequest { + customCss?: { + backgroundColor?: string; + textColor?: string; + }; +} + +/** + * Response from generating card PIN token + */ +export interface CardPinTokenResponse { + token: string; + imageUrl: string; +} + /** * Payment methods supported for orders */ diff --git a/app/components/UI/Card/util/metrics.ts b/app/components/UI/Card/util/metrics.ts index a0a5c1fdf85..97497367280 100644 --- a/app/components/UI/Card/util/metrics.ts +++ b/app/components/UI/Card/util/metrics.ts @@ -78,6 +78,7 @@ enum CardActions { ADD_TO_WALLET_BUTTON = 'ADD_TO_WALLET_BUTTON', FREEZE_CARD_BUTTON = 'FREEZE_CARD_BUTTON', UNFREEZE_CARD_BUTTON = 'UNFREEZE_CARD_BUTTON', + VIEW_PIN_BUTTON = 'VIEW_PIN_BUTTON', CASHBACK_BUTTON = 'CASHBACK_BUTTON', } diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 56552a0cd8e..84d69e111c1 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -453,6 +453,7 @@ const Routes = { PASSWORD: 'CardPasswordModal', RECURRING_FEE: 'CardRecurringFeeModal', DAIMO_PAY: 'CardDaimoPayModal', + VIEW_PIN: 'CardViewPinModal', }, }, SEND: { diff --git a/locales/languages/en.json b/locales/languages/en.json index 57ebeb0f55a..832ea7949a7 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -6871,12 +6871,16 @@ "title": "Enter password", "description": "Enter your wallet password to view card details.", "description_unfreeze": "Enter your wallet password to resume spending on your card.", + "description_view_pin": "Enter your wallet password to view your card PIN.", "placeholder": "Password", "confirm": "Confirm", "cancel": "Cancel", "error_empty": "Please enter your password", "error_incorrect": "Incorrect password. Please try again." }, + "view_pin_bottomsheet": { + "title": "Your Card PIN" + }, "choose_your_card": { "title": "Choose your card", "upgrade_title": "Upgrade to Metal", @@ -7192,6 +7196,9 @@ "view_card_details": "View card details", "hide_card_details": "Hide card details", "view_card_details_description": "Card number, expiration and CVV", + "view_pin": "View PIN", + "view_pin_description": "View your card PIN securely", + "view_pin_error": "Failed to load PIN. Please try again.", "manage_spending_limit": "Manage limit", "manage_spending_limit_description_restricted": "Limited spending is on", "manage_spending_limit_description_full": "Full access is on", From c0576fc3fe4c0df451ad942f13783bf190722bbd Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 3 Mar 2026 03:41:23 +0000 Subject: [PATCH 074/131] [skip ci] Bump version number to 3868 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1ed86b4b054..1f752db5447 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.68.0" - versionCode 3855 + versionCode 3868 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index bcf72eb99c8..4f8062c7cdb 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.68.0 - opts: is_expand: false - VERSION_NUMBER: 3855 + VERSION_NUMBER: 3868 - opts: is_expand: false FLASK_VERSION_NAME: 7.68.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3855 + FLASK_VERSION_NUMBER: 3868 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 096fbcd4845..7795389f7e5 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3855; + CURRENT_PROJECT_VERSION = 3868; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3855; + CURRENT_PROJECT_VERSION = 3868; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3855; + CURRENT_PROJECT_VERSION = 3868; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3855; + CURRENT_PROJECT_VERSION = 3868; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3855; + CURRENT_PROJECT_VERSION = 3868; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3855; + CURRENT_PROJECT_VERSION = 3868; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 6a6fc2018365415d9593175a9eec01afeb72127e Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:54:15 +0000 Subject: [PATCH 075/131] chore(runway): cherry-pick fix: Bump @metamask/transaction-pay-controller to ^16.1.1 (#26897) - fix: Bump @metamask/transaction-pay-controller to ^16.1.1 cp-7.68.0 (#26782) ## **Description** Bumps @metamask/transaction-pay-controller to ^16.1.1. It has support for gasless predict withdraw. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Although this is a patch-level dependency bump, it touches `transaction-pay` behavior and updates transitive controller versions (`bridge-controller`, `remote-feature-flag-controller`), which could impact transaction/payment flows. > > **Overview** > Updates the `@metamask/transaction-pay-controller` dependency from `^16.1.0` to `^16.1.1`. > > The lockfile is refreshed to pull in the new `transaction-pay-controller` release and its updated transitive requirements, including newer `@metamask/bridge-controller` and `@metamask/remote-feature-flag-controller` versions. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4cbee1104a48bb64921551bb6bf833db69d3a76b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> [ba615cf](https://github.com/MetaMask/metamask-mobile/commit/ba615cf5a06e3975d62fe341fdf2406e1db0c1f2) Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com> Co-authored-by: dan437 <80175477+dan437@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index eb696f97f17..db870557be1 100644 --- a/package.json +++ b/package.json @@ -294,7 +294,7 @@ "@metamask/swappable-obj-proxy": "^2.1.0", "@metamask/swaps-controller": "^15.0.0", "@metamask/transaction-controller": "^62.19.0", - "@metamask/transaction-pay-controller": "^16.1.0", + "@metamask/transaction-pay-controller": "^16.1.1", "@metamask/tron-wallet-snap": "^1.21.1", "@metamask/utils": "^11.8.1", "@myx-trade/sdk": "^0.1.265", diff --git a/yarn.lock b/yarn.lock index 849ccebbbbc..869fde2c193 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7698,7 +7698,7 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:100.0.3, @metamask/assets-controllers@npm:^100.0.2, @metamask/assets-controllers@npm:^100.0.3": +"@metamask/assets-controllers@npm:100.0.3, @metamask/assets-controllers@npm:^100.0.3": version: 100.0.3 resolution: "@metamask/assets-controllers@npm:100.0.3" dependencies: @@ -7910,9 +7910,9 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-controller@npm:^67.1.1, @metamask/bridge-controller@npm:^67.2.0": - version: 67.2.0 - resolution: "@metamask/bridge-controller@npm:67.2.0" +"@metamask/bridge-controller@npm:^67.1.1, @metamask/bridge-controller@npm:^67.4.0": + version: 67.4.0 + resolution: "@metamask/bridge-controller@npm:67.4.0" dependencies: "@ethersproject/address": "npm:^5.7.0" "@ethersproject/bignumber": "npm:^5.7.0" @@ -7920,7 +7920,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/accounts-controller": "npm:^36.0.1" - "@metamask/assets-controllers": "npm:^100.0.2" + "@metamask/assets-controllers": "npm:^100.0.3" "@metamask/base-controller": "npm:^9.0.0" "@metamask/controller-utils": "npm:^11.19.0" "@metamask/gas-fee-controller": "npm:^26.0.3" @@ -7931,14 +7931,14 @@ __metadata: "@metamask/network-controller": "npm:^30.0.0" "@metamask/polling-controller": "npm:^16.0.3" "@metamask/profile-sync-controller": "npm:^27.1.0" - "@metamask/remote-feature-flag-controller": "npm:^4.0.0" + "@metamask/remote-feature-flag-controller": "npm:^4.1.0" "@metamask/snaps-controllers": "npm:^17.2.0" - "@metamask/transaction-controller": "npm:^62.18.0" + "@metamask/transaction-controller": "npm:^62.19.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/524ed3663b3e1f87a34171dc0b43b7b5751288d410d90b5d266923e71c974b47f3ec4e87d95baf6aa83ff41120b7625488f563e1c63048657241b24711089a95 + checksum: 10/a3db92c7a4212e693edb16b081dc24b4108ce7d81a94688e1de3512580b7e1c2ea38147e5b43e98b606a1556692af9f18d81a39669483284dc6c3dabe5f093b6 languageName: node linkType: hard @@ -9516,16 +9516,16 @@ __metadata: languageName: node linkType: hard -"@metamask/remote-feature-flag-controller@npm:^4.0.0": - version: 4.0.0 - resolution: "@metamask/remote-feature-flag-controller@npm:4.0.0" +"@metamask/remote-feature-flag-controller@npm:^4.0.0, @metamask/remote-feature-flag-controller@npm:^4.1.0": + version: 4.1.0 + resolution: "@metamask/remote-feature-flag-controller@npm:4.1.0" dependencies: "@metamask/base-controller": "npm:^9.0.0" - "@metamask/controller-utils": "npm:^11.17.0" + "@metamask/controller-utils": "npm:^11.19.0" "@metamask/messenger": "npm:^0.3.0" "@metamask/utils": "npm:^11.9.0" uuid: "npm:^8.3.2" - checksum: 10/5cb0e380d9b68666564f8e34add9a3c4cab1907bc3e0abddf37d1c06a6311f07cc6718baf4091aeab44c2d2cde378bb072e07a713546be3d5c9358fda5d75ab8 + checksum: 10/30122c316e788adc2abb6875eefef189946e2af469c1b217f8617ade17693666cde896e043fcb2a65874b2e62d4499b05456345dd2425dee6f9ea92f1f2d12e3 languageName: node linkType: hard @@ -10155,30 +10155,30 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-pay-controller@npm:^16.1.0": - version: 16.1.0 - resolution: "@metamask/transaction-pay-controller@npm:16.1.0" +"@metamask/transaction-pay-controller@npm:^16.1.1": + version: 16.1.1 + resolution: "@metamask/transaction-pay-controller@npm:16.1.1" dependencies: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/assets-controllers": "npm:^100.0.3" "@metamask/base-controller": "npm:^9.0.0" - "@metamask/bridge-controller": "npm:^67.2.0" + "@metamask/bridge-controller": "npm:^67.4.0" "@metamask/bridge-status-controller": "npm:^67.0.1" "@metamask/controller-utils": "npm:^11.19.0" "@metamask/gas-fee-controller": "npm:^26.0.3" "@metamask/messenger": "npm:^0.3.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/network-controller": "npm:^30.0.0" - "@metamask/remote-feature-flag-controller": "npm:^4.0.0" + "@metamask/remote-feature-flag-controller": "npm:^4.1.0" "@metamask/transaction-controller": "npm:^62.19.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/bfba59abf458bd7af18741b7697d2f0aedb3cc6a86c275626d4cbcac4f4b5b6b96ceb28809b7d1316e466e96c913f8cef3b2f1e73f154ea9aa3eb66856e5bc3d + checksum: 10/23f537240235afb68bb968f19ccbbcd638fbc6e8e04db2e9d6edaec873739de12261b55aff532b5f533b184a22b46e99ed76840287c5f729907367852a5be001 languageName: node linkType: hard @@ -35477,7 +35477,7 @@ __metadata: "@metamask/test-dapp-multichain": "npm:^0.17.1" "@metamask/test-dapp-solana": "npm:^0.3.0" "@metamask/transaction-controller": "npm:^62.19.0" - "@metamask/transaction-pay-controller": "npm:^16.1.0" + "@metamask/transaction-pay-controller": "npm:^16.1.1" "@metamask/tron-wallet-snap": "npm:^1.21.1" "@metamask/utils": "npm:^11.8.1" "@myx-trade/sdk": "npm:^0.1.265" From 5c7c134205c0aa6f99bee6df491b60dd8076af5f Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 3 Mar 2026 14:55:52 +0000 Subject: [PATCH 076/131] [skip ci] Bump version number to 3873 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1f752db5447..3a72ec7fa79 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.68.0" - versionCode 3868 + versionCode 3873 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 4f8062c7cdb..f03f9903849 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.68.0 - opts: is_expand: false - VERSION_NUMBER: 3868 + VERSION_NUMBER: 3873 - opts: is_expand: false FLASK_VERSION_NAME: 7.68.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3868 + FLASK_VERSION_NUMBER: 3873 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 7795389f7e5..daaee676e75 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3868; + CURRENT_PROJECT_VERSION = 3873; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3868; + CURRENT_PROJECT_VERSION = 3873; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3868; + CURRENT_PROJECT_VERSION = 3873; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3868; + CURRENT_PROJECT_VERSION = 3873; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3868; + CURRENT_PROJECT_VERSION = 3873; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3868; + CURRENT_PROJECT_VERSION = 3873; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 5f8c7c04007a36e7862f885e34e69cc4d87f957a Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:58:23 +0000 Subject: [PATCH 077/131] chore(runway): cherry-pick feat: stocks section in explore cp-7.68.0 (#26768) - feat: stocks section in explore cp-7.68.0 (#26426) ## **Description** This PR aims to add the stocks section in the explore page. In order to get this PR to the finish line, I have: - Updated the core package [here](https://github.com/MetaMask/core/pull/8019) to fix an issue with the `limit` param in the search endpoint request and support for a higher limit when calling with the QueryString `Ondo` - Asked the design team to add the corporate icon [here](https://github.com/MetaMask/metamask-mobile/pull/26492) - Raised the following issues in api-platform and got a fix - https://consensys.slack.com/archives/C03MLR70YSK/p1771489684959419 - https://consensys.slack.com/archives/C03MLR70YSK/p1771850443811719 - Added Geo-blocking (hardcoded for now but it should live on the API at some point) - Modified order of sections in explore page following @chaoticgoodpanda guidance - Modified predictions section to not be a carousel ## **Changelog** CHANGELOG entry: added stocks section to explore page ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-2635 & https://consensyssoftware.atlassian.net/browse/ASSETS-2632 ## **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** [Here](https://consensys.slack.com/archives/C07NF2K42LE/p1771849520486939) is a full video explaining the e2e functionality. https://github.com/user-attachments/assets/ac8c7c52-30c6-4913-9e69-7b8fe8e0db52 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Adds a new Explore section and full-screen view backed by a new search hook with geo-restriction logic and new navigation routes, which could affect what data users see and how filtering/refetch behaves. Refactors Trending Tokens full view into shared layout/components, so regressions could impact existing trending-token filtering UI and bottom sheets. > > **Overview** > Adds a new **Stocks** section to Explore, including a new `RWATokensFullView` screen/route and `useRwaTokens` hook that queries Ondo RWA assets (with production geo-blocking) and supports search, network, and sort filters. > > Refactors `TrendingTokensFullView` into the `UI/Trending` area and introduces shared `TokenListPageLayout`, `FilterBar`, and `useTokenListFilters` to unify header/search/filter behavior across token list full views; updates the network bottom sheet to take an explicit `networks` prop and adjusts token sorting to push missing market data to the end. Explore/QuickActions/predictions presentation, navigation, mocks, and smoke tests are updated to include the new Stocks section and new predictions row rendering. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2be0c5a0f95d227967addb09b5779495b5fa66d5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [f8c1bda](https://github.com/MetaMask/metamask-mobile/commit/f8c1bdab58fa27a50ac10c286337cce82f5a0bae) Co-authored-by: Juanmi <95381763+juanmigdr@users.noreply.github.com> --- app/components/Nav/Main/MainNavigator.js | 8 +- .../__snapshots__/MainNavigator.test.tsx.snap | 30 + .../RWATokensFullView.test.tsx | 214 ++++++ .../RWATokensFullView/RWATokensFullView.tsx | 64 ++ .../TrendingTokensFullView.test.tsx | 20 +- .../TrendingTokensFullView.tsx | 265 ++++++++ .../components/FilterBar/FilterBar.tsx | 117 ++++ .../TokenListPageLayout.tsx | 129 ++++ .../TrendingTokenRowItem.tsx | 10 - .../TrendingTokenNetworkBottomSheet.test.tsx | 101 ++- .../TrendingTokenNetworkBottomSheet.tsx | 5 +- .../useNetworkName/useNetworkName.test.ts | 89 +++ .../hooks/useNetworkName/useNetworkName.ts | 50 ++ .../hooks/useRwaTokens/useRwaTokens.test.ts | 291 +++++++++ .../hooks/useRwaTokens/useRwaTokens.ts | 147 +++++ .../useTokenListFilters.test.ts | 317 +++++++++ .../useTokenListFilters.ts | 235 +++++++ .../useTrendingSearch/useTrendingSearch.ts | 40 +- .../Trending/utils/sortTrendingTokens.test.ts | 302 +++------ .../UI/Trending/utils/sortTrendingTokens.ts | 73 ++- .../UI/Trending/utils/trendingNetworksList.ts | 15 + .../TrendingTokensFullView.tsx | 617 ------------------ .../ExploreSearchResults.test.tsx | 35 +- .../components/QuickActions/QuickActions.tsx | 46 +- .../hooks/useExploreSearch.test.ts | 17 +- .../Views/TrendingView/sections.config.tsx | 77 ++- app/constants/navigation/Routes.ts | 1 + app/core/NavigationService/types.ts | 1 + locales/languages/en.json | 1 + .../mock-responses/trending-api-mocks.ts | 26 + tests/component-view/renderers/trending.ts | 7 +- .../Trending/TrendingView.selectors.ts | 5 +- tests/page-objects/Trending/TrendingView.ts | 11 +- tests/smoke/trending/trending-feed.spec.ts | 47 +- 34 files changed, 2417 insertions(+), 996 deletions(-) create mode 100644 app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.test.tsx create mode 100644 app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx rename app/components/{Views/TrendingTokens => UI/Trending/Views}/TrendingTokensFullView/TrendingTokensFullView.test.tsx (95%) create mode 100644 app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.tsx create mode 100644 app/components/UI/Trending/components/FilterBar/FilterBar.tsx create mode 100644 app/components/UI/Trending/components/TokenListPageLayout/TokenListPageLayout.tsx create mode 100644 app/components/UI/Trending/hooks/useNetworkName/useNetworkName.test.ts create mode 100644 app/components/UI/Trending/hooks/useNetworkName/useNetworkName.ts create mode 100644 app/components/UI/Trending/hooks/useRwaTokens/useRwaTokens.test.ts create mode 100644 app/components/UI/Trending/hooks/useRwaTokens/useRwaTokens.ts create mode 100644 app/components/UI/Trending/hooks/useTokenListFilters/useTokenListFilters.test.ts create mode 100644 app/components/UI/Trending/hooks/useTokenListFilters/useTokenListFilters.ts delete mode 100644 app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index 809e9e923a0..85ba523818f 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -30,8 +30,9 @@ import AssetDetails from '../../Views/AssetDetails'; import AddAsset from '../../Views/AddAsset/AddAsset'; import NftFullView from '../../Views/NftFullView'; import TokensFullView from '../../Views/TokensFullView'; -import TrendingTokensFullView from '../../Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView'; import DeFiFullView from '../../Views/DeFiFullView'; +import TrendingTokensFullView from '../../UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView'; +import RWATokensFullView from '../../UI/Trending/Views/RWATokensFullView/RWATokensFullView'; import { RevealPrivateCredential } from '../../Views/RevealPrivateCredential'; import WalletConnectSessions from '../../Views/WalletConnectSessions'; import OfflineMode from '../../Views/OfflineMode'; @@ -1009,6 +1010,11 @@ const MainNavigator = () => { component={TrendingTokensFullView} options={slideFromRightAnimation} /> + + + + ({ + useNavigation: () => ({ + navigate: mockNavigate, + goBack: mockGoBack, + }), + createNavigatorFactory: () => ({}), +})); + +jest.mock('../../hooks/useRwaTokens/useRwaTokens'); +const mockUseRwaTokens = jest.mocked(useRwaTokens); + +jest.mock( + '../../components/TrendingTokensList/TrendingTokensList', + (): typeof TrendingTokensList => { + const { View, Text, ScrollView } = jest.requireActual('react-native'); + return ({ trendingTokens, refreshControl }) => ( + + {trendingTokens.map((token, index) => ( + + {token.name} + + ))} + + ); + }, +); + +const createMockToken = ( + overrides: Partial = {}, +): TrendingAsset => ({ + assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + name: 'OUSG Token', + symbol: 'OUSG', + decimals: 18, + price: '100.50', + aggregatedUsdVolume: 500000, + marketCap: 1000000000, + priceChangePct: { h24: '1.5' }, + ...overrides, +}); + +const arrangeMocks = () => { + const mockRefetch = jest.fn(); + + const setRwaTokensMock = (options: { + data?: TrendingAsset[]; + isLoading?: boolean; + }) => { + mockUseRwaTokens.mockReturnValue({ + data: options.data ?? [], + isLoading: options.isLoading ?? false, + refetch: mockRefetch, + }); + }; + + setRwaTokensMock({}); + + return { + mockRefetch, + mockGoBack, + mockNavigate, + setRwaTokensMock, + }; +}; + +describe('RWATokensFullView', () => { + const renderRWAFullView = () => + renderWithProvider( + + + , + { state: mockState }, + false, + ); + + beforeEach(() => { + jest.clearAllMocks(); + arrangeMocks(); + }); + + it('renders header with Stocks title', () => { + const { getByText } = renderRWAFullView(); + + expect(getByText('Stocks')).toBeOnTheScreen(); + }); + + it('does not render the time filter button', () => { + const { queryByTestId } = renderRWAFullView(); + + expect(queryByTestId('24h-button')).toBeNull(); + }); + + it('renders price-change and network filter buttons', () => { + const { getByTestId } = renderRWAFullView(); + + expect(getByTestId('price-change-button')).toBeOnTheScreen(); + expect(getByTestId('all-networks-button')).toBeOnTheScreen(); + }); + + it('navigates back when back button is pressed', async () => { + const mocks = arrangeMocks(); + const { getByTestId } = renderRWAFullView(); + + const backButton = getByTestId('rwa-tokens-header-back-button'); + await userEvent.press(backButton); + + expect(mocks.mockGoBack).toHaveBeenCalled(); + }); + + it('displays skeleton loader when loading', () => { + const mocks = arrangeMocks(); + mocks.setRwaTokensMock({ data: [], isLoading: true }); + + const { getByTestId } = renderRWAFullView(); + + expect(getByTestId(TEST_IDS.skeleton)).toBeOnTheScreen(); + }); + + it('displays empty error state when results are empty', () => { + const mocks = arrangeMocks(); + mocks.setRwaTokensMock({ data: [] }); + + const { getByTestId } = renderRWAFullView(); + + expect(getByTestId(TEST_IDS.emptyErrorState)).toBeOnTheScreen(); + }); + + it('displays stocks data from useRwaTokens', () => { + const stockTokens = [ + createMockToken({ + name: 'OUSG Token', + assetId: 'eip155:1/erc20:0xstock1', + }), + createMockToken({ + name: 'USDY Token', + assetId: 'eip155:1/erc20:0xstock2', + }), + ]; + + const mocks = arrangeMocks(); + mocks.setRwaTokensMock({ data: stockTokens }); + + const { getByText, getByTestId } = renderRWAFullView(); + + expect(getByTestId(TEST_IDS.tokensList)).toBeOnTheScreen(); + expect(getByText('OUSG Token')).toBeOnTheScreen(); + expect(getByText('USDY Token')).toBeOnTheScreen(); + }); + + it('calls refetch when pull-to-refresh is triggered', () => { + const stockTokens = [ + createMockToken({ name: 'OUSG', assetId: 'eip155:1/erc20:0xstock1' }), + ]; + + const mocks = arrangeMocks(); + mocks.setRwaTokensMock({ data: stockTokens }); + + const { getByTestId, UNSAFE_getByType } = renderRWAFullView(); + + expect(getByTestId(TEST_IDS.tokensList)).toBeOnTheScreen(); + const { RefreshControl } = jest.requireActual('react-native'); + const refreshControl = UNSAFE_getByType(RefreshControl); + fireEvent(refreshControl, 'refresh'); + + expect(mocks.mockRefetch).toHaveBeenCalledTimes(1); + }); + + it('opens network bottom sheet when button is pressed', async () => { + const { getByTestId } = renderRWAFullView(); + + const networkButton = getByTestId('all-networks-button'); + await userEvent.press(networkButton); + + expect( + getByTestId('trending-token-network-bottom-sheet'), + ).toBeOnTheScreen(); + }); + + it('opens price change bottom sheet when button is pressed', async () => { + const { getByTestId } = renderRWAFullView(); + + const priceChangeButton = getByTestId('price-change-button'); + await userEvent.press(priceChangeButton); + + expect( + getByTestId('trending-token-price-change-bottom-sheet'), + ).toBeOnTheScreen(); + }); +}); diff --git a/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx b/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx new file mode 100644 index 00000000000..f008e21f776 --- /dev/null +++ b/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx @@ -0,0 +1,64 @@ +import React, { useCallback, useMemo } from 'react'; +import { strings } from '../../../../../../locales/i18n'; +import type { TrendingAsset } from '@metamask/assets-controllers'; +import { + PriceChangeOption, + TimeOption, +} from '../../components/TrendingTokensBottomSheet'; +import { useRwaTokens } from '../../hooks/useRwaTokens/useRwaTokens'; +import { useTokenListFilters } from '../../hooks/useTokenListFilters/useTokenListFilters'; +import TokenListPageLayout from '../../components/TokenListPageLayout/TokenListPageLayout'; +import { RWA_NETWORKS_LIST } from '../../utils/trendingNetworksList'; + +const RWATokensFullView = () => { + const filters = useTokenListFilters({ + timeOption: TimeOption.TwentyFourHours, + }); + + const { + data: searchResults, + isLoading, + refetch: refetchStocks, + } = useRwaTokens({ + searchQuery: filters.searchQuery || undefined, + chainIds: filters.selectedNetwork, + sortTrendingTokensOptions: { + option: + filters.selectedPriceChangeOption ?? PriceChangeOption.PriceChange, + direction: filters.priceChangeSortDirection, + }, + }); + + const trendingTokens = useMemo( + () => (searchResults.length === 0 ? [] : searchResults), + [searchResults], + ); + + const handleRefresh = useCallback(async () => { + filters.setRefreshing(true); + try { + refetchStocks?.(); + } catch (error) { + console.warn('Failed to refresh stocks:', error); + } finally { + filters.setRefreshing(false); + } + }, [refetchStocks, filters]); + + return ( + + ); +}; + +RWATokensFullView.displayName = 'RWATokensFullView'; + +export default RWATokensFullView; diff --git a/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.test.tsx b/app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.test.tsx similarity index 95% rename from app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.test.tsx rename to app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.test.tsx index 4f72a2278c2..37b97b7dbb2 100644 --- a/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.test.tsx +++ b/app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.test.tsx @@ -1,22 +1,22 @@ import React from 'react'; import { render, userEvent, fireEvent } from '@testing-library/react-native'; import { Metrics, SafeAreaProvider } from 'react-native-safe-area-context'; -import renderWithProvider from '../../../../util/test/renderWithProvider'; +import renderWithProvider from '../../../../../util/test/renderWithProvider'; import TrendingTokensFullView, { TrendingTokensData, TrendingTokensDataProps, } from './TrendingTokensFullView'; import type { TrendingAsset } from '@metamask/assets-controllers'; -import { useTrendingSearch } from '../../../UI/Trending/hooks/useTrendingSearch/useTrendingSearch'; +import { useTrendingSearch } from '../../hooks/useTrendingSearch/useTrendingSearch'; import { TimeOption, PriceChangeOption, -} from '../../../UI/Trending/components/TrendingTokensBottomSheet'; -import { TrendingFilterContext } from '../../../UI/Trending/components/TrendingTokensList/TrendingTokensList'; +} from '../../components/TrendingTokensBottomSheet'; +import { TrendingFilterContext } from '../../components/TrendingTokensList/TrendingTokensList'; -import { useTrendingRequest } from '../../../UI/Trending/hooks/useTrendingRequest/useTrendingRequest'; -import type TrendingTokensList from '../../../UI/Trending/components/TrendingTokensList'; -import mockState from '../../../../util/test/initial-root-state'; +import { useTrendingRequest } from '../../hooks/useTrendingRequest/useTrendingRequest'; +import type TrendingTokensList from '../../components/TrendingTokensList'; +import mockState from '../../../../../util/test/initial-root-state'; const TEST_IDS = { skeleton: 'trending-tokens-skeleton', @@ -42,14 +42,14 @@ jest.mock('@react-navigation/native', () => ({ createNavigatorFactory: () => ({}), })); -jest.mock('../../../UI/Trending/hooks/useTrendingRequest/useTrendingRequest'); +jest.mock('../../hooks/useTrendingRequest/useTrendingRequest'); const mockUseTrendingRequest = jest.mocked(useTrendingRequest); -jest.mock('../../../UI/Trending/hooks/useTrendingSearch/useTrendingSearch'); +jest.mock('../../hooks/useTrendingSearch/useTrendingSearch'); const mockUseTrendingSearch = jest.mocked(useTrendingSearch); jest.mock( - '../../../UI/Trending/components/TrendingTokensList/TrendingTokensList', + '../../components/TrendingTokensList/TrendingTokensList', (): typeof TrendingTokensList => { const { View, Text, ScrollView } = jest.requireActual('react-native'); return ({ trendingTokens, refreshControl }) => ( diff --git a/app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.tsx b/app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.tsx new file mode 100644 index 00000000000..3d711c9d538 --- /dev/null +++ b/app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.tsx @@ -0,0 +1,265 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { View, TouchableOpacity, RefreshControl } from 'react-native'; +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import { strings } from '../../../../../../locales/i18n'; +import TrendingTokensList, { + TrendingFilterContext, +} from '../../components/TrendingTokensList/TrendingTokensList'; +import TrendingTokensSkeleton from '../../components/TrendingTokenSkeleton/TrendingTokensSkeleton'; +import { + SortTrendingBy, + type TrendingAsset, +} from '@metamask/assets-controllers'; +import Icon, { + IconName, + IconColor, + IconSize, +} from '../../../../../component-library/components/Icons/Icon'; +import Text from '../../../../../component-library/components/Texts/Text'; +import { + TrendingTokenTimeBottomSheet, + PriceChangeOption, + TimeOption, +} from '../../components/TrendingTokensBottomSheet'; +import { sortTrendingTokens } from '../../utils/sortTrendingTokens'; +import { useTrendingSearch } from '../../hooks/useTrendingSearch/useTrendingSearch'; +import { useTokenListFilters } from '../../hooks/useTokenListFilters/useTokenListFilters'; +import EmptyErrorTrendingState from '../../../../Views/TrendingView/components/EmptyErrorState/EmptyErrorTrendingState'; +import EmptySearchResultState from '../../../../Views/TrendingView/components/EmptyErrorState/EmptySearchResultState'; +import TrendingFeedSessionManager from '../../services/TrendingFeedSessionManager'; +import { useSearchTracking } from '../../hooks/useSearchTracking/useSearchTracking'; +import TokenListPageLayout from '../../components/TokenListPageLayout/TokenListPageLayout'; +import { TRENDING_NETWORKS_LIST } from '../../utils/trendingNetworksList'; +import type { Theme } from '../../../../../util/theme/models'; + +export interface TrendingTokensDataProps { + isLoading: boolean; + refreshing: boolean; + trendingTokens: TrendingAsset[]; + handleRefresh: () => void; + selectedTimeOption: TimeOption; + filterContext: TrendingFilterContext; + theme: Theme; + + search: { + searchResults: TrendingAsset[]; + searchQuery: string; + }; +} + +export const TrendingTokensData = (props: TrendingTokensDataProps) => { + const { + isLoading, + refreshing, + trendingTokens, + search, + handleRefresh, + selectedTimeOption, + filterContext, + theme, + } = props; + + const tw = useTailwind(); + + const isSearching = search.searchQuery.trim().length > 0; + const hasSearchResults = search.searchResults.length > 0; + + if (isLoading) { + return ( + + {Array.from({ length: 12 }).map((_, index) => ( + + ))} + + ); + } + + if (isSearching && !hasSearchResults) { + return ; + } + + if (!isSearching && !hasSearchResults) { + return ; + } + + return ( + + + } + /> + + ); +}; + +const TrendingTokensFullView = () => { + const tw = useTailwind(); + const sessionManager = TrendingFeedSessionManager.getInstance(); + const filters = useTokenListFilters(); + + const [sortBy, setSortBy] = useState(undefined); + const [showTimeBottomSheet, setShowTimeBottomSheet] = useState(false); + + const { + data: searchResults, + isLoading, + refetch: refetchTokensSection, + } = useTrendingSearch({ + searchQuery: filters.searchQuery || undefined, + sortBy, + chainIds: filters.selectedNetwork, + }); + + const trendingTokens = useMemo(() => { + if (searchResults.length === 0) { + return []; + } + + if (filters.searchQuery?.trim()) { + return searchResults; + } + + if (!filters.selectedPriceChangeOption) { + return searchResults; + } + + return sortTrendingTokens( + searchResults, + filters.selectedPriceChangeOption, + filters.priceChangeSortDirection, + filters.selectedTimeOption, + ); + }, [ + searchResults, + filters.searchQuery, + filters.selectedPriceChangeOption, + filters.priceChangeSortDirection, + filters.selectedTimeOption, + ]); + + useSearchTracking({ + searchQuery: filters.searchQuery, + resultsCount: trendingTokens.length, + isLoading, + timeFilter: filters.selectedTimeOption, + sortOption: + filters.selectedPriceChangeOption || PriceChangeOption.PriceChange, + networkFilter: + filters.selectedNetwork && filters.selectedNetwork.length > 0 + ? filters.selectedNetwork[0] + : 'all', + }); + + const { + selectedTimeOption, + selectedPriceChangeOption, + selectedNetwork, + setSelectedTimeOption, + } = filters; + + const handleTimeSelect = useCallback( + (selectedSortBy: SortTrendingBy, timeOption: TimeOption) => { + const previousValue = selectedTimeOption; + setSortBy(selectedSortBy); + setSelectedTimeOption(timeOption); + + if (timeOption !== previousValue) { + sessionManager.trackFilterChange({ + filter_type: 'time', + previous_value: previousValue, + new_value: timeOption, + time_filter: timeOption, + sort_option: + selectedPriceChangeOption || PriceChangeOption.PriceChange, + network_filter: + selectedNetwork && selectedNetwork.length > 0 + ? selectedNetwork[0] + : 'all', + }); + } + }, + [ + selectedTimeOption, + selectedPriceChangeOption, + selectedNetwork, + setSelectedTimeOption, + sessionManager, + ], + ); + + const handle24hPress = useCallback(() => { + setShowTimeBottomSheet(true); + }, []); + + const { setRefreshing } = filters; + + const handleRefresh = useCallback(async () => { + setRefreshing(true); + try { + refetchTokensSection?.(); + } catch (error) { + console.warn('Failed to refresh trending tokens:', error); + } finally { + setRefreshing(false); + } + }, [refetchTokensSection, setRefreshing]); + + const timeFilterButton = ( + + + + {filters.selectedTimeOption} + + + + + ); + + return ( + setShowTimeBottomSheet(false)} + onTimeSelect={handleTimeSelect} + selectedTime={filters.selectedTimeOption} + /> + } + /> + ); +}; + +TrendingTokensFullView.displayName = 'TrendingTokensFullView'; + +export default TrendingTokensFullView; diff --git a/app/components/UI/Trending/components/FilterBar/FilterBar.tsx b/app/components/UI/Trending/components/FilterBar/FilterBar.tsx new file mode 100644 index 00000000000..3ecb916d3a5 --- /dev/null +++ b/app/components/UI/Trending/components/FilterBar/FilterBar.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { View, TouchableOpacity } from 'react-native'; +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import Icon, { + IconName, + IconColor, + IconSize, +} from '../../../../../component-library/components/Icons/Icon'; +import Text from '../../../../../component-library/components/Texts/Text'; + +interface FilterButtonProps { + testID: string; + label: string; + onPress: () => void; + disabled?: boolean; + numberOfLines?: number; + ellipsizeMode?: 'tail' | 'head' | 'middle' | 'clip'; + /** Extra horizontal padding (px-3) vs default (p-2) */ + wide?: boolean; +} + +const FilterButton: React.FC = ({ + testID, + label, + onPress, + disabled = false, + numberOfLines, + ellipsizeMode, + wide = false, +}) => { + const tw = useTailwind(); + + return ( + + + + {label} + + + + + ); +}; + +export interface FilterBarProps { + priceChangeButtonText: string; + onPriceChangePress: () => void; + isPriceChangeDisabled?: boolean; + + networkName: string; + onNetworkPress: () => void; + + /** Optional extra filter buttons rendered after the network button */ + extraFilters?: React.ReactNode; +} + +/** + * Shared filter toolbar used in token list views. + * Renders price-change and network filter buttons with an optional slot + * for view-specific extras (e.g., time filter in TrendingTokensFullView). + */ +const FilterBar: React.FC = ({ + priceChangeButtonText, + onPriceChangePress, + isPriceChangeDisabled = false, + networkName, + onNetworkPress, + extraFilters, +}) => { + const tw = useTailwind(); + + return ( + + + + + + {extraFilters} + + + + ); +}; + +FilterBar.displayName = 'FilterBar'; + +export default FilterBar; diff --git a/app/components/UI/Trending/components/TokenListPageLayout/TokenListPageLayout.tsx b/app/components/UI/Trending/components/TokenListPageLayout/TokenListPageLayout.tsx new file mode 100644 index 00000000000..6dfcdab0905 --- /dev/null +++ b/app/components/UI/Trending/components/TokenListPageLayout/TokenListPageLayout.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { Platform, View } from 'react-native'; +import { + SafeAreaView, + useSafeAreaInsets, +} from 'react-native-safe-area-context'; +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import { useAppThemeFromContext } from '../../../../../util/theme'; +import { TrendingListHeader } from '../TrendingListHeader'; +import FilterBar from '../FilterBar/FilterBar'; +import { + TrendingTokenNetworkBottomSheet, + TrendingTokenPriceChangeBottomSheet, +} from '../TrendingTokensBottomSheet'; +import { TrendingTokensData } from '../../Views/TrendingTokensFullView/TrendingTokensFullView'; +import type { TrendingAsset } from '@metamask/assets-controllers'; +import type { ProcessedNetwork } from '../../../../hooks/useNetworksByNamespace/useNetworksByNamespace'; +import type { TokenListFilters } from '../../hooks/useTokenListFilters/useTokenListFilters'; + +export interface TokenListPageLayoutProps { + /** Page title displayed in the header */ + title: string; + /** Base testID used to derive sub-component testIDs */ + testID: string; + /** Filter state & handlers from useTokenListFilters */ + filters: TokenListFilters; + /** Token data to display */ + tokens: TrendingAsset[]; + /** Search results (may differ from tokens if client-side filtering is applied) */ + searchResults: TrendingAsset[]; + /** Whether data is currently loading */ + isLoading: boolean; + /** Callback to trigger data refetch */ + onRefresh: () => void; + /** Networks to show in the network filter bottom sheet */ + allowedNetworks: ProcessedNetwork[]; + /** Optional extra filter buttons (e.g., time filter) */ + extraFilters?: React.ReactNode; + /** Optional extra bottom sheets rendered at the end */ + extraBottomSheets?: React.ReactNode; +} + +/** + * Shared page layout for token list full-screen views. + * + * Renders SafeAreaView shell, TrendingListHeader with search, + * FilterBar with price-change / network buttons, + * TrendingTokensData (skeleton / empty states / token list), + * and Network & price-change bottom sheets. + */ +const TokenListPageLayout: React.FC = ({ + title, + testID, + filters, + tokens, + searchResults, + isLoading, + onRefresh, + allowedNetworks, + extraFilters, + extraBottomSheets, +}) => { + const tw = useTailwind(); + const theme = useAppThemeFromContext(); + const insets = useSafeAreaInsets(); + + return ( + + + + + + {!filters.isSearchVisible ? ( + + ) : null} + + + + filters.setShowNetworkBottomSheet(false)} + onNetworkSelect={filters.handleNetworkSelect} + selectedNetwork={filters.selectedNetwork} + networks={allowedNetworks} + /> + filters.setShowPriceChangeBottomSheet(false)} + onPriceChangeSelect={filters.handlePriceChangeSelect} + selectedOption={filters.selectedPriceChangeOption} + sortDirection={filters.priceChangeSortDirection} + /> + {extraBottomSheets} + + ); +}; + +TokenListPageLayout.displayName = 'TokenListPageLayout'; + +export default TokenListPageLayout; diff --git a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx index d4682486296..8a6c17a3d7c 100644 --- a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx +++ b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx @@ -40,12 +40,9 @@ import { formatPriceWithSubscriptNotation } from '../../../Predict/utils/format' import { TimeOption, PriceChangeOption } from '../TrendingTokensBottomSheet'; import { selectNetworkConfigurationsByCaipChainId } from '../../../../../selectors/networkController'; import { getTrendingTokenImageUrl } from '../../utils/getTrendingTokenImageUrl'; -import { useRWAToken } from '../../../Bridge/hooks/useRWAToken'; -import StockBadge from '../../../shared/StockBadge'; import { useAddPopularNetwork } from '../../../../hooks/useAddPopularNetwork'; import TrendingFeedSessionManager from '../../services/TrendingFeedSessionManager'; import type { TrendingFilterContext } from '../TrendingTokensList/TrendingTokensList'; -import { BridgeToken } from '../../../Bridge/types'; import { TokenDetailsSource } from '../../../TokenDetails/constants/constants'; /** @@ -181,7 +178,6 @@ const TrendingTokenRowItem = ({ selectNetworkConfigurationsByCaipChainId, ); const { addPopularNetwork } = useAddPopularNetwork(); - const { isStockToken } = useRWAToken(); const sessionManager = TrendingFeedSessionManager.getInstance(); // Memoize derived values @@ -310,12 +306,6 @@ const TrendingTokenRowItem = ({ token.aggregatedUsdVolume ?? 0, )} - {isStockToken(token as unknown as BridgeToken) && ( - - )} diff --git a/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.test.tsx b/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.test.tsx index c298f45f5f8..12564d914ef 100644 --- a/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.test.tsx +++ b/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.test.tsx @@ -15,33 +15,26 @@ jest.mock('../../../../../util/networks', () => ({ mockGetNetworkImageSource(params), })); -// Mock the TRENDING_NETWORKS_LIST constant -jest.mock('../../utils/trendingNetworksList', () => { - const mockNetworks: ProcessedNetwork[] = [ - { - id: 'eip155:1', - name: 'Ethereum Mainnet', - caipChainId: 'eip155:1' as CaipChainId, - imageSource: { - uri: 'https://example.com/ethereum.png', - } as ImageSourcePropType, - isSelected: false, - }, - { - id: 'eip155:137', - name: 'Polygon', - caipChainId: 'eip155:137' as CaipChainId, - imageSource: { - uri: 'https://example.com/polygon.png', - } as ImageSourcePropType, - isSelected: false, - }, - ]; - - return { - TRENDING_NETWORKS_LIST: mockNetworks, - }; -}); +const mockNetworks: ProcessedNetwork[] = [ + { + id: 'eip155:1', + name: 'Ethereum Mainnet', + caipChainId: 'eip155:1' as CaipChainId, + imageSource: { + uri: 'https://example.com/ethereum.png', + } as ImageSourcePropType, + isSelected: false, + }, + { + id: 'eip155:137', + name: 'Polygon', + caipChainId: 'eip155:137' as CaipChainId, + imageSource: { + uri: 'https://example.com/polygon.png', + } as ImageSourcePropType, + isSelected: false, + }, +]; let storedOnClose: (() => void) | undefined; @@ -278,7 +271,11 @@ describe('TrendingTokenNetworkBottomSheet', () => { it('renders with default "All networks" selected', () => { const { getByText, getByTestId } = renderWithProvider( - , + , { state: mockState }, false, ); @@ -290,7 +287,11 @@ describe('TrendingTokenNetworkBottomSheet', () => { it('renders all network options', () => { const { getByText } = renderWithProvider( - , + , { state: mockState }, false, ); @@ -308,6 +309,7 @@ describe('TrendingTokenNetworkBottomSheet', () => { isVisible onClose={mockOnClose} onNetworkSelect={mockOnNetworkSelect} + networks={mockNetworks} />, { state: mockState }, false, @@ -329,6 +331,7 @@ describe('TrendingTokenNetworkBottomSheet', () => { isVisible onClose={mockOnClose} onNetworkSelect={mockOnNetworkSelect} + networks={mockNetworks} />, { state: mockState }, false, @@ -350,6 +353,7 @@ describe('TrendingTokenNetworkBottomSheet', () => { isVisible onClose={mockOnClose} onNetworkSelect={mockOnNetworkSelect} + networks={mockNetworks} />, { state: mockState }, false, @@ -366,7 +370,11 @@ describe('TrendingTokenNetworkBottomSheet', () => { it('calls onClose when close button is pressed', () => { const { getByTestId } = renderWithProvider( - , + , { state: mockState }, false, ); @@ -380,7 +388,11 @@ describe('TrendingTokenNetworkBottomSheet', () => { it('calls onClose when sheet is closed via onClose callback', () => { renderWithProvider( - , + , { state: mockState }, false, ); @@ -398,6 +410,7 @@ describe('TrendingTokenNetworkBottomSheet', () => { isVisible onClose={mockOnClose} selectedNetwork={['eip155:1']} + networks={mockNetworks} />, { state: mockState }, false, @@ -409,7 +422,11 @@ describe('TrendingTokenNetworkBottomSheet', () => { it('displays selection indicator for "All networks" when selected', () => { const { getByText, getByTestId } = renderWithProvider( - , + , { state: mockState }, false, ); @@ -420,7 +437,11 @@ describe('TrendingTokenNetworkBottomSheet', () => { it('renders network avatars with correct props', () => { const { getByTestId } = renderWithProvider( - , + , { state: mockState }, false, ); @@ -440,7 +461,11 @@ describe('TrendingTokenNetworkBottomSheet', () => { it('renders global icon for "All networks" option', () => { const { getByTestId } = renderWithProvider( - , + , { state: mockState }, false, ); @@ -453,6 +478,7 @@ describe('TrendingTokenNetworkBottomSheet', () => { , { state: mockState }, false, @@ -466,6 +492,7 @@ describe('TrendingTokenNetworkBottomSheet', () => { , { state: mockState }, false, @@ -474,7 +501,11 @@ describe('TrendingTokenNetworkBottomSheet', () => { expect(mockOnOpenBottomSheet).not.toHaveBeenCalled(); rerender( - , + , ); expect(mockOnOpenBottomSheet).toHaveBeenCalled(); diff --git a/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.tsx b/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.tsx index e8e6c7a07b7..06eca9eaa0e 100644 --- a/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.tsx +++ b/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.tsx @@ -18,7 +18,6 @@ import Cell, { import { strings } from '../../../../../../locales/i18n'; import { ProcessedNetwork } from '../../../../hooks/useNetworksByNamespace/useNetworksByNamespace'; import { CaipChainId } from '@metamask/utils'; -import { TRENDING_NETWORKS_LIST } from '../../utils/trendingNetworksList'; export enum NetworkOption { AllNetworks = 'all', @@ -29,6 +28,8 @@ export interface TrendingTokenNetworkBottomSheetProps { onClose: () => void; onNetworkSelect?: (chainIds: CaipChainId[] | null) => void; selectedNetwork?: CaipChainId[] | null; + /** Networks to display in the bottom sheet */ + networks: ProcessedNetwork[]; } const TrendingTokenNetworkBottomSheet: React.FC< @@ -38,9 +39,9 @@ const TrendingTokenNetworkBottomSheet: React.FC< onClose, onNetworkSelect, selectedNetwork: initialSelectedNetwork, + networks, }) => { const sheetRef = useRef(null); - const networks = TRENDING_NETWORKS_LIST; // Default to "All networks" if no selection const [selectedNetwork, setSelectedNetwork] = useState< diff --git a/app/components/UI/Trending/hooks/useNetworkName/useNetworkName.test.ts b/app/components/UI/Trending/hooks/useNetworkName/useNetworkName.test.ts new file mode 100644 index 00000000000..ed5382d02d1 --- /dev/null +++ b/app/components/UI/Trending/hooks/useNetworkName/useNetworkName.test.ts @@ -0,0 +1,89 @@ +import { useNetworkName } from './useNetworkName'; +import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; +import type { CaipChainId } from '@metamask/utils'; + +jest.mock('../../../../../selectors/networkController', () => ({ + ...jest.requireActual('../../../../../selectors/networkController'), + selectNetworkConfigurationsByCaipChainId: jest.fn(), +})); + +import { selectNetworkConfigurationsByCaipChainId } from '../../../../../selectors/networkController'; + +const mockSelector = jest.mocked(selectNetworkConfigurationsByCaipChainId); + +const renderUseNetworkName = (selectedNetwork: CaipChainId[] | null) => + renderHookWithProvider(() => useNetworkName(selectedNetwork)); + +describe('useNetworkName', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockSelector.mockImplementation(() => ({})); + }); + + it('returns "All networks" when selectedNetwork is null', () => { + const { result } = renderUseNetworkName(null); + + expect(result.current).toBe('All networks'); + }); + + it('returns "All networks" when selectedNetwork is empty', () => { + const { result } = renderUseNetworkName([]); + + expect(result.current).toBe('All networks'); + }); + + it('returns network name from user-configured networks', () => { + const caipId = 'eip155:42161' as CaipChainId; + mockSelector.mockImplementation( + () => + ({ + [caipId]: { name: 'My Custom Arbitrum' }, + }) as ReturnType, + ); + + const { result } = renderUseNetworkName([caipId]); + + expect(result.current).toBe('My Custom Arbitrum'); + }); + + it('falls back to PopularList nickname for eip155 chain not in user config', () => { + const avalancheCaipId = 'eip155:43114' as CaipChainId; + + const { result } = renderUseNetworkName([avalancheCaipId]); + + expect(result.current).toBe('Avalanche'); + }); + + it('returns "All networks" for eip155 chain not in user config or PopularList', () => { + const unknownCaipId = 'eip155:99999' as CaipChainId; + + const { result } = renderUseNetworkName([unknownCaipId]); + + expect(result.current).toBe('All networks'); + }); + + it('returns "All networks" for non-eip155 namespace not in user config', () => { + const solanaId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' as CaipChainId; + + const { result } = renderUseNetworkName([solanaId]); + + expect(result.current).toBe('All networks'); + }); + + it('returns "All networks" when CAIP chain ID parsing fails', () => { + const malformedId = 'not-a-valid-caip-id' as CaipChainId; + + const { result } = renderUseNetworkName([malformedId]); + + expect(result.current).toBe('All networks'); + }); + + it('uses only the first element when multiple chain IDs are provided', () => { + const caipId = 'eip155:43114' as CaipChainId; + const secondId = 'eip155:42161' as CaipChainId; + + const { result } = renderUseNetworkName([caipId, secondId]); + + expect(result.current).toBe('Avalanche'); + }); +}); diff --git a/app/components/UI/Trending/hooks/useNetworkName/useNetworkName.ts b/app/components/UI/Trending/hooks/useNetworkName/useNetworkName.ts new file mode 100644 index 00000000000..98348691e23 --- /dev/null +++ b/app/components/UI/Trending/hooks/useNetworkName/useNetworkName.ts @@ -0,0 +1,50 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { CaipChainId, Hex, parseCaipChainId } from '@metamask/utils'; +import { selectNetworkConfigurationsByCaipChainId } from '../../../../../selectors/networkController'; +import { PopularList } from '../../../../../util/networks/customNetworks'; +import { strings } from '../../../../../../locales/i18n'; + +/** + * Resolves a human-readable network name from selected CAIP chain IDs. + * + * Checks user-configured networks first, then falls back to PopularList, + * and finally defaults to "All networks". + */ +export const useNetworkName = ( + selectedNetwork: CaipChainId[] | null, +): string => { + const networkConfigurations = useSelector( + selectNetworkConfigurationsByCaipChainId, + ); + + return useMemo(() => { + if (!selectedNetwork || selectedNetwork.length === 0) { + return strings('trending.all_networks'); + } + + const selectedNetworkChainId = selectedNetwork[0]; + + const networkConfig = networkConfigurations[selectedNetworkChainId]; + if (networkConfig?.name) { + return networkConfig.name; + } + + try { + const { namespace, reference } = parseCaipChainId(selectedNetworkChainId); + if (namespace === 'eip155') { + const hexChainId = `0x${Number(reference).toString(16)}` as Hex; + const popularNetwork = PopularList.find( + (network) => network.chainId === hexChainId, + ); + if (popularNetwork?.nickname) { + return popularNetwork.nickname; + } + } + } catch { + // If parsing fails, fall through to default + } + + return strings('trending.all_networks'); + }, [selectedNetwork, networkConfigurations]); +}; diff --git a/app/components/UI/Trending/hooks/useRwaTokens/useRwaTokens.test.ts b/app/components/UI/Trending/hooks/useRwaTokens/useRwaTokens.test.ts new file mode 100644 index 00000000000..b4ef9c449be --- /dev/null +++ b/app/components/UI/Trending/hooks/useRwaTokens/useRwaTokens.test.ts @@ -0,0 +1,291 @@ +import { useRwaTokens } from './useRwaTokens'; +import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; +import { useSearchRequest } from '../useSearchRequest/useSearchRequest'; +import { sortTrendingTokens } from '../../utils/sortTrendingTokens'; +import { + PriceChangeOption, + SortDirection, +} from '../../components/TrendingTokensBottomSheet'; +import { RWA_CHAIN_IDS } from '../../utils/trendingNetworksList'; +import type { CaipChainId } from '@metamask/utils'; + +jest.mock('../useSearchRequest/useSearchRequest'); +jest.mock('../../utils/sortTrendingTokens'); + +const mockUseSearchRequest = jest.mocked(useSearchRequest); +const mockSortTrendingTokens = jest.mocked(sortTrendingTokens); + +const mockRefetch = jest.fn(); + +const createSearchResult = (overrides: Record = {}) => ({ + assetId: 'eip155:1/erc20:0xaaa' as CaipChainId, + symbol: 'OUSG', + name: 'OUSG Token', + decimals: 18, + price: '100.50', + aggregatedUsdVolume: 500000, + marketCap: 1000000000, + pricePercentChange1d: '1.5', + rwaData: { underlyingAsset: 'US Treasury' }, + ...overrides, +}); + +const arrangeMocks = (options?: { + results?: ReturnType[]; + isLoading?: boolean; +}) => { + mockUseSearchRequest.mockReturnValue({ + results: (options?.results ?? []) as ReturnType< + typeof useSearchRequest + >['results'], + isLoading: options?.isLoading ?? false, + error: null, + search: mockRefetch, + }); + mockSortTrendingTokens.mockImplementation((tokens) => tokens); +}; + +const NON_RESTRICTED_GEO_STATE = { + state: { fiatOrders: { detectedGeolocation: 'AR' } }, +}; + +const renderHookWithGeo = ( + geolocation: string | undefined, + hookOpts?: Parameters[0], +) => + renderHookWithProvider(() => useRwaTokens(hookOpts), { + state: { fiatOrders: { detectedGeolocation: geolocation } }, + }); + +describe('useRwaTokens', () => { + beforeEach(() => { + jest.clearAllMocks(); + arrangeMocks(); + }); + + it('calls useSearchRequest with correct defaults', () => { + renderHookWithProvider(() => useRwaTokens(), NON_RESTRICTED_GEO_STATE); + + expect(mockUseSearchRequest).toHaveBeenCalledWith({ + query: '(Ondo Tokenized)', + limit: 500, + chainIds: RWA_CHAIN_IDS, + includeMarketData: true, + }); + }); + + it('uses provided chainIds instead of defaults', () => { + const customChainIds: CaipChainId[] = ['eip155:137' as CaipChainId]; + + renderHookWithProvider( + () => useRwaTokens({ chainIds: customChainIds }), + NON_RESTRICTED_GEO_STATE, + ); + + expect(mockUseSearchRequest).toHaveBeenCalledWith( + expect.objectContaining({ chainIds: customChainIds }), + ); + }); + + it('defaults to RWA_CHAIN_IDS when chainIds is null', () => { + renderHookWithProvider( + () => useRwaTokens({ chainIds: null }), + NON_RESTRICTED_GEO_STATE, + ); + + expect(mockUseSearchRequest).toHaveBeenCalledWith( + expect.objectContaining({ chainIds: RWA_CHAIN_IDS }), + ); + }); + + it('filters out assets without rwaData', () => { + const rwaAsset = createSearchResult({ assetId: 'eip155:1/erc20:0x111' }); + const nonRwaAsset = createSearchResult({ + assetId: 'eip155:1/erc20:0x222', + symbol: 'ETH', + name: 'Ethereum', + rwaData: undefined, + }); + arrangeMocks({ results: [rwaAsset, nonRwaAsset] }); + + const { result } = renderHookWithProvider( + () => useRwaTokens(), + NON_RESTRICTED_GEO_STATE, + ); + + expect(result.current.data).toHaveLength(1); + expect(result.current.data[0].symbol).toBe('OUSG'); + }); + + it('normalizes pricePercentChange1d to priceChangePct.h24', () => { + arrangeMocks({ + results: [createSearchResult({ pricePercentChange1d: '3.14' })], + }); + + const { result } = renderHookWithProvider( + () => useRwaTokens(), + NON_RESTRICTED_GEO_STATE, + ); + + expect(result.current.data[0].priceChangePct).toEqual({ h24: '3.14' }); + }); + + it('sorts results with default options when no searchQuery', () => { + const results = [ + createSearchResult({ assetId: 'eip155:1/erc20:0x111', symbol: 'A' }), + createSearchResult({ assetId: 'eip155:1/erc20:0x222', symbol: 'B' }), + ]; + arrangeMocks({ results }); + + renderHookWithProvider(() => useRwaTokens(), NON_RESTRICTED_GEO_STATE); + + expect(mockSortTrendingTokens).toHaveBeenCalledWith( + expect.any(Array), + PriceChangeOption.PriceChange, + SortDirection.Descending, + ); + }); + + it('sorts results with custom sort options', () => { + arrangeMocks({ results: [createSearchResult()] }); + + renderHookWithProvider( + () => + useRwaTokens({ + sortTrendingTokensOptions: { + option: PriceChangeOption.Volume, + direction: SortDirection.Ascending, + }, + }), + NON_RESTRICTED_GEO_STATE, + ); + + expect(mockSortTrendingTokens).toHaveBeenCalledWith( + expect.any(Array), + PriceChangeOption.Volume, + SortDirection.Ascending, + ); + }); + + it('applies fuse search instead of sorting when searchQuery is provided', () => { + const results = [ + createSearchResult({ + assetId: 'eip155:1/erc20:0x111', + symbol: 'OUSG', + name: 'OUSG Token', + }), + createSearchResult({ + assetId: 'eip155:1/erc20:0x222', + symbol: 'USDY', + name: 'USDY Token', + }), + ]; + arrangeMocks({ results }); + + const { result } = renderHookWithProvider( + () => useRwaTokens({ searchQuery: 'OUSG' }), + NON_RESTRICTED_GEO_STATE, + ); + + expect(mockSortTrendingTokens).not.toHaveBeenCalled(); + expect(result.current.data).toHaveLength(1); + expect(result.current.data[0].symbol).toBe('OUSG'); + }); + + it('returns empty array when no results match search query', () => { + arrangeMocks({ results: [createSearchResult()] }); + + const { result } = renderHookWithProvider( + () => useRwaTokens({ searchQuery: 'nonexistent' }), + NON_RESTRICTED_GEO_STATE, + ); + + expect(result.current.data).toHaveLength(0); + }); + + it('passes through isLoading from useSearchRequest', () => { + arrangeMocks({ isLoading: true }); + + const { result } = renderHookWithProvider( + () => useRwaTokens(), + NON_RESTRICTED_GEO_STATE, + ); + + expect(result.current.isLoading).toBe(true); + }); + + it('exposes refetch from useSearchRequest', () => { + const { result } = renderHookWithProvider( + () => useRwaTokens(), + NON_RESTRICTED_GEO_STATE, + ); + + result.current.refetch(); + + expect(mockRefetch).toHaveBeenCalledTimes(1); + }); + + it('returns empty data when useSearchRequest returns no results', () => { + arrangeMocks({ results: [] }); + + const { result } = renderHookWithProvider( + () => useRwaTokens(), + NON_RESTRICTED_GEO_STATE, + ); + + expect(result.current.data).toEqual([]); + }); + + describe('geo-restriction (production mode)', () => { + const originalDev = (globalThis as Record).__DEV__; + + beforeEach(() => { + (globalThis as Record).__DEV__ = false; + }); + + afterEach(() => { + (globalThis as Record).__DEV__ = originalDev; + }); + + it('returns empty data for a restricted country', () => { + arrangeMocks({ results: [createSearchResult()] }); + + const { result } = renderHookWithGeo('US'); + + expect(result.current.data).toEqual([]); + }); + + it('returns empty data when geolocation is unknown', () => { + arrangeMocks({ results: [createSearchResult()] }); + + const { result } = renderHookWithGeo(undefined); + + expect(result.current.data).toEqual([]); + }); + + it('returns data for a non-restricted country', () => { + arrangeMocks({ results: [createSearchResult()] }); + + const { result } = renderHookWithGeo('AR'); + + expect(result.current.data).toHaveLength(1); + expect(result.current.data[0].symbol).toBe('OUSG'); + }); + + it('handles region suffixes correctly', () => { + arrangeMocks({ results: [createSearchResult()] }); + + const { result } = renderHookWithGeo('GB-ENG'); + + expect(result.current.data).toEqual([]); + }); + + it('sends empty query to search API when restricted', () => { + renderHookWithGeo('US'); + + expect(mockUseSearchRequest).toHaveBeenCalledWith( + expect.objectContaining({ query: '' }), + ); + }); + }); +}); diff --git a/app/components/UI/Trending/hooks/useRwaTokens/useRwaTokens.ts b/app/components/UI/Trending/hooks/useRwaTokens/useRwaTokens.ts new file mode 100644 index 00000000000..baaeb61ad69 --- /dev/null +++ b/app/components/UI/Trending/hooks/useRwaTokens/useRwaTokens.ts @@ -0,0 +1,147 @@ +import { useMemo, useState, useEffect } from 'react'; +import Fuse, { type FuseOptions } from 'fuse.js'; +import type { CaipChainId } from '@metamask/utils'; +import { SortTrendingBy, TrendingAsset } from '@metamask/assets-controllers'; +import { useSelector } from 'react-redux'; +import { useSearchRequest } from '../useSearchRequest/useSearchRequest'; +import { sortTrendingTokens } from '../../utils/sortTrendingTokens'; +import { + PriceChangeOption, + SortDirection, +} from '../../components/TrendingTokensBottomSheet'; +import { RWA_CHAIN_IDS } from '../../utils/trendingNetworksList'; +import { isEqual } from 'lodash'; +import { getDetectedGeolocation } from '../../../../../reducers/fiatOrders'; + +// prettier-ignore +const ONDO_RESTRICTED_COUNTRIES = new Set([ + 'AF', 'DZ', 'BY', 'CA', 'CN', 'CU', 'KP', + 'ER', 'IR', 'LY', 'MM', 'MA', 'NP', 'RU', + 'SO', 'SS', 'SD', 'SY', 'US', 'VE', + 'BR', 'HK', 'MY', 'SG', 'CH', 'GB', + 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', + 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', + 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', + 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'IS', + 'LI', 'NO', 'UA', +]); + +const useStableReference = (value: T) => { + const [stableValue, setStableValue] = useState(value); + + useEffect(() => { + if (!isEqual(stableValue, value)) { + setStableValue(value); + } + }, [value, stableValue]); + + return stableValue; +}; + +const TOKEN_FUSE_OPTIONS: FuseOptions = { + shouldSort: true, + threshold: 0.2, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: ['symbol', 'name', 'assetId'], +}; + +const fuseSearch = ( + data: TrendingAsset[], + searchQuery: string | undefined, +): TrendingAsset[] => { + const trimmed = searchQuery?.trim(); + if (!trimmed) { + return data; + } + const fuse = new Fuse(data, TOKEN_FUSE_OPTIONS); + const results = fuse.search(trimmed); + // Penalize zero-marketCap tokens + return results.sort((a, b) => (b.marketCap ?? 0) - (a.marketCap ?? 0)); +}; + +/** + * Hook for RWA tokens search. + * Defaults to Ethereum + BNB when no chainIds are provided. + * + * @param opts.searchQuery - Client-side fuse.js query to filter results + * @param opts.chainIds - Chain IDs to filter by (defaults to RWA_CHAIN_IDS) + * @param opts.sortTrendingTokensOptions - Sorting options for price change / volume / market cap + * @returns Search results, loading state, and refetch function for rwa tokens + */ +export const useRwaTokens = (opts?: { + searchQuery?: string; + sortBy?: SortTrendingBy; + chainIds?: CaipChainId[] | null; + includeMarketData?: boolean; + sortTrendingTokensOptions?: { + option: PriceChangeOption; + direction: SortDirection; + }; +}) => { + const geolocation = useSelector(getDetectedGeolocation); + const isGeoRestricted = useMemo(() => { + if (__DEV__) return false; + const country = geolocation?.toUpperCase().split('-')[0]; + return !country || ONDO_RESTRICTED_COUNTRIES.has(country); + }, [geolocation]); + + const { + searchQuery, + chainIds, + includeMarketData = true, + sortTrendingTokensOptions = { + option: PriceChangeOption.PriceChange, + direction: SortDirection.Descending, + }, + } = useStableReference(opts ?? {}); + + const effectiveChainIds = chainIds ?? RWA_CHAIN_IDS; + + const { + results: searchResults, + isLoading: isSearchLoading, + search: refetch, + } = useSearchRequest({ + query: isGeoRestricted ? '' : '(Ondo Tokenized)', + limit: 500, + chainIds: effectiveChainIds, + includeMarketData, + }); + + const data = useMemo(() => { + if (isGeoRestricted) return []; + + const normalizedResults: TrendingAsset[] = searchResults + .filter((asset) => asset.rwaData) + .map((asset) => ({ + assetId: asset.assetId, + symbol: asset.symbol, + name: asset.name, + decimals: asset.decimals, + price: asset.price, + aggregatedUsdVolume: asset.aggregatedUsdVolume, + marketCap: asset.marketCap, + priceChangePct: { + h24: asset.pricePercentChange1d, + }, + rwaData: asset.rwaData as unknown as + | TrendingAsset['rwaData'] + | undefined, + })); + + if (searchQuery?.trim()) { + return fuseSearch(normalizedResults, searchQuery); + } + + return sortTrendingTokens( + normalizedResults, + sortTrendingTokensOptions.option, + sortTrendingTokensOptions.direction, + ); + }, [isGeoRestricted, searchResults, searchQuery, sortTrendingTokensOptions]); + + return { data, isLoading: isSearchLoading, refetch }; +}; diff --git a/app/components/UI/Trending/hooks/useTokenListFilters/useTokenListFilters.test.ts b/app/components/UI/Trending/hooks/useTokenListFilters/useTokenListFilters.test.ts new file mode 100644 index 00000000000..7b2294c5ca3 --- /dev/null +++ b/app/components/UI/Trending/hooks/useTokenListFilters/useTokenListFilters.test.ts @@ -0,0 +1,317 @@ +import { act } from '@testing-library/react-native'; +import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; +import { useTokenListFilters } from './useTokenListFilters'; +import { + PriceChangeOption, + SortDirection, + TimeOption, +} from '../../components/TrendingTokensBottomSheet'; +import type { CaipChainId } from '@metamask/utils'; + +const mockGoBack = jest.fn(); +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ goBack: mockGoBack }), +})); + +const mockTrackFilterChange = jest.fn(); +jest.mock('../../services/TrendingFeedSessionManager', () => ({ + __esModule: true, + default: { + getInstance: () => ({ + trackFilterChange: mockTrackFilterChange, + }), + }, +})); + +jest.mock('../useNetworkName/useNetworkName', () => ({ + useNetworkName: (network: CaipChainId[] | null) => + network && network.length > 0 ? 'Mock Network' : 'All networks', +})); + +const renderFilters = (options = {}) => + renderHookWithProvider(() => useTokenListFilters(options)); + +describe('useTokenListFilters', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('initial state', () => { + it('returns correct defaults', () => { + const { result } = renderFilters(); + + expect(result.current.selectedNetwork).toBeNull(); + expect(result.current.selectedPriceChangeOption).toBe( + PriceChangeOption.PriceChange, + ); + expect(result.current.priceChangeSortDirection).toBe( + SortDirection.Descending, + ); + expect(result.current.selectedTimeOption).toBe( + TimeOption.TwentyFourHours, + ); + expect(result.current.isSearchVisible).toBe(false); + expect(result.current.searchQuery).toBe(''); + expect(result.current.showNetworkBottomSheet).toBe(false); + expect(result.current.showPriceChangeBottomSheet).toBe(false); + expect(result.current.refreshing).toBe(false); + expect(result.current.selectedNetworkName).toBe('All networks'); + }); + + it('uses provided timeOption instead of default', () => { + const { result } = renderFilters({ timeOption: TimeOption.OneHour }); + + expect(result.current.selectedTimeOption).toBe(TimeOption.OneHour); + }); + }); + + describe('handleBackPress', () => { + it('calls navigation.goBack', () => { + const { result } = renderFilters(); + + act(() => result.current.handleBackPress()); + + expect(mockGoBack).toHaveBeenCalledTimes(1); + }); + }); + + describe('handleSearchToggle', () => { + it('opens search on first toggle', () => { + const { result } = renderFilters(); + + act(() => result.current.handleSearchToggle()); + + expect(result.current.isSearchVisible).toBe(true); + }); + + it('closes search and clears query on second toggle', () => { + const { result } = renderFilters(); + + act(() => result.current.handleSearchToggle()); + act(() => result.current.handleSearchQueryChange('test')); + act(() => result.current.handleSearchToggle()); + + expect(result.current.isSearchVisible).toBe(false); + expect(result.current.searchQuery).toBe(''); + }); + }); + + describe('handlePriceChangeSelect', () => { + it('updates price change option and sort direction', () => { + const { result } = renderFilters(); + + act(() => + result.current.handlePriceChangeSelect( + PriceChangeOption.Volume, + SortDirection.Ascending, + ), + ); + + expect(result.current.selectedPriceChangeOption).toBe( + PriceChangeOption.Volume, + ); + expect(result.current.priceChangeSortDirection).toBe( + SortDirection.Ascending, + ); + }); + + it('tracks analytics when option changes', () => { + const { result } = renderFilters(); + + act(() => + result.current.handlePriceChangeSelect( + PriceChangeOption.MarketCap, + SortDirection.Descending, + ), + ); + + expect(mockTrackFilterChange).toHaveBeenCalledWith( + expect.objectContaining({ + filter_type: 'sort', + previous_value: PriceChangeOption.PriceChange, + new_value: PriceChangeOption.MarketCap, + sort_option: PriceChangeOption.MarketCap, + network_filter: 'all', + }), + ); + }); + + it('includes selected network in analytics when a network is active', () => { + const { result } = renderFilters(); + + act(() => + result.current.handleNetworkSelect(['eip155:137' as CaipChainId]), + ); + mockTrackFilterChange.mockClear(); + + act(() => + result.current.handlePriceChangeSelect( + PriceChangeOption.Volume, + SortDirection.Ascending, + ), + ); + + expect(mockTrackFilterChange).toHaveBeenCalledWith( + expect.objectContaining({ + network_filter: 'eip155:137', + }), + ); + }); + + it('does not track analytics when re-selecting same option', () => { + const { result } = renderFilters(); + + act(() => + result.current.handlePriceChangeSelect( + PriceChangeOption.PriceChange, + SortDirection.Ascending, + ), + ); + + expect(mockTrackFilterChange).not.toHaveBeenCalled(); + }); + }); + + describe('handlePriceChangePress', () => { + it('opens price change bottom sheet', () => { + const { result } = renderFilters(); + + act(() => result.current.handlePriceChangePress()); + + expect(result.current.showPriceChangeBottomSheet).toBe(true); + }); + }); + + describe('handleNetworkSelect', () => { + it('updates selected network', () => { + const { result } = renderFilters(); + const chainIds = ['eip155:1' as CaipChainId]; + + act(() => result.current.handleNetworkSelect(chainIds)); + + expect(result.current.selectedNetwork).toEqual(chainIds); + }); + + it('tracks analytics when network changes', () => { + const { result } = renderFilters(); + const chainIds = ['eip155:137' as CaipChainId]; + + act(() => result.current.handleNetworkSelect(chainIds)); + + expect(mockTrackFilterChange).toHaveBeenCalledWith( + expect.objectContaining({ + filter_type: 'network', + previous_value: 'all', + new_value: 'eip155:137', + network_filter: 'eip155:137', + }), + ); + }); + + it('does not track analytics when selecting same network', () => { + const { result } = renderFilters(); + const chainIds = ['eip155:1' as CaipChainId]; + + act(() => result.current.handleNetworkSelect(chainIds)); + mockTrackFilterChange.mockClear(); + + act(() => result.current.handleNetworkSelect(chainIds)); + + expect(mockTrackFilterChange).not.toHaveBeenCalled(); + }); + + it('tracks analytics when clearing network back to all', () => { + const { result } = renderFilters(); + + act(() => + result.current.handleNetworkSelect(['eip155:1' as CaipChainId]), + ); + mockTrackFilterChange.mockClear(); + + act(() => result.current.handleNetworkSelect(null)); + + expect(mockTrackFilterChange).toHaveBeenCalledWith( + expect.objectContaining({ + previous_value: 'eip155:1', + new_value: 'all', + }), + ); + }); + }); + + describe('handleAllNetworksPress', () => { + it('opens network bottom sheet', () => { + const { result } = renderFilters(); + + act(() => result.current.handleAllNetworksPress()); + + expect(result.current.showNetworkBottomSheet).toBe(true); + }); + }); + + describe('priceChangeButtonText', () => { + it('returns "Price change" for PriceChange option', () => { + const { result } = renderFilters(); + + expect(result.current.priceChangeButtonText).toBe('Price change'); + }); + + it('returns "Volume" for Volume option', () => { + const { result } = renderFilters(); + + act(() => + result.current.handlePriceChangeSelect( + PriceChangeOption.Volume, + SortDirection.Descending, + ), + ); + + expect(result.current.priceChangeButtonText).toBe('Volume'); + }); + + it('returns "Market cap" for MarketCap option', () => { + const { result } = renderFilters(); + + act(() => + result.current.handlePriceChangeSelect( + PriceChangeOption.MarketCap, + SortDirection.Descending, + ), + ); + + expect(result.current.priceChangeButtonText).toBe('Market cap'); + }); + }); + + describe('filterContext', () => { + it('reflects current filter state', () => { + const { result } = renderFilters(); + + expect(result.current.filterContext).toEqual({ + timeFilter: TimeOption.TwentyFourHours, + sortOption: PriceChangeOption.PriceChange, + networkFilter: 'all', + isSearchResult: false, + }); + }); + + it('updates isSearchResult when search query has content', () => { + const { result } = renderFilters(); + + act(() => result.current.handleSearchQueryChange('eth')); + + expect(result.current.filterContext.isSearchResult).toBe(true); + }); + + it('updates networkFilter when a network is selected', () => { + const { result } = renderFilters(); + + act(() => + result.current.handleNetworkSelect(['eip155:42161' as CaipChainId]), + ); + + expect(result.current.filterContext.networkFilter).toBe('eip155:42161'); + }); + }); +}); diff --git a/app/components/UI/Trending/hooks/useTokenListFilters/useTokenListFilters.ts b/app/components/UI/Trending/hooks/useTokenListFilters/useTokenListFilters.ts new file mode 100644 index 00000000000..8dce1bcf7f9 --- /dev/null +++ b/app/components/UI/Trending/hooks/useTokenListFilters/useTokenListFilters.ts @@ -0,0 +1,235 @@ +import { useCallback, useMemo, useState } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { CaipChainId } from '@metamask/utils'; +import { strings } from '../../../../../../locales/i18n'; +import { + PriceChangeOption, + SortDirection, + TimeOption, +} from '../../components/TrendingTokensBottomSheet'; +import type { TrendingFilterContext } from '../../components/TrendingTokensList/TrendingTokensList'; +import TrendingFeedSessionManager from '../../services/TrendingFeedSessionManager'; +import { useNetworkName } from '../useNetworkName/useNetworkName'; + +interface UseTokenListFiltersOptions { + /** + * Fixed time option when the view doesn't support time selection. + * When provided, the time filter is static and no time bottom sheet is shown. + */ + timeOption?: TimeOption; +} + +export interface TokenListFilters { + // Navigation + handleBackPress: () => void; + + // Search + isSearchVisible: boolean; + searchQuery: string; + handleSearchToggle: () => void; + handleSearchQueryChange: (query: string) => void; + + // Network + selectedNetwork: CaipChainId[] | null; + selectedNetworkName: string; + showNetworkBottomSheet: boolean; + setShowNetworkBottomSheet: (visible: boolean) => void; + handleNetworkSelect: (chainIds: CaipChainId[] | null) => void; + handleAllNetworksPress: () => void; + + // Price change / sort + selectedPriceChangeOption: PriceChangeOption | undefined; + priceChangeSortDirection: SortDirection; + showPriceChangeBottomSheet: boolean; + setShowPriceChangeBottomSheet: (visible: boolean) => void; + handlePriceChangeSelect: ( + option: PriceChangeOption, + sortDirection: SortDirection, + ) => void; + handlePriceChangePress: () => void; + priceChangeButtonText: string; + + // Time + selectedTimeOption: TimeOption; + setSelectedTimeOption: (timeOption: TimeOption) => void; + + // Refresh + refreshing: boolean; + setRefreshing: (refreshing: boolean) => void; + + // Analytics filter context + filterContext: TrendingFilterContext; +} + +/** + * Manages all filter-related state and handlers shared across token list views + * (TrendingTokensFullView, RWATokensFullView). + */ +export const useTokenListFilters = ( + options: UseTokenListFiltersOptions = {}, +): TokenListFilters => { + const { timeOption } = options; + + const navigation = + useNavigation>>(); + const sessionManager = TrendingFeedSessionManager.getInstance(); + + const [selectedNetwork, setSelectedNetwork] = useState( + null, + ); + const [selectedPriceChangeOption, setSelectedPriceChangeOption] = useState< + PriceChangeOption | undefined + >(PriceChangeOption.PriceChange); + const [priceChangeSortDirection, setPriceChangeSortDirection] = + useState(SortDirection.Descending); + const [showNetworkBottomSheet, setShowNetworkBottomSheet] = useState(false); + const [showPriceChangeBottomSheet, setShowPriceChangeBottomSheet] = + useState(false); + const [isSearchVisible, setIsSearchVisible] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [refreshing, setRefreshing] = useState(false); + const [selectedTimeOption, setSelectedTimeOption] = useState( + timeOption ?? TimeOption.TwentyFourHours, + ); + + const selectedNetworkName = useNetworkName(selectedNetwork); + + const handleBackPress = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + const handleSearchToggle = useCallback(() => { + setIsSearchVisible((prev) => !prev); + if (isSearchVisible) { + setSearchQuery(''); + } + }, [isSearchVisible]); + + const handleSearchQueryChange = useCallback((query: string) => { + setSearchQuery(query); + }, []); + + const handlePriceChangeSelect = useCallback( + (option: PriceChangeOption, sortDirection: SortDirection) => { + const previousValue = + selectedPriceChangeOption || PriceChangeOption.PriceChange; + setSelectedPriceChangeOption(option); + setPriceChangeSortDirection(sortDirection); + + if (option !== previousValue) { + sessionManager.trackFilterChange({ + filter_type: 'sort', + previous_value: previousValue, + new_value: option, + time_filter: selectedTimeOption, + sort_option: option, + network_filter: + selectedNetwork && selectedNetwork.length > 0 + ? selectedNetwork[0] + : 'all', + }); + } + }, + [ + selectedPriceChangeOption, + selectedTimeOption, + selectedNetwork, + sessionManager, + ], + ); + + const handlePriceChangePress = useCallback(() => { + setShowPriceChangeBottomSheet(true); + }, []); + + const handleNetworkSelect = useCallback( + (chainIds: CaipChainId[] | null) => { + const previousValue = + selectedNetwork && selectedNetwork.length > 0 + ? selectedNetwork[0] + : 'all'; + const newValue = chainIds && chainIds.length > 0 ? chainIds[0] : 'all'; + + setSelectedNetwork(chainIds); + + if (newValue !== previousValue) { + sessionManager.trackFilterChange({ + filter_type: 'network', + previous_value: previousValue, + new_value: newValue, + time_filter: selectedTimeOption, + sort_option: + selectedPriceChangeOption || PriceChangeOption.PriceChange, + network_filter: newValue, + }); + } + }, + [ + selectedNetwork, + selectedTimeOption, + selectedPriceChangeOption, + sessionManager, + ], + ); + + const handleAllNetworksPress = useCallback(() => { + setShowNetworkBottomSheet(true); + }, []); + + const priceChangeButtonText = useMemo(() => { + switch (selectedPriceChangeOption) { + case PriceChangeOption.Volume: + return strings('trending.volume'); + case PriceChangeOption.MarketCap: + return strings('trending.market_cap'); + case PriceChangeOption.PriceChange: + default: + return strings('trending.price_change'); + } + }, [selectedPriceChangeOption]); + + const filterContext: TrendingFilterContext = useMemo( + () => ({ + timeFilter: selectedTimeOption, + sortOption: selectedPriceChangeOption, + networkFilter: + selectedNetwork && selectedNetwork.length > 0 + ? selectedNetwork[0] + : 'all', + isSearchResult: Boolean(searchQuery?.trim()), + }), + [ + selectedTimeOption, + selectedPriceChangeOption, + selectedNetwork, + searchQuery, + ], + ); + + return { + handleBackPress, + isSearchVisible, + searchQuery, + handleSearchToggle, + handleSearchQueryChange, + selectedNetwork, + selectedNetworkName, + showNetworkBottomSheet, + setShowNetworkBottomSheet, + handleNetworkSelect, + handleAllNetworksPress, + selectedPriceChangeOption, + priceChangeSortDirection, + showPriceChangeBottomSheet, + setShowPriceChangeBottomSheet, + handlePriceChangeSelect, + handlePriceChangePress, + priceChangeButtonText, + selectedTimeOption, + setSelectedTimeOption, + refreshing, + setRefreshing, + filterContext, + }; +}; diff --git a/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts b/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts index 74cb665ee54..b8baa6eb536 100644 --- a/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts +++ b/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts @@ -104,25 +104,27 @@ export const useTrendingSearch = (opts?: { filteredTrendingResults.map((result) => [result.assetId, result]), ); - searchResults.forEach((asset) => { - if (!resultMap.has(asset.assetId)) { - resultMap.set(asset.assetId, { - assetId: asset.assetId, - symbol: asset.symbol, - name: asset.name, - decimals: asset.decimals, - price: asset.price, - aggregatedUsdVolume: asset.aggregatedUsdVolume, - marketCap: asset.marketCap, - priceChangePct: { - h24: asset.pricePercentChange1d, - }, - rwaData: asset.rwaData as unknown as - | TrendingAsset['rwaData'] - | undefined, - }); - } - }); + searchResults + .filter((item) => !item.rwaData) + .forEach((asset) => { + if (!resultMap.has(asset.assetId)) { + resultMap.set(asset.assetId, { + assetId: asset.assetId, + symbol: asset.symbol, + name: asset.name, + decimals: asset.decimals, + price: asset.price, + aggregatedUsdVolume: asset.aggregatedUsdVolume, + marketCap: asset.marketCap, + priceChangePct: { + h24: asset.pricePercentChange1d, + }, + rwaData: asset.rwaData as unknown as + | TrendingAsset['rwaData'] + | undefined, + }); + } + }); return Array.from(resultMap.values()); }, [ diff --git a/app/components/UI/Trending/utils/sortTrendingTokens.test.ts b/app/components/UI/Trending/utils/sortTrendingTokens.test.ts index 7ccb39751be..296d00afa73 100644 --- a/app/components/UI/Trending/utils/sortTrendingTokens.test.ts +++ b/app/components/UI/Trending/utils/sortTrendingTokens.test.ts @@ -20,48 +20,50 @@ const createMockToken = ( }); describe('sortTrendingTokens', () => { + it('returns empty array for empty input', () => { + expect(sortTrendingTokens([])).toEqual([]); + }); + describe('PriceChange sorting', () => { it('sorts by price change descending (default)', () => { - const tokens: TrendingAsset[] = [ + const tokens = [ createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', + assetId: 'a', + symbol: 'A', priceChangePct: { h24: '5.0' }, }), createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', + assetId: 'b', + symbol: 'B', priceChangePct: { h24: '10.0' }, }), createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', + assetId: 'c', + symbol: 'C', priceChangePct: { h24: '2.0' }, }), ]; const result = sortTrendingTokens(tokens); - expect(result[0].symbol).toBe('TOKEN2'); // Highest price change - expect(result[1].symbol).toBe('TOKEN1'); - expect(result[2].symbol).toBe('TOKEN3'); // Lowest price change + expect(result.map((t) => t.symbol)).toEqual(['B', 'A', 'C']); }); it('sorts by price change ascending', () => { - const tokens: TrendingAsset[] = [ + const tokens = [ createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', + assetId: 'a', + symbol: 'A', priceChangePct: { h24: '5.0' }, }), createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', + assetId: 'b', + symbol: 'B', priceChangePct: { h24: '10.0' }, }), createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', + assetId: 'c', + symbol: 'C', priceChangePct: { h24: '2.0' }, }), ]; @@ -72,142 +74,112 @@ describe('sortTrendingTokens', () => { SortDirection.Ascending, ); - expect(result[0].symbol).toBe('TOKEN3'); // Lowest price change - expect(result[1].symbol).toBe('TOKEN1'); - expect(result[2].symbol).toBe('TOKEN2'); // Highest price change + expect(result.map((t) => t.symbol)).toEqual(['C', 'A', 'B']); }); - it('handles missing priceChangePct.h24 by treating as 0', () => { - const tokens: TrendingAsset[] = [ + it('handles missing priceChangePct by treating value as 0', () => { + const tokens = [ createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', + assetId: 'a', + symbol: 'A', priceChangePct: { h24: '5.0' }, }), createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', + assetId: 'b', + symbol: 'B', priceChangePct: undefined, }), createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', + assetId: 'c', + symbol: 'C', priceChangePct: { h24: '10.0' }, }), ]; - const result = sortTrendingTokens( - tokens, - PriceChangeOption.PriceChange, - SortDirection.Descending, - ); + const result = sortTrendingTokens(tokens); - expect(result[0].symbol).toBe('TOKEN3'); // Highest - expect(result[1].symbol).toBe('TOKEN1'); - expect(result[2].symbol).toBe('TOKEN2'); // Missing value treated as 0 + expect(result.map((t) => t.symbol)).toEqual(['C', 'A', 'B']); }); - it('handles invalid priceChangePct.h24 string by treating as 0', () => { - const tokens: TrendingAsset[] = [ + it('handles invalid priceChangePct string by treating value as 0', () => { + const tokens = [ createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', + assetId: 'a', + symbol: 'A', priceChangePct: { h24: 'invalid' }, }), createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', + assetId: 'b', + symbol: 'B', priceChangePct: { h24: '5.0' }, }), ]; - const result = sortTrendingTokens( - tokens, - PriceChangeOption.PriceChange, - SortDirection.Descending, - ); + const result = sortTrendingTokens(tokens); - expect(result[0].symbol).toBe('TOKEN2'); - expect(result[1].symbol).toBe('TOKEN1'); // Invalid value treated as 0 + expect(result.map((t) => t.symbol)).toEqual(['B', 'A']); }); - it('handles negative price changes correctly', () => { - const tokens: TrendingAsset[] = [ + it('sorts negative price changes correctly', () => { + const tokens = [ createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', + assetId: 'a', + symbol: 'A', priceChangePct: { h24: '-5.0' }, }), createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', + assetId: 'b', + symbol: 'B', priceChangePct: { h24: '10.0' }, }), createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', + assetId: 'c', + symbol: 'C', priceChangePct: { h24: '-2.0' }, }), ]; - const result = sortTrendingTokens( - tokens, - PriceChangeOption.PriceChange, - SortDirection.Descending, - ); + const result = sortTrendingTokens(tokens); - expect(result[0].symbol).toBe('TOKEN2'); // Highest (positive) - expect(result[1].symbol).toBe('TOKEN3'); // Less negative - expect(result[2].symbol).toBe('TOKEN1'); // Most negative + expect(result.map((t) => t.symbol)).toEqual(['B', 'C', 'A']); }); - }); - describe('Volume sorting', () => { - it('sorts by volume descending', () => { - const tokens: TrendingAsset[] = [ - createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', - aggregatedUsdVolume: 1000000, - }), + it('pushes tokens with no price data to end', () => { + const tokens = [ + createMockToken({ assetId: 'a', symbol: 'A', price: undefined }), createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', - aggregatedUsdVolume: 5000000, - }), - createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', - aggregatedUsdVolume: 2000000, + assetId: 'b', + symbol: 'B', + priceChangePct: { h24: '3.0' }, }), + createMockToken({ assetId: 'c', symbol: 'C', price: '0' }), ]; - const result = sortTrendingTokens( - tokens, - PriceChangeOption.Volume, - SortDirection.Descending, - ); + const result = sortTrendingTokens(tokens); - expect(result[0].symbol).toBe('TOKEN2'); // Highest volume - expect(result[1].symbol).toBe('TOKEN3'); - expect(result[2].symbol).toBe('TOKEN1'); // Lowest volume + expect(result[0].symbol).toBe('B'); + expect(result.slice(1).map((t) => t.symbol)).toEqual( + expect.arrayContaining(['A', 'C']), + ); }); + }); - it('sorts by volume ascending', () => { - const tokens: TrendingAsset[] = [ + describe('Volume sorting', () => { + it('sorts by volume descending', () => { + const tokens = [ createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', + assetId: 'a', + symbol: 'A', aggregatedUsdVolume: 1000000, }), createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', + assetId: 'b', + symbol: 'B', aggregatedUsdVolume: 5000000, }), createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', + assetId: 'c', + symbol: 'C', aggregatedUsdVolume: 2000000, }), ]; @@ -215,29 +187,27 @@ describe('sortTrendingTokens', () => { const result = sortTrendingTokens( tokens, PriceChangeOption.Volume, - SortDirection.Ascending, + SortDirection.Descending, ); - expect(result[0].symbol).toBe('TOKEN1'); // Lowest volume - expect(result[1].symbol).toBe('TOKEN3'); - expect(result[2].symbol).toBe('TOKEN2'); // Highest volume + expect(result.map((t) => t.symbol)).toEqual(['B', 'C', 'A']); }); - it('handles missing aggregatedUsdVolume by treating as 0', () => { - const tokens: TrendingAsset[] = [ + it('pushes tokens with no volume to end', () => { + const tokens = [ createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', + assetId: 'a', + symbol: 'A', aggregatedUsdVolume: 1000000, }), createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', + assetId: 'b', + symbol: 'B', aggregatedUsdVolume: undefined, }), createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', + assetId: 'c', + symbol: 'C', aggregatedUsdVolume: 5000000, }), ]; @@ -248,30 +218,16 @@ describe('sortTrendingTokens', () => { SortDirection.Descending, ); - expect(result[0].symbol).toBe('TOKEN3'); // Highest - expect(result[1].symbol).toBe('TOKEN1'); - expect(result[2].symbol).toBe('TOKEN2'); // Missing value treated as 0 + expect(result.map((t) => t.symbol)).toEqual(['C', 'A', 'B']); }); }); describe('MarketCap sorting', () => { it('sorts by market cap descending', () => { - const tokens: TrendingAsset[] = [ - createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', - marketCap: 10000000, - }), - createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', - marketCap: 50000000, - }), - createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', - marketCap: 20000000, - }), + const tokens = [ + createMockToken({ assetId: 'a', symbol: 'A', marketCap: 10000000 }), + createMockToken({ assetId: 'b', symbol: 'B', marketCap: 50000000 }), + createMockToken({ assetId: 'c', symbol: 'C', marketCap: 20000000 }), ]; const result = sortTrendingTokens( @@ -280,58 +236,14 @@ describe('sortTrendingTokens', () => { SortDirection.Descending, ); - expect(result[0].symbol).toBe('TOKEN2'); // Highest market cap - expect(result[1].symbol).toBe('TOKEN3'); - expect(result[2].symbol).toBe('TOKEN1'); // Lowest market cap + expect(result.map((t) => t.symbol)).toEqual(['B', 'C', 'A']); }); - it('sorts by market cap ascending', () => { - const tokens: TrendingAsset[] = [ - createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', - marketCap: 10000000, - }), - createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', - marketCap: 50000000, - }), - createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', - marketCap: 20000000, - }), - ]; - - const result = sortTrendingTokens( - tokens, - PriceChangeOption.MarketCap, - SortDirection.Ascending, - ); - - expect(result[0].symbol).toBe('TOKEN1'); // Lowest market cap - expect(result[1].symbol).toBe('TOKEN3'); - expect(result[2].symbol).toBe('TOKEN2'); // Highest market cap - }); - - it('handles missing marketCap by treating as 0', () => { - const tokens: TrendingAsset[] = [ - createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', - marketCap: 10000000, - }), - createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', - marketCap: undefined, - }), - createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', - marketCap: 50000000, - }), + it('pushes tokens with no market cap to end', () => { + const tokens = [ + createMockToken({ assetId: 'a', symbol: 'A', marketCap: 10000000 }), + createMockToken({ assetId: 'b', symbol: 'B', marketCap: undefined }), + createMockToken({ assetId: 'c', symbol: 'C', marketCap: 50000000 }), ]; const result = sortTrendingTokens( @@ -340,37 +252,7 @@ describe('sortTrendingTokens', () => { SortDirection.Descending, ); - expect(result[0].symbol).toBe('TOKEN3'); // Highest - expect(result[1].symbol).toBe('TOKEN1'); - expect(result[2].symbol).toBe('TOKEN2'); // Missing value treated as 0 - }); - }); - - describe('Default parameters', () => { - it('uses PriceChange and Descending as defaults', () => { - const tokens: TrendingAsset[] = [ - createMockToken({ - assetId: 'token1', - symbol: 'TOKEN1', - priceChangePct: { h24: '2.0' }, - }), - createMockToken({ - assetId: 'token2', - symbol: 'TOKEN2', - priceChangePct: { h24: '10.0' }, - }), - createMockToken({ - assetId: 'token3', - symbol: 'TOKEN3', - priceChangePct: { h24: '5.0' }, - }), - ]; - - const result = sortTrendingTokens(tokens); - - expect(result[0].symbol).toBe('TOKEN2'); // Highest price change - expect(result[1].symbol).toBe('TOKEN3'); - expect(result[2].symbol).toBe('TOKEN1'); // Lowest price change + expect(result.map((t) => t.symbol)).toEqual(['C', 'A', 'B']); }); }); }); diff --git a/app/components/UI/Trending/utils/sortTrendingTokens.ts b/app/components/UI/Trending/utils/sortTrendingTokens.ts index 6d975a3867b..dcb09e21788 100644 --- a/app/components/UI/Trending/utils/sortTrendingTokens.ts +++ b/app/components/UI/Trending/utils/sortTrendingTokens.ts @@ -6,6 +6,39 @@ import { } from '../components/TrendingTokensBottomSheet'; import { getPriceChangeFieldKey } from '../components/TrendingTokenRowItem/utils'; +const getDataPredicate = ( + option: PriceChangeOption, +): ((t: TrendingAsset) => boolean) => { + switch (option) { + case PriceChangeOption.PriceChange: + // Not using price change on purpose since price change can be 0% + return (t) => Boolean(t.price && t.price !== '0'); + case PriceChangeOption.Volume: + return (t) => Boolean(t.aggregatedUsdVolume); + case PriceChangeOption.MarketCap: + return (t) => Boolean(t.marketCap); + } +}; + +const getValueExtractor = ( + option: PriceChangeOption, + timeOption: TimeOption, +): ((t: TrendingAsset) => number) => { + switch (option) { + case PriceChangeOption.PriceChange: { + const key = getPriceChangeFieldKey(timeOption); + return (t) => { + const v = t.priceChangePct?.[key]; + return v ? parseFloat(v) || 0 : 0; + }; + } + case PriceChangeOption.Volume: + return (t) => t.aggregatedUsdVolume ?? 0; + case PriceChangeOption.MarketCap: + return (t) => t.marketCap ?? 0; + } +}; + /** * Sorts trending tokens based on the selected option and direction * @param tokens - Array of trending tokens to sort @@ -24,37 +57,17 @@ export const sortTrendingTokens = ( return []; } - // Create a new array and sort in-place for better performance - const sorted = [...tokens]; - sorted.sort((a, b) => { - let aValue: number; - let bValue: number; + const hasData = getDataPredicate(option); + const getValue = getValueExtractor(option, timeOption); + const dirMultiplier = direction === SortDirection.Ascending ? 1 : -1; - switch (option) { - case PriceChangeOption.PriceChange: { - // For price change, use the priceChangePct field corresponding to the selected time option - const priceChangeFieldKey = getPriceChangeFieldKey(timeOption); - const aPriceChange = a.priceChangePct?.[priceChangeFieldKey]; - aValue = aPriceChange ? parseFloat(aPriceChange) || 0 : 0; - const bPriceChange = b.priceChangePct?.[priceChangeFieldKey]; - bValue = bPriceChange ? parseFloat(bPriceChange) || 0 : 0; - break; - } - case PriceChangeOption.Volume: - aValue = a.aggregatedUsdVolume ?? 0; - bValue = b.aggregatedUsdVolume ?? 0; - break; - case PriceChangeOption.MarketCap: - aValue = a.marketCap ?? 0; - bValue = b.marketCap ?? 0; - break; - default: - return 0; - } + const sortable: TrendingAsset[] = []; + const nulled: TrendingAsset[] = []; + for (const token of tokens) { + (hasData(token) ? sortable : nulled).push(token); + } - const comparison = aValue - bValue; - return direction === SortDirection.Ascending ? comparison : -comparison; - }); + sortable.sort((a, b) => (getValue(a) - getValue(b)) * dirMultiplier); - return sorted; + return [...sortable, ...nulled]; }; diff --git a/app/components/UI/Trending/utils/trendingNetworksList.ts b/app/components/UI/Trending/utils/trendingNetworksList.ts index 50207e763ae..8bd1a98ba19 100644 --- a/app/components/UI/Trending/utils/trendingNetworksList.ts +++ b/app/components/UI/Trending/utils/trendingNetworksList.ts @@ -3,6 +3,7 @@ import { TrxScope, ///: END:ONLY_INCLUDE_IF } from '@metamask/keyring-api'; +import type { CaipChainId } from '@metamask/utils'; import { ProcessedNetwork } from '../../../hooks/useNetworksByNamespace/useNetworksByNamespace'; import { getNetworkImageSource } from '../../../../util/networks'; import { NetworkToCaipChainId } from '../../NetworkMultiSelector/NetworkMultiSelector.constants'; @@ -123,3 +124,17 @@ export const TRENDING_NETWORKS_LIST: ProcessedNetwork[] = [ }), }, ]; + +/** + * Networks supported for RWA (Real World Asset) tokens. + */ +export const RWA_NETWORKS_LIST: ProcessedNetwork[] = + TRENDING_NETWORKS_LIST.filter((n) => + [NetworkToCaipChainId.ETHEREUM, NetworkToCaipChainId.BNB].includes( + n.caipChainId as NetworkToCaipChainId, + ), + ); + +export const RWA_CHAIN_IDS: CaipChainId[] = RWA_NETWORKS_LIST.map( + (n) => n.caipChainId, +); diff --git a/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx b/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx deleted file mode 100644 index 443bf3c1078..00000000000 --- a/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx +++ /dev/null @@ -1,617 +0,0 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { useNavigation } from '@react-navigation/native'; -import { - SafeAreaView, - useSafeAreaInsets, -} from 'react-native-safe-area-context'; -import { - Platform, - StyleSheet, - View, - TouchableOpacity, - RefreshControl, -} from 'react-native'; -import { useSelector } from 'react-redux'; -import { useAppThemeFromContext } from '../../../../util/theme'; -import { Theme } from '../../../../util/theme/models'; -import { selectNetworkConfigurationsByCaipChainId } from '../../../../selectors/networkController'; -import Icon, { - IconName, - IconColor, - IconSize, -} from '../../../../component-library/components/Icons/Icon'; -import { strings } from '../../../../../locales/i18n'; -import { TrendingListHeader } from '../../../UI/Trending/components/TrendingListHeader'; -import TrendingTokensList, { - TrendingFilterContext, -} from '../../../UI/Trending/components/TrendingTokensList/TrendingTokensList'; -import TrendingTokensSkeleton from '../../../UI/Trending/components/TrendingTokenSkeleton/TrendingTokensSkeleton'; -import { - SortTrendingBy, - type TrendingAsset, -} from '@metamask/assets-controllers'; -import { CaipChainId, Hex, parseCaipChainId } from '@metamask/utils'; -import { PopularList } from '../../../../util/networks/customNetworks'; -import Text from '../../../../component-library/components/Texts/Text'; -import { - TrendingTokenTimeBottomSheet, - TrendingTokenNetworkBottomSheet, - TrendingTokenPriceChangeBottomSheet, - PriceChangeOption, - SortDirection, - TimeOption, -} from '../../../UI/Trending/components/TrendingTokensBottomSheet'; -import { sortTrendingTokens } from '../../../UI/Trending/utils/sortTrendingTokens'; -import { useTrendingSearch } from '../../../UI/Trending/hooks/useTrendingSearch/useTrendingSearch'; -import EmptyErrorTrendingState from '../../TrendingView/components/EmptyErrorState/EmptyErrorTrendingState'; -import EmptySearchResultState from '../../TrendingView/components/EmptyErrorState/EmptySearchResultState'; -import { useTailwind } from '@metamask/design-system-twrnc-preset'; -import TrendingFeedSessionManager from '../../../UI/Trending/services/TrendingFeedSessionManager'; -import { useSearchTracking } from '../../../UI/Trending/hooks/useSearchTracking/useSearchTracking'; - -const createStyles = (theme: Theme) => - StyleSheet.create({ - safeArea: { - flex: 1, - backgroundColor: theme.colors.background.default, - }, - headerContainer: { - backgroundColor: theme.colors.background.default, - }, - cardContainer: { - margin: 16, - borderRadius: 16, - backgroundColor: theme.colors.background.muted, - padding: 16, - }, - controlBarWrapper: { - paddingVertical: 16, - paddingHorizontal: 16, - flexGrow: 0, - }, - controlButtonOuterWrapper: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - controlButtonInnerWrapper: { - flexDirection: 'row', - gap: 8, - alignItems: 'center', - flexShrink: 1, - marginLeft: 8, - minWidth: 0, - }, - controlButton: { - paddingVertical: 8, - paddingHorizontal: 12, - alignItems: 'center', - borderRadius: 8, - backgroundColor: theme.colors.background.muted, - }, - controlButtonRight: { - padding: 8, - alignItems: 'center', - borderRadius: 8, - backgroundColor: theme.colors.background.muted, - flexShrink: 1, - minWidth: 0, - }, - controlButtonRightFixed: { - padding: 8, - alignItems: 'center', - borderRadius: 8, - backgroundColor: theme.colors.background.muted, - flexShrink: 0, - }, - controlButtonContent: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 4, - }, - controlButtonText: { - color: theme.colors.text.default, - fontSize: 14, - fontWeight: '600', - lineHeight: 19.6, // 140% of 14px - fontStyle: 'normal', - flexShrink: 1, - minWidth: 0, - }, - controlButtonDisabled: { - opacity: 0.5, - }, - }); - -export interface TrendingTokensDataProps { - isLoading: boolean; - refreshing: boolean; - trendingTokens: TrendingAsset[]; - handleRefresh: () => void; - selectedTimeOption: TimeOption; - filterContext: TrendingFilterContext; - theme: Theme; - - search: { - searchResults: TrendingAsset[]; - searchQuery: string; - }; -} - -export const TrendingTokensData = (props: TrendingTokensDataProps) => { - const { - isLoading, - refreshing, - trendingTokens, - search, - handleRefresh, - selectedTimeOption, - filterContext, - theme, - } = props; - - const tw = useTailwind(); - - const isSearching = search.searchQuery.trim().length > 0; - const hasSearchResults = search.searchResults.length > 0; - - // Loading - show skeleton - if (isLoading) { - return ( - - {Array.from({ length: 12 }).map((_, index) => ( - - ))} - - ); - } - - // Show empty trending search results - if (isSearching && !hasSearchResults) { - return ; - } - - // Show error if no results found - if (!isSearching && !hasSearchResults) { - return ; - } - - // Show trending tokens list - return ( - - - } - /> - - ); -}; - -const TrendingTokensFullView = () => { - const navigation = useNavigation(); - const theme = useAppThemeFromContext(); - const styles = useMemo(() => createStyles(theme), [theme]); - const insets = useSafeAreaInsets(); - const sessionManager = TrendingFeedSessionManager.getInstance(); - const [sortBy, setSortBy] = useState(undefined); - const [selectedTimeOption, setSelectedTimeOption] = useState( - TimeOption.TwentyFourHours, - ); - const [selectedNetwork, setSelectedNetwork] = useState( - null, - ); - const [selectedPriceChangeOption, setSelectedPriceChangeOption] = useState< - PriceChangeOption | undefined - >(PriceChangeOption.PriceChange); - const [priceChangeSortDirection, setPriceChangeSortDirection] = - useState(SortDirection.Descending); - const [showTimeBottomSheet, setShowTimeBottomSheet] = useState(false); - const [showNetworkBottomSheet, setShowNetworkBottomSheet] = useState(false); - const [showPriceChangeBottomSheet, setShowPriceChangeBottomSheet] = - useState(false); - const [isSearchVisible, setIsSearchVisible] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - const [refreshing, setRefreshing] = useState(false); - - const handleBackPress = useCallback(() => { - navigation.goBack(); - }, [navigation]); - - const handleSearchToggle = useCallback(() => { - setIsSearchVisible((prev) => !prev); - if (isSearchVisible) { - setSearchQuery(''); - } - }, [isSearchVisible]); - - const handleSearchQueryChange = useCallback((query: string) => { - setSearchQuery(query); - }, []); - - const networkConfigurations = useSelector( - selectNetworkConfigurationsByCaipChainId, - ); - - // Derive network name from selectedNetwork chain IDs - const selectedNetworkName = useMemo(() => { - if (!selectedNetwork || selectedNetwork.length === 0) { - return strings('trending.all_networks'); - } - const selectedNetworkChainId = selectedNetwork[0]; - - // First check if network is in user's configurations - const networkConfig = networkConfigurations[selectedNetworkChainId]; - if (networkConfig?.name) { - return networkConfig.name; - } - - // If not found, check PopularList - try { - const { namespace, reference } = parseCaipChainId(selectedNetworkChainId); - if (namespace === 'eip155') { - const hexChainId = `0x${Number(reference).toString(16)}` as Hex; - const popularNetwork = PopularList.find( - (network) => network.chainId === hexChainId, - ); - if (popularNetwork?.nickname) { - return popularNetwork.nickname; - } - } - } catch { - // If parsing fails, fall through to default - } - - return strings('trending.all_networks'); - }, [selectedNetwork, networkConfigurations]); - - // Use tokens section data as the single source of truth: - // - When no search query: returns trending results from useTrendingRequest - // - When search query exists: returns merged trending + search results - const { - data: searchResults, - isLoading, - refetch: refetchTokensSection, - } = useTrendingSearch({ - searchQuery: searchQuery || undefined, - sortBy, - chainIds: selectedNetwork, - }); - - // Sort and display tokens based on selected option and direction - const trendingTokens = useMemo(() => { - // Early return if no results - if (searchResults.length === 0) { - return []; - } - - // When searching, return results in relevance order (no sorting) - if (searchQuery?.trim()) { - return searchResults; - } - - // When browsing (no search), apply sorting if option is selected - if (!selectedPriceChangeOption) { - return searchResults; - } - - // Sort using the shared utility function - const sorted = sortTrendingTokens( - searchResults, - selectedPriceChangeOption, - priceChangeSortDirection, - selectedTimeOption, - ); - - return sorted; - }, [ - searchResults, - searchQuery, - selectedPriceChangeOption, - priceChangeSortDirection, - selectedTimeOption, - ]); - - // Compute filter context for analytics tracking - const filterContext: TrendingFilterContext = useMemo( - () => ({ - timeFilter: selectedTimeOption, - sortOption: selectedPriceChangeOption, - networkFilter: - selectedNetwork && selectedNetwork.length > 0 - ? selectedNetwork[0] - : 'all', - isSearchResult: Boolean(searchQuery?.trim()), - }), - [ - selectedTimeOption, - selectedPriceChangeOption, - selectedNetwork, - searchQuery, - ], - ); - - // Track search events with debounce - useSearchTracking({ - searchQuery, - resultsCount: trendingTokens.length, - isLoading, - timeFilter: selectedTimeOption, - sortOption: selectedPriceChangeOption || PriceChangeOption.PriceChange, - networkFilter: - selectedNetwork && selectedNetwork.length > 0 - ? selectedNetwork[0] - : 'all', - }); - - const handlePriceChangeSelect = useCallback( - (option: PriceChangeOption, sortDirection: SortDirection) => { - const previousValue = - selectedPriceChangeOption || PriceChangeOption.PriceChange; - setSelectedPriceChangeOption(option); - setPriceChangeSortDirection(sortDirection); - - // Track filter change if value actually changed - if (option !== previousValue) { - sessionManager.trackFilterChange({ - filter_type: 'sort', - previous_value: previousValue, - new_value: option, - time_filter: selectedTimeOption, - sort_option: option, - network_filter: - selectedNetwork && selectedNetwork.length > 0 - ? selectedNetwork[0] - : 'all', - }); - } - }, - [ - selectedPriceChangeOption, - selectedTimeOption, - selectedNetwork, - sessionManager, - ], - ); - - const handlePriceChangePress = useCallback(() => { - setShowPriceChangeBottomSheet(true); - }, []); - - const handleNetworkSelect = useCallback( - (chainIds: CaipChainId[] | null) => { - const previousValue = - selectedNetwork && selectedNetwork.length > 0 - ? selectedNetwork[0] - : 'all'; - const newValue = chainIds && chainIds.length > 0 ? chainIds[0] : 'all'; - - setSelectedNetwork(chainIds); - - // Track filter change if value actually changed - if (newValue !== previousValue) { - sessionManager.trackFilterChange({ - filter_type: 'network', - previous_value: previousValue, - new_value: newValue, - time_filter: selectedTimeOption, - sort_option: - selectedPriceChangeOption || PriceChangeOption.PriceChange, - network_filter: newValue, - }); - } - }, - [ - selectedNetwork, - selectedTimeOption, - selectedPriceChangeOption, - sessionManager, - ], - ); - - const handleAllNetworksPress = useCallback(() => { - setShowNetworkBottomSheet(true); - }, []); - - const handleTimeSelect = useCallback( - (selectedSortBy: SortTrendingBy, timeOption: TimeOption) => { - const previousValue = selectedTimeOption; - setSortBy(selectedSortBy); - setSelectedTimeOption(timeOption); - - // Track filter change if value actually changed - if (timeOption !== previousValue) { - sessionManager.trackFilterChange({ - filter_type: 'time', - previous_value: previousValue, - new_value: timeOption, - time_filter: timeOption, - sort_option: - selectedPriceChangeOption || PriceChangeOption.PriceChange, - network_filter: - selectedNetwork && selectedNetwork.length > 0 - ? selectedNetwork[0] - : 'all', - }); - } - }, - [ - selectedTimeOption, - selectedPriceChangeOption, - selectedNetwork, - sessionManager, - ], - ); - - const handle24hPress = useCallback(() => { - setShowTimeBottomSheet(true); - }, []); - - // Handle pull-to-refresh - const handleRefresh = useCallback(async () => { - setRefreshing(true); - try { - refetchTokensSection?.(); - } catch (error) { - console.warn('Failed to refresh trending tokens:', error); - } finally { - setRefreshing(false); - } - }, [refetchTokensSection]); - - // Get the button text based on selected price change option - const priceChangeButtonText = useMemo(() => { - switch (selectedPriceChangeOption) { - case PriceChangeOption.Volume: - return strings('trending.volume'); - case PriceChangeOption.MarketCap: - return strings('trending.market_cap'); - case PriceChangeOption.PriceChange: - default: - return strings('trending.price_change'); - } - }, [selectedPriceChangeOption]); - - return ( - - - - - {!isSearchVisible ? ( - - - - - - {priceChangeButtonText} - - - - - - - - - {selectedNetworkName} - - - - - - - - {selectedTimeOption} - - - - - - - - ) : null} - - - - setShowTimeBottomSheet(false)} - onTimeSelect={handleTimeSelect} - selectedTime={selectedTimeOption} - /> - setShowNetworkBottomSheet(false)} - onNetworkSelect={handleNetworkSelect} - selectedNetwork={selectedNetwork} - /> - setShowPriceChangeBottomSheet(false)} - onPriceChangeSelect={handlePriceChangeSelect} - selectedOption={selectedPriceChangeOption} - sortDirection={priceChangeSortDirection} - /> - - ); -}; - -TrendingTokensFullView.displayName = 'TrendingTokensFullView'; - -export default TrendingTokensFullView; diff --git a/app/components/Views/TrendingView/components/ExploreSearchResults/ExploreSearchResults.test.tsx b/app/components/Views/TrendingView/components/ExploreSearchResults/ExploreSearchResults.test.tsx index fc8ceafa8f8..7ccc8b84a2c 100644 --- a/app/components/Views/TrendingView/components/ExploreSearchResults/ExploreSearchResults.test.tsx +++ b/app/components/Views/TrendingView/components/ExploreSearchResults/ExploreSearchResults.test.tsx @@ -93,15 +93,17 @@ describe('ExploreSearchResults', () => { ], }, ], + stocks: [], sites: [], }, isLoading: { tokens: false, perps: false, predictions: false, + stocks: false, sites: false, }, - sectionsOrder: ['tokens', 'perps', 'predictions', 'sites'], + sectionsOrder: ['tokens', 'stocks', 'perps', 'predictions', 'sites'], }); const { getByText, getByTestId } = render( @@ -120,15 +122,17 @@ describe('ExploreSearchResults', () => { tokens: [{ assetId: '1', symbol: 'BTC', name: 'Bitcoin' }], perps: [], predictions: [], + stocks: [], sites: [], }, isLoading: { tokens: false, perps: false, predictions: false, + stocks: false, sites: false, }, - sectionsOrder: ['tokens', 'perps', 'predictions', 'sites'], + sectionsOrder: ['tokens', 'stocks', 'perps', 'predictions', 'sites'], }); const { getByText, queryByText } = render( @@ -146,15 +150,17 @@ describe('ExploreSearchResults', () => { tokens: [], perps: [], predictions: [], + stocks: [], sites: [], }, isLoading: { tokens: false, perps: false, predictions: false, + stocks: false, sites: false, }, - sectionsOrder: ['tokens', 'perps', 'predictions', 'sites'], + sectionsOrder: ['tokens', 'stocks', 'perps', 'predictions', 'sites'], }); render(); @@ -174,15 +180,17 @@ describe('ExploreSearchResults', () => { tokens: [{ assetId: '1', symbol: 'BTC', name: 'Bitcoin' }], perps: [], predictions: [], + stocks: [], sites: [], }, isLoading: { tokens: false, perps: false, predictions: false, + stocks: false, sites: false, }, - sectionsOrder: ['tokens', 'perps', 'predictions', 'sites'], + sectionsOrder: ['tokens', 'stocks', 'perps', 'predictions', 'sites'], }); // Act @@ -203,15 +211,17 @@ describe('ExploreSearchResults', () => { tokens: [{ assetId: '1', symbol: 'BTC', name: 'Bitcoin' }], perps: [], predictions: [], + stocks: [], sites: [], }, isLoading: { tokens: false, perps: false, predictions: false, + stocks: false, sites: false, }, - sectionsOrder: ['tokens', 'perps', 'predictions', 'sites'], + sectionsOrder: ['tokens', 'stocks', 'perps', 'predictions', 'sites'], }); // Act @@ -231,15 +241,17 @@ describe('ExploreSearchResults', () => { tokens: [], perps: [], predictions: [], + stocks: [], sites: [], }, isLoading: { tokens: true, perps: false, predictions: false, + stocks: false, sites: false, }, - sectionsOrder: ['tokens', 'perps', 'predictions', 'sites'], + sectionsOrder: ['tokens', 'stocks', 'perps', 'predictions', 'sites'], }); // Act @@ -258,15 +270,17 @@ describe('ExploreSearchResults', () => { tokens: [], perps: [], predictions: [], + stocks: [], sites: [], }, isLoading: { tokens: false, perps: false, predictions: false, + stocks: false, sites: false, }, - sectionsOrder: ['tokens', 'perps', 'predictions', 'sites'], + sectionsOrder: ['tokens', 'stocks', 'perps', 'predictions', 'sites'], }); // Act @@ -295,15 +309,17 @@ describe('ExploreSearchResults', () => { tokens: [{ assetId: '1', symbol: 'BTC', name: 'Bitcoin' }], perps: [{ symbol: 'BTC-USD', name: 'Bitcoin' }], predictions: [], + stocks: [], sites: [], }, isLoading: { tokens: false, perps: false, predictions: false, + stocks: false, sites: false, }, - sectionsOrder: ['tokens', 'perps', 'predictions', 'sites'], + sectionsOrder: ['tokens', 'stocks', 'perps', 'predictions', 'sites'], }); // Act @@ -325,17 +341,20 @@ describe('ExploreSearchResults', () => { tokens: [{ assetId: '1', symbol: 'BTC', name: 'Bitcoin' }], perps: [], predictions: [], + stocks: [], sites: [], }, isLoading: { tokens: false, perps: false, predictions: false, + stocks: false, sites: false, }, sectionsOrder: [ 'tokens', 'unknown' as 'tokens', // Intentionally invalid ID to test graceful handling + 'stocks', 'perps', 'predictions', 'sites', diff --git a/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx b/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx index f1b5630fcef..841fa2576be 100644 --- a/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx +++ b/app/components/Views/TrendingView/components/QuickActions/QuickActions.tsx @@ -3,16 +3,48 @@ import { ScrollView, TouchableOpacity } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { Box, - Icon, - IconColor, - IconSize, + Icon as DSIcon, + IconColor as DSIconColor, + IconSize as DSIconSize, Text, TextVariant, } from '@metamask/design-system-react-native'; +import LocalIcon, { + IconColor as LocalIconColor, + IconSize as LocalIconSize, +} from '../../../../../component-library/components/Icons/Icon'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; -import { useSectionsArray, SectionId } from '../../sections.config'; +import { + useSectionsArray, + SectionId, + type SectionIcon, +} from '../../sections.config'; import { TrendingViewSelectorsIDs } from '../../TrendingView.testIds'; +const SectionIconRenderer: React.FC<{ + icon: SectionIcon; + style?: object; +}> = ({ icon, style }) => { + if (icon.source === 'design-system') { + return ( + + ); + } + return ( + + ); +}; + interface QuickActionsProps { /** Set of section IDs that have empty data and should be hidden */ emptySections: Set; @@ -47,10 +79,8 @@ const QuickActions: React.FC = ({ emptySections }) => { 'flex-row items-center justify-center gap-1 rounded-xl bg-background-section px-3 py-2', )} > - {section.title} diff --git a/app/components/Views/TrendingView/hooks/useExploreSearch.test.ts b/app/components/Views/TrendingView/hooks/useExploreSearch.test.ts index a4fe12bffb6..39ca30fd05e 100644 --- a/app/components/Views/TrendingView/hooks/useExploreSearch.test.ts +++ b/app/components/Views/TrendingView/hooks/useExploreSearch.test.ts @@ -91,9 +91,18 @@ jest.mock('../../../UI/Sites/hooks/useSiteData/useSitesData', () => ({ }), })); +jest.mock('../../../UI/Trending/hooks/useRwaTokens/useRwaTokens', () => ({ + useRwaTokens: () => ({ + data: [], + isLoading: false, + refetch: jest.fn(), + }), +})); + // Mock useSectionsArray to return all sections for testing const mockSectionsArray: { id: SectionId }[] = [ { id: 'tokens' }, + { id: 'stocks' }, { id: 'perps' }, { id: 'predictions' }, { id: 'sites' }, @@ -248,7 +257,13 @@ describe('useExploreSearch', () => { }); it('returns custom sectionsOrder when provided in options', () => { - const customOrder = ['sites', 'tokens', 'perps', 'predictions'] as const; + const customOrder = [ + 'sites', + 'tokens', + 'stocks', + 'perps', + 'predictions', + ] as const; const { result } = renderHook(() => useExploreSearch('', { sectionsOrder: [...customOrder] }), ); diff --git a/app/components/Views/TrendingView/sections.config.tsx b/app/components/Views/TrendingView/sections.config.tsx index a2ff5e6e1b4..0c0eeae9294 100644 --- a/app/components/Views/TrendingView/sections.config.tsx +++ b/app/components/Views/TrendingView/sections.config.tsx @@ -12,10 +12,8 @@ import { filterMarketsByQuery, type PerpsMarketData, } from '@metamask/perps-controller'; -import PredictMarket from '../../UI/Predict/components/PredictMarket'; import type { PredictMarket as PredictMarketType } from '../../UI/Predict/types'; import type { PerpsNavigationParamList } from '../../UI/Perps/types/navigation'; -import PredictMarketSkeleton from '../../UI/Predict/components/PredictMarketSkeleton'; import { usePredictMarketData } from '../../UI/Predict/hooks/usePredictMarketData'; import { selectPerpsEnabledFlag } from '../../UI/Perps'; import { usePerpsMarkets } from '../../UI/Perps/hooks'; @@ -24,7 +22,8 @@ import { PerpsConnectionContext, } from '../../UI/Perps/providers/PerpsConnectionProvider'; import { PerpsStreamProvider } from '../../UI/Perps/providers/PerpsStreamManager'; -import { Box, IconName } from '@metamask/design-system-react-native'; +import { IconName as DSIconName } from '@metamask/design-system-react-native'; +import { IconName as LocalIconName } from '../../../component-library/components/Icons/Icon/Icon.types'; import type { SiteData } from '../../UI/Sites/components/SiteRowItem/SiteRowItem'; import SiteRowItemWrapper from '../../UI/Sites/components/SiteRowItemWrapper/SiteRowItemWrapper'; import SiteSkeleton from '../../UI/Sites/components/SiteSkeleton/SiteSkeleton'; @@ -37,9 +36,13 @@ import { import type { TrendingFilterContext } from '../../UI/Trending/components/TrendingTokensList/TrendingTokensList'; import PredictMarketRowItem from '../../UI/Predict/components/PredictMarketRowItem'; import SectionCard from './components/Sections/SectionTypes/SectionCard'; -import SectionCarrousel from './components/Sections/SectionTypes/SectionCarrousel'; +import { useRwaTokens } from '../../UI/Trending/hooks/useRwaTokens/useRwaTokens'; -export type SectionId = 'predictions' | 'tokens' | 'perps' | 'sites'; +export type SectionId = 'predictions' | 'tokens' | 'perps' | 'stocks' | 'sites'; + +export type SectionIcon = + | { source: 'local'; name: LocalIconName } + | { source: 'design-system'; name: DSIconName }; interface SectionData { data: unknown[]; @@ -49,7 +52,7 @@ interface SectionData { interface SectionConfig { id: SectionId; title: string; - icon: IconName; + icon: SectionIcon; viewAllAction: (navigation: NavigationProp) => void; RowItem: React.ComponentType<{ item: unknown; @@ -163,7 +166,7 @@ export const SECTIONS_CONFIG: Record = { tokens: { id: 'tokens', title: strings('trending.trending_tokens'), - icon: IconName.Ethereum, + icon: { source: 'design-system', name: DSIconName.Ethereum }, viewAllAction: (navigation) => { navigation.navigate(Routes.WALLET.TRENDING_TOKENS_FULL_VIEW); }, @@ -205,7 +208,7 @@ export const SECTIONS_CONFIG: Record = { perps: { id: 'perps', title: strings('trending.perps'), - icon: IconName.Candlestick, + icon: { source: 'design-system', name: DSIconName.Candlestick }, viewAllAction: (navigation) => { navigation.navigate(Routes.PERPS.ROOT, { screen: Routes.PERPS.MARKET_LIST, @@ -258,33 +261,53 @@ export const SECTIONS_CONFIG: Record = { }; }, }, + stocks: { + id: 'stocks', + title: strings('trending.stocks'), + icon: { source: 'local', name: LocalIconName.CorporateFare }, + viewAllAction: (navigation) => { + navigation.navigate(Routes.WALLET.RWA_TOKENS_FULL_VIEW); + }, + RowItem: ({ item, index }) => ( + + ), + OverrideRowItemSearch: ({ item, index }) => ( + + ), + Skeleton: TrendingTokensSkeleton, + Section: SectionCard, + useSectionData: (searchQuery) => { + const { data, isLoading, refetch } = useRwaTokens({ searchQuery }); + return { data, isLoading, refetch }; + }, + }, predictions: { id: 'predictions', title: strings('wallet.predict'), - icon: IconName.Speedometer, + icon: { source: 'design-system', name: DSIconName.Speedometer }, viewAllAction: (navigation) => { navigation.navigate(Routes.PREDICT.ROOT, { screen: Routes.PREDICT.MARKET_LIST, }); }, RowItem: ({ item, index: _index }) => ( - - - + ), OverrideRowItemSearch: ({ item }) => ( ), - Skeleton: () => , + Skeleton: SiteSkeleton, // Using sites skeleton cause PredictMarketSkeleton has too much spacing OverrideSkeletonSearch: SiteSkeleton, - Section: SectionCarrousel, + Section: SectionCard, useSectionData: (searchQuery) => { const { marketData, isFetching, refetch } = usePredictMarketData({ category: 'trending', @@ -303,7 +326,7 @@ export const SECTIONS_CONFIG: Record = { sites: { id: 'sites', title: strings('trending.sites'), - icon: IconName.Global, + icon: { source: 'design-system', name: DSIconName.Global }, viewAllAction: (navigation) => { navigation.navigate(Routes.SITES_FULL_VIEW); }, @@ -321,15 +344,17 @@ export const SECTIONS_CONFIG: Record = { // Sorted by order on the main screen const HOME_SECTIONS_ARRAY: (SectionConfig & { id: SectionId })[] = [ - SECTIONS_CONFIG.predictions, SECTIONS_CONFIG.tokens, + SECTIONS_CONFIG.stocks, SECTIONS_CONFIG.perps, + SECTIONS_CONFIG.predictions, SECTIONS_CONFIG.sites, ]; // Sorted by order on the QuickAction buttons and SearchResults const SECTIONS_ARRAY: (SectionConfig & { id: SectionId })[] = [ SECTIONS_CONFIG.tokens, + SECTIONS_CONFIG.stocks, SECTIONS_CONFIG.perps, SECTIONS_CONFIG.predictions, SECTIONS_CONFIG.sites, @@ -376,6 +401,9 @@ export const useSectionsData = ( const { data: perpsMarkets, isLoading: isPerpsLoading } = SECTIONS_CONFIG.perps.useSectionData(searchQuery); + const { data: stocks, isLoading: isStocksLoading } = + SECTIONS_CONFIG.stocks.useSectionData(searchQuery); + const { data: predictionMarkets, isLoading: isPredictionsLoading } = SECTIONS_CONFIG.predictions.useSectionData(searchQuery); @@ -391,6 +419,11 @@ export const useSectionsData = ( data: perpsMarkets, isLoading: isPerpsLoading, }, + stocks: { + // Avoids making 2 API calls to the search endpoint when searching on the main search + data: stocks, + isLoading: isStocksLoading, + }, predictions: { data: predictionMarkets, isLoading: isPredictionsLoading, diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 84d69e111c1..f223a0b15ef 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -258,6 +258,7 @@ const Routes = { NFTS_FULL_VIEW: 'NftFullView', TOKENS_FULL_VIEW: 'TokensFullView', TRENDING_TOKENS_FULL_VIEW: 'TrendingTokensFullView', + RWA_TOKENS_FULL_VIEW: 'RWATokensFullView', DEFI_FULL_VIEW: 'DeFiFullView', }, VAULT_RECOVERY: { diff --git a/app/core/NavigationService/types.ts b/app/core/NavigationService/types.ts index 9ddf3c55890..8769ce5d452 100644 --- a/app/core/NavigationService/types.ts +++ b/app/core/NavigationService/types.ts @@ -443,6 +443,7 @@ export interface RootStackParamList extends ParamListBase { NftFullView: undefined; TokensFullView: undefined; TrendingTokensFullView: undefined; + RWATokensFullView: undefined; // Vault recovery routes RestoreWallet: undefined; diff --git a/locales/languages/en.json b/locales/languages/en.json index 832ea7949a7..b0e88a5c617 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -7811,6 +7811,7 @@ "trending": { "title": "Explore", "trending_tokens": "Trending tokens", + "stocks": "Stocks", "price_change": "Price change", "all_networks": "All networks", "24h": "24h", diff --git a/tests/api-mocking/mock-responses/trending-api-mocks.ts b/tests/api-mocking/mock-responses/trending-api-mocks.ts index 2bcb46a8eb0..7071d9b7320 100644 --- a/tests/api-mocking/mock-responses/trending-api-mocks.ts +++ b/tests/api-mocking/mock-responses/trending-api-mocks.ts @@ -15,6 +15,26 @@ const TRENDING_TOKENS_RESPONSE = [ }, ]; +export const RWA_STOCK_ASSET_ID = + 'eip155:1/erc20:0x96f6ef951840721adbf46ac996b59e0235cb985c'; + +const RWA_TOKENS_SEARCH_RESPONSE = { + count: 1, + data: [ + { + assetId: RWA_STOCK_ASSET_ID, + symbol: 'USDY', + name: 'Ondo US Dollar Yield (Ondo Tokenized)', + decimals: 18, + price: '1.05', + aggregatedUsdVolume: 500000, + marketCap: 200000000, + pricePercentChange1d: '0.12', + rwaData: { type: 'rwa' }, + }, + ], +}; + export const TRENDING_API_MOCKS: MockEventsObject = { GET: [ { @@ -122,6 +142,12 @@ export const TRENDING_API_MOCKS: MockEventsObject = { ], priority: 1000, }, + { + urlEndpoint: /\/tokens\/search.*Ondo/, + responseCode: 200, + response: RWA_TOKENS_SEARCH_RESPONSE, + priority: 1001, + }, { urlEndpoint: /\/tokens\/search.*/, responseCode: 200, diff --git a/tests/component-view/renderers/trending.ts b/tests/component-view/renderers/trending.ts index eb8f8b5cbfe..98f10a7ee70 100644 --- a/tests/component-view/renderers/trending.ts +++ b/tests/component-view/renderers/trending.ts @@ -7,7 +7,8 @@ import Routes from '../../../app/constants/navigation/Routes'; import { ExploreFeed } from '../../../app/components/Views/TrendingView/TrendingView'; import ExploreSearchScreen from '../../../app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen'; import AssetDetails from '../../../app/components/Views/AssetDetails'; -import TrendingTokensFullView from '../../../app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView'; +import TrendingTokensFullView from '../../../app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView'; +import RWATokensFullView from '../../../app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView'; import { initialStateTrending } from '../presets/trending'; interface RenderTrendingViewOptions { @@ -44,6 +45,10 @@ export function renderTrendingViewWithRoutes( Component: TrendingTokensFullView as unknown as React.ComponentType, }, + { + name: Routes.WALLET.RWA_TOKENS_FULL_VIEW, + Component: RWATokensFullView as unknown as React.ComponentType, + }, ], { state }, ); diff --git a/tests/locators/Trending/TrendingView.selectors.ts b/tests/locators/Trending/TrendingView.selectors.ts index d4be3f30435..eea617ae9ae 100644 --- a/tests/locators/Trending/TrendingView.selectors.ts +++ b/tests/locators/Trending/TrendingView.selectors.ts @@ -8,7 +8,7 @@ export const TrendingViewSelectorsIDs = { SEARCH_CANCEL_BUTTON: 'explore-search-cancel-button', TOKEN_ROW_ITEM_PREFIX: 'trending-token-row-item-', PERPS_ROW_ITEM_PREFIX: 'perps-market-row-item-', - PREDICTIONS_ROW_ITEM_PREFIX: 'predict-market-list-trending-card-', + PREDICTIONS_ROW_ITEM_PREFIX: 'predict-market-row-item-', SITE_ROW_ITEM_PREFIX: 'site-row-item-', SEARCH_FOOTER_GOOGLE_LINK: 'trending-search-footer-google-link', SCROLL_VIEW: AppTrendingViewSelectorsIDs.TRENDING_FEED_SCROLL_VIEW, @@ -22,6 +22,7 @@ export const TrendingViewSelectorsText = { // Section titles - must match the actual localized strings from sections.config.tsx SECTION_PREDICTIONS: 'Predictions', SECTION_TOKENS: 'Trending tokens', + SECTION_STOCKS: 'Stocks', SECTION_PERPS: 'Perps', SECTION_SITES: 'Sites', } as const; @@ -30,6 +31,7 @@ export const TrendingViewSelectorsText = { export const SECTION_BACK_BUTTONS: Record = { [TrendingViewSelectorsText.SECTION_TOKENS]: 'trending-tokens-header-back-button', + [TrendingViewSelectorsText.SECTION_STOCKS]: 'rwa-tokens-header-back-button', [TrendingViewSelectorsText.SECTION_PERPS]: 'perps-market-list-close-button-back-button', [TrendingViewSelectorsText.SECTION_SITES]: @@ -48,6 +50,7 @@ export const DETAILS_BACK_BUTTONS: Record = { export const SECTION_FULL_VIEW_HEADERS: Record = { [TrendingViewSelectorsText.SECTION_SITES]: 'sites-full-view-header', [TrendingViewSelectorsText.SECTION_TOKENS]: 'trending-tokens-header', + [TrendingViewSelectorsText.SECTION_STOCKS]: 'rwa-tokens-header', [TrendingViewSelectorsText.SECTION_PERPS]: 'perps-market-list-close-button', [TrendingViewSelectorsText.SECTION_PREDICTIONS]: 'back-button', // PredictFeed uses back-button as main identifier }; diff --git a/tests/page-objects/Trending/TrendingView.ts b/tests/page-objects/Trending/TrendingView.ts index 05b079c2580..055dfae8f9b 100644 --- a/tests/page-objects/Trending/TrendingView.ts +++ b/tests/page-objects/Trending/TrendingView.ts @@ -143,6 +143,7 @@ class TrendingView { private getSectionId(sectionTitle: string): string { const sectionIdMap: Record = { 'Trending tokens': 'tokens', + Stocks: 'stocks', Sites: 'sites', Predictions: 'predictions', Perps: 'perps', @@ -203,13 +204,9 @@ class TrendingView { `section-header-view-all-${id}`, ); - // Determine scroll direction: Predictions and Trending tokens are usually near top - // But scrollToElement can handle both directions, so we try 'up' first for top sections - // and it will automatically adjust if needed - const direction = - sectionTitle === 'Predictions' || sectionTitle === 'Trending tokens' - ? 'up' - : 'down'; + // Trending tokens is at the top of the feed; scroll up to find it. + // All other sections (stocks, perps, predictions, sites) are below. + const direction = sectionTitle === 'Trending tokens' ? 'up' : 'down'; // Use generic scroll method await this.scrollToElementInFeed( diff --git a/tests/smoke/trending/trending-feed.spec.ts b/tests/smoke/trending/trending-feed.spec.ts index b3ca24d79af..c446e56855e 100644 --- a/tests/smoke/trending/trending-feed.spec.ts +++ b/tests/smoke/trending/trending-feed.spec.ts @@ -6,7 +6,10 @@ import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFea import { Mockttp } from 'mockttp'; import TrendingView from '../../page-objects/Trending/TrendingView'; import { TrendingViewSelectorsText } from '../../locators/Trending/TrendingView.selectors'; -import { TRENDING_API_MOCKS } from '../../api-mocking/mock-responses/trending-api-mocks'; +import { + TRENDING_API_MOCKS, + RWA_STOCK_ASSET_ID, +} from '../../api-mocking/mock-responses/trending-api-mocks'; import { setupMockEvents } from '../../api-mocking/helpers/mockHelpers'; import { remoteFeatureFlagTrendingTokensEnabled, @@ -38,11 +41,13 @@ describe(SmokeWalletPlatform('Trending Feed View All Navigation'), () => { // Navigate to Trending Tab await TrendingView.tapTrendingTab(); - // First, test QuickAction buttons (buttons below search bar) + // Test QuickAction buttons in their rendered order (left to right) + // to allow progressive right-scrolling through the horizontal list const quickActionSections = [ - TrendingViewSelectorsText.SECTION_PREDICTIONS, TrendingViewSelectorsText.SECTION_TOKENS, + TrendingViewSelectorsText.SECTION_STOCKS, TrendingViewSelectorsText.SECTION_PERPS, + TrendingViewSelectorsText.SECTION_PREDICTIONS, TrendingViewSelectorsText.SECTION_SITES, ]; @@ -63,18 +68,9 @@ describe(SmokeWalletPlatform('Trending Feed View All Navigation'), () => { await TrendingView.verifyFeedVisible(); } - // Define the sections to visit in order with their test data + // Define the sections to visit in feed order (top to bottom) for reliable + // progressive downward scrolling: tokens → stocks → perps → predictions → sites const sectionsConfig = [ - { - section: TrendingViewSelectorsText.SECTION_PREDICTIONS, - itemId: '1', - itemTitle: 'Will Bitcoin hit $100k?', - verifyItemVisible: () => TrendingView.verifyPredictionVisible('1'), - tapItem: () => TrendingView.tapPredictionRow('1'), - verifyDetailsVisible: () => - TrendingView.verifyPredictionDetailsVisible(), - tapBack: () => TrendingView.tapBackFromPredictionDetails(), - }, { section: TrendingViewSelectorsText.SECTION_TOKENS, itemId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', @@ -91,6 +87,19 @@ describe(SmokeWalletPlatform('Trending Feed View All Navigation'), () => { TrendingView.verifyTokenDetailsTitleVisible('USD Coin'), tapBack: () => TrendingView.tapBackFromTokenDetails(), }, + { + section: TrendingViewSelectorsText.SECTION_STOCKS, + itemId: RWA_STOCK_ASSET_ID, + itemTitle: 'Ondo US Dollar Yield (Ondo Tokenized)', + verifyItemVisible: () => + TrendingView.verifyTokenVisible(RWA_STOCK_ASSET_ID), + tapItem: () => TrendingView.tapTokenRow(RWA_STOCK_ASSET_ID), + verifyDetailsVisible: () => + TrendingView.verifyTokenDetailsTitleVisible( + 'Ondo US Dollar Yield (Ondo Tokenized)', + ), + tapBack: () => TrendingView.tapBackFromTokenDetails(), + }, { section: TrendingViewSelectorsText.SECTION_PERPS, itemId: 'BTC', @@ -100,6 +109,16 @@ describe(SmokeWalletPlatform('Trending Feed View All Navigation'), () => { verifyDetailsVisible: () => TrendingView.verifyPerpDetailsVisible(), tapBack: () => TrendingView.tapBackFromPerpDetails(), }, + { + section: TrendingViewSelectorsText.SECTION_PREDICTIONS, + itemId: '1', + itemTitle: 'Will Bitcoin hit $100k?', + verifyItemVisible: () => TrendingView.verifyPredictionVisible('1'), + tapItem: () => TrendingView.tapPredictionRow('1'), + verifyDetailsVisible: () => + TrendingView.verifyPredictionDetailsVisible(), + tapBack: () => TrendingView.tapBackFromPredictionDetails(), + }, { section: TrendingViewSelectorsText.SECTION_SITES, itemId: 'Uniswap', From 347c07225b07b1a8aa827eb13eb4ce2b5b8ecc77 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 3 Mar 2026 15:59:58 +0000 Subject: [PATCH 078/131] [skip ci] Bump version number to 3874 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 3a72ec7fa79..f5353d992d7 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.68.0" - versionCode 3873 + versionCode 3874 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index f03f9903849..db90d0ddb92 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.68.0 - opts: is_expand: false - VERSION_NUMBER: 3873 + VERSION_NUMBER: 3874 - opts: is_expand: false FLASK_VERSION_NAME: 7.68.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3873 + FLASK_VERSION_NUMBER: 3874 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index daaee676e75..9f13ec46e56 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3873; + CURRENT_PROJECT_VERSION = 3874; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3873; + CURRENT_PROJECT_VERSION = 3874; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3873; + CURRENT_PROJECT_VERSION = 3874; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3873; + CURRENT_PROJECT_VERSION = 3874; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3873; + CURRENT_PROJECT_VERSION = 3874; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3873; + CURRENT_PROJECT_VERSION = 3874; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 9ad6bba58bf0604a3a6440e747e25d6bd171deb5 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:19:14 +0000 Subject: [PATCH 079/131] chore(runway): cherry-pick fix: cp-7.68.0 Bump `TransactionPayController` controller version (#26908) - fix: cp-7.68.0 Bump `TransactionPayController` controller version (#26895) ## **Description** Updates `TransactionPayController` version to `16.1.2` to fix `POL` token issues in MMPay ## **Changelog** CHANGELOG entry: ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/26797 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I've included tests if applicable - [X] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Moderate risk because it changes a dependency involved in transaction/payment flows; behavior changes come from the upstream controller update rather than local code. > > **Overview** > Updates the `@metamask/transaction-pay-controller` dependency from `^16.1.1` to `^16.1.2` and refreshes `yarn.lock` to pull in the new published version. > > No application code changes are included beyond the dependency bump, so any functional impact is driven by the upstream controller release (intended to address MMPay `POL` token issues). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 12540d9ebdb891a3cf4126f6cc1d87c93b288d58. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [86fddcb](https://github.com/MetaMask/metamask-mobile/commit/86fddcb60ee619e9993bcc44781e8e793e1e4de7) Co-authored-by: OGPoyraz --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index db870557be1..f355fe525da 100644 --- a/package.json +++ b/package.json @@ -294,7 +294,7 @@ "@metamask/swappable-obj-proxy": "^2.1.0", "@metamask/swaps-controller": "^15.0.0", "@metamask/transaction-controller": "^62.19.0", - "@metamask/transaction-pay-controller": "^16.1.1", + "@metamask/transaction-pay-controller": "^16.1.2", "@metamask/tron-wallet-snap": "^1.21.1", "@metamask/utils": "^11.8.1", "@myx-trade/sdk": "^0.1.265", diff --git a/yarn.lock b/yarn.lock index 869fde2c193..52d487ddc1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10155,9 +10155,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-pay-controller@npm:^16.1.1": - version: 16.1.1 - resolution: "@metamask/transaction-pay-controller@npm:16.1.1" +"@metamask/transaction-pay-controller@npm:^16.1.2": + version: 16.1.2 + resolution: "@metamask/transaction-pay-controller@npm:16.1.2" dependencies: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" @@ -10178,7 +10178,7 @@ __metadata: bn.js: "npm:^5.2.1" immer: "npm:^9.0.6" lodash: "npm:^4.17.21" - checksum: 10/23f537240235afb68bb968f19ccbbcd638fbc6e8e04db2e9d6edaec873739de12261b55aff532b5f533b184a22b46e99ed76840287c5f729907367852a5be001 + checksum: 10/d304824be5a3218a28b39ccde514748f38b6ed23d694fee08f562c53dc9c16d3257f6a036bc26b5193db35b5bb3e9d9669b2411ff532285738e7310e81090654 languageName: node linkType: hard @@ -35477,7 +35477,7 @@ __metadata: "@metamask/test-dapp-multichain": "npm:^0.17.1" "@metamask/test-dapp-solana": "npm:^0.3.0" "@metamask/transaction-controller": "npm:^62.19.0" - "@metamask/transaction-pay-controller": "npm:^16.1.1" + "@metamask/transaction-pay-controller": "npm:^16.1.2" "@metamask/tron-wallet-snap": "npm:^1.21.1" "@metamask/utils": "npm:^11.8.1" "@myx-trade/sdk": "npm:^0.1.265" From c82785ea7bf7c921f2e21b9e64931561e667d57c Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 3 Mar 2026 16:21:18 +0000 Subject: [PATCH 080/131] [skip ci] Bump version number to 3875 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index f5353d992d7..529257ee66b 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.68.0" - versionCode 3874 + versionCode 3875 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index db90d0ddb92..556aef959f6 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.68.0 - opts: is_expand: false - VERSION_NUMBER: 3874 + VERSION_NUMBER: 3875 - opts: is_expand: false FLASK_VERSION_NAME: 7.68.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3874 + FLASK_VERSION_NUMBER: 3875 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 9f13ec46e56..ad0a21720b8 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3874; + CURRENT_PROJECT_VERSION = 3875; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3874; + CURRENT_PROJECT_VERSION = 3875; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3874; + CURRENT_PROJECT_VERSION = 3875; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3874; + CURRENT_PROJECT_VERSION = 3875; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3874; + CURRENT_PROJECT_VERSION = 3875; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3874; + CURRENT_PROJECT_VERSION = 3875; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 25299674122b7914c8aa68c565a14eff714cc859 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:32:11 +0000 Subject: [PATCH 081/131] chore(runway): cherry-pick fix(perps): recover connection after app state changes (#26918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(perps): recover connection after app state changes cp-7.67.1 (#26780) ## **Description** Fixes Perps WebSocket connectivity issues when: 1. **App returns from background** — after a few minutes in background, the OS silently kills the WebSocket but `PerpsConnectionManager` still reports `isConnected = true`. No code path detected the stale connection or triggered reconnection. 2. **WiFi/network drops and restores** — toggling WiFi, airplane mode, or losing cellular signal kills the WebSocket, but since the app stays in `active` state, the existing `AppState`-based recovery (if any) never fires. ### Root Cause `PerpsConnectionManager` had no lifecycle awareness of: - **React Native `AppState` transitions** (background → foreground) - **Network connectivity changes** (offline → online via `@react-native-community/netinfo`) Arthur's prior fix ([#26334](https://github.com/MetaMask/metamask-mobile/pull/26334)) made `StreamChannel.ensureReady()` connection-aware to avoid blind polling on slow connections, but it only helps when a reconnection is **already in progress** (`isConnecting = true`). After background resume or WiFi restore, nobody triggers the reconnection in the first place. ### Fix - **`AppState` listener** — on `active`, cancels any pending grace period and runs `validateAndReconnect()` - **`NetInfo` listener** — tracks `wasOffline` state; on offline → online transition, runs `validateAndReconnect()` - **`validateAndReconnect(context)`** — shared method that sends a lightweight `ping()` health check to the active provider. If the ping fails (stale WebSocket), marks the connection as lost and triggers `reconnectWithNewContext({ force: true })` which reinitializes the controller, validates with a fresh health check, and preloads all stream subscriptions. - **Cleanup** — both listeners are properly removed in `cleanupStateMonitoring()` ## **Changelog** CHANGELOG entry: Fixed Perps WebSocket not reconnecting after app resume from background or WiFi/network toggle ## **Related issues** Fixes: connectivity loss after backgrounding app, WiFi off/on not recovering Perps data ## **Manual testing steps** ```gherkin Feature: Perps connection recovery Scenario: App returns from background after several minutes Given the user has navigated to the Perps trading screen And the user has an open position When the user backgrounds the app for 3+ minutes And the user returns to the app Then the Perps WebSocket reconnects automatically And positions, prices, and account data resume updating Scenario: WiFi is toggled off and back on Given the user is viewing live Perps positions And WiFi is connected When the user turns WiFi off And waits a few seconds And turns WiFi back on Then the Perps WebSocket reconnects after network is restored And live data resumes without requiring navigation away Scenario: Airplane mode is toggled Given the user is on the Perps trading screen When the user enables airplane mode And then disables airplane mode Then the connection recovers and live data resumes ``` ## **Screenshots/Recordings** ### **Before** After backgrounding or WiFi toggle, Perps shows stale data with no automatic recovery. User must navigate away and back to restore the connection. ### **After** Connection automatically recovers via health-check ping and force reconnection. Live data resumes within seconds. ## **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 - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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 Perps connection lifecycle and reconnection paths; regressions could cause reconnect loops or delayed/stuck loading during flaky connectivity. > > **Overview** > Improves Perps WebSocket resilience by adding AppState and NetInfo listeners in `PerpsConnectionManager` to detect background→foreground and offline→online transitions, validate the connection via provider `ping()`, and force a reconnect when stale. > > Adds network-restore retry/backoff knobs (`NetworkRestoreMaxRetries`, `NetworkRestoreRetryBaseMs`) and ensures cleanup of new subscriptions/timers on teardown; reconnection now explicitly calls `PerpsController.disconnect()` before `init()` to avoid skipping re-init on a dead socket. > > Updates `usePerpsHomeData` to treat WebSocket-backed sections (positions/orders/activity) as loading while `isConnecting`, preventing brief empty-state flashes during reconnection. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b3aab14cd5c66e00f6cb80762f5f7b67f2ccbfe6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [c4f83e4](https://github.com/MetaMask/metamask-mobile/commit/c4f83e4148417219ccde8dc0f59442d46aca07de) Co-authored-by: Alejandro Garcia Anglada --- .../UI/Perps/hooks/usePerpsHomeData.ts | 10 +- .../Perps/services/PerpsConnectionManager.ts | 197 ++++++++++++++++++ .../perps/constants/perpsConfig.ts | 2 + 3 files changed, 205 insertions(+), 4 deletions(-) diff --git a/app/components/UI/Perps/hooks/usePerpsHomeData.ts b/app/components/UI/Perps/hooks/usePerpsHomeData.ts index 43467a97dd7..38c48d4578d 100644 --- a/app/components/UI/Perps/hooks/usePerpsHomeData.ts +++ b/app/components/UI/Perps/hooks/usePerpsHomeData.ts @@ -63,7 +63,7 @@ export const usePerpsHomeData = ({ searchQuery = '', }: UsePerpsHomeDataParams = {}): UsePerpsHomeDataReturn => { // Get connection state to guard REST calls that require an initialized controller - const { isConnected, isInitialized } = usePerpsConnection(); + const { isConnected, isInitialized, isConnecting } = usePerpsConnection(); // Fetch positions via WebSocket with throttling for performance const { positions, isInitialLoading: isPositionsLoading } = @@ -366,12 +366,14 @@ export const usePerpsHomeData = ({ recentActivity: limitedActivity, sortBy, isLoading: { - positions: isPositionsLoading, - orders: isOrdersLoading, + // During reconnection, treat WebSocket-backed data as loading so the UI + // shows skeletons instead of briefly flashing "no positions" → positions. + positions: isPositionsLoading || isConnecting, + orders: isOrdersLoading || isConnecting, markets: isMarketsLoading, // Only wait for WebSocket fills (fast ~100ms), not REST fills (slow 3s+) // REST fills merge in background via mergedFills without blocking initial render - activity: isFillsLoading, + activity: isFillsLoading || isConnecting, }, refresh, }; diff --git a/app/components/UI/Perps/services/PerpsConnectionManager.ts b/app/components/UI/Perps/services/PerpsConnectionManager.ts index dff4bf9695b..4632db78eb2 100644 --- a/app/components/UI/Perps/services/PerpsConnectionManager.ts +++ b/app/components/UI/Perps/services/PerpsConnectionManager.ts @@ -1,3 +1,8 @@ +import { AppState, type AppStateStatus } from 'react-native'; +import { + addEventListener as netInfoAddEventListener, + type NetInfoState, +} from '@react-native-community/netinfo'; import { captureException, setMeasurement } from '@sentry/react-native'; import BackgroundTimer from 'react-native-background-timer'; import performance from 'react-native-performance'; @@ -57,6 +62,13 @@ class PerpsConnectionManagerClass { private isInGracePeriod = false; private pendingReconnectPromise: Promise | null = null; private connectionTimeoutRef: ReturnType | null = null; + private appStateSubscription: ReturnType< + typeof AppState.addEventListener + > | null = null; + private netInfoUnsubscribe: (() => void) | null = null; + private wasOffline = false; + private networkRestoreRetryTimer: ReturnType | null = null; + private networkRestoreRetryCount = 0; private constructor() { // Private constructor to enforce singleton pattern @@ -178,13 +190,188 @@ class PerpsConnectionManagerClass { this.previousHip3Version = currentHip3Version; }); + // Listen for app state changes to reconnect after background + if (!this.appStateSubscription) { + this.appStateSubscription = AppState.addEventListener( + 'change', + (nextAppState: AppStateStatus) => { + this.handleAppStateChange(nextAppState).catch((error) => { + Logger.error( + ensureError(error, 'PerpsConnectionManager.appStateListener'), + { + tags: { feature: PERPS_CONSTANTS.FeatureName }, + context: { + name: 'PerpsConnectionManager.appStateListener', + data: { message: 'Error handling app state change' }, + }, + }, + ); + }); + }, + ); + } + + // Listen for connectivity changes (WiFi off/on, airplane mode, etc.) + if (!this.netInfoUnsubscribe) { + this.netInfoUnsubscribe = netInfoAddEventListener( + (netState: NetInfoState) => { + const isOnline = netState.isInternetReachable === true; + + if (isOnline && this.wasOffline) { + DevLogger.log( + 'PerpsConnectionManager: Network restored - validating connection', + ); + this.reconnectAfterNetworkRestore(); + } + + if (!isOnline) { + this.wasOffline = true; + } else if (isOnline && this.isConnected) { + this.wasOffline = false; + } + }, + ); + } + DevLogger.log('PerpsConnectionManager: State monitoring set up'); } + /** + * Validate the WebSocket connection and force-reconnect if it is stale. + * Shared by both AppState (background→foreground) and NetInfo (offline→online) + * recovery paths. + * + * @param context - Caller identifier for error reporting + * @param skipPing - Skip ping and force reconnect after known network loss + */ + private async validateAndReconnect( + context: string, + skipPing = false, + ): Promise { + if (this.connectionRefCount <= 0 || this.isConnecting) { + return; + } + + if (this.isConnected && !skipPing) { + try { + const provider = Engine.context.PerpsController.getActiveProvider(); + await provider.ping(); + DevLogger.log( + `PerpsConnectionManager: ${context} - connection healthy`, + ); + return; + } catch { + DevLogger.log( + `PerpsConnectionManager: ${context} - connection stale, triggering reconnection`, + ); + } + } + + this.isConnected = false; + this.isInitialized = false; + + await this.reconnectWithNewContext({ force: true }); + DevLogger.log( + `PerpsConnectionManager: ${context} - reconnection successful`, + ); + } + + /** + * Attempt reconnection after network restore with exponential backoff. + * WebSocket endpoints may not be reachable immediately even though + * NetInfo reports isInternetReachable: true. + */ + private reconnectAfterNetworkRestore(): void { + this.cancelNetworkRestoreRetry(); + this.networkRestoreRetryCount = 0; + this.attemptNetworkRestoreReconnect(); + } + + private attemptNetworkRestoreReconnect(): void { + this.validateAndReconnect('PerpsConnectionManager.netInfoListener', true) + .then(() => { + this.wasOffline = false; + this.networkRestoreRetryCount = 0; + }) + .catch((error) => { + this.networkRestoreRetryCount++; + if ( + this.networkRestoreRetryCount < + PERPS_CONSTANTS.NetworkRestoreMaxRetries + ) { + const delay = + PERPS_CONSTANTS.NetworkRestoreRetryBaseMs * + this.networkRestoreRetryCount; + DevLogger.log( + `PerpsConnectionManager: Network restore retry ${this.networkRestoreRetryCount} in ${delay}ms`, + ); + this.networkRestoreRetryTimer = setTimeout(() => { + this.networkRestoreRetryTimer = null; + this.attemptNetworkRestoreReconnect(); + }, delay); + } else { + Logger.error( + ensureError(error, 'PerpsConnectionManager.netInfoListener'), + { + tags: { feature: PERPS_CONSTANTS.FeatureName }, + context: { + name: 'PerpsConnectionManager.netInfoListener', + data: { + message: + 'Network restore reconnection failed after max retries', + }, + }, + }, + ); + } + }); + } + + private cancelNetworkRestoreRetry(): void { + if (this.networkRestoreRetryTimer) { + clearTimeout(this.networkRestoreRetryTimer); + this.networkRestoreRetryTimer = null; + } + this.networkRestoreRetryCount = 0; + } + + /** + * Handle app state transitions to recover from stale WebSocket connections. + * When the app returns from background, the OS may have silently killed the + * WebSocket. A health-check ping detects this and triggers reconnection. + */ + private async handleAppStateChange( + nextAppState: AppStateStatus, + ): Promise { + if (nextAppState !== 'active') { + return; + } + + // Cancel any pending grace period — user is back + if (this.isInGracePeriod) { + DevLogger.log( + 'PerpsConnectionManager: App resumed - cancelling grace period', + ); + this.cancelGracePeriod(); + } + + await this.validateAndReconnect('appResume'); + } + /** * Clean up state monitoring */ private cleanupStateMonitoring(): void { + if (this.appStateSubscription) { + this.appStateSubscription.remove(); + this.appStateSubscription = null; + } + if (this.netInfoUnsubscribe) { + this.netInfoUnsubscribe(); + this.netInfoUnsubscribe = null; + this.wasOffline = false; + } + this.cancelNetworkRestoreRetry(); if (this.unsubscribeFromStore) { this.unsubscribeFromStore(); this.unsubscribeFromStore = null; @@ -747,7 +934,17 @@ class PerpsConnectionManagerClass { this.clearError(); // Stage 2: Force the controller to reinitialize with new context + // Disconnect first so the controller resets its isInitialized flag — + // without this, init() silently skips when the controller thinks it's + // already initialized (even though the underlying WebSocket is dead). const reinitStart = performance.now(); + try { + await Engine.context.PerpsController.disconnect(); + } catch { + DevLogger.log( + 'PerpsConnectionManager: disconnect before reinit failed (continuing)', + ); + } await Engine.context.PerpsController.init(); setMeasurement( PerpsMeasurementName.PerpsControllerReinit, diff --git a/app/controllers/perps/constants/perpsConfig.ts b/app/controllers/perps/constants/perpsConfig.ts index c12fe232d0e..d19c3184b20 100644 --- a/app/controllers/perps/constants/perpsConfig.ts +++ b/app/controllers/perps/constants/perpsConfig.ts @@ -30,6 +30,8 @@ export const PERPS_CONSTANTS = { ReconnectionDelayAndroidMs: 300, // Android-specific reconnection delay for better reliability on slower devices ReconnectionDelayIosMs: 100, // iOS-specific reconnection delay for optimal performance ReconnectionRetryDelayMs: 5_000, // 5 seconds delay between reconnection attempts + NetworkRestoreMaxRetries: 8, // Max retry attempts when reconnecting after WiFi/network restore + NetworkRestoreRetryBaseMs: 1_500, // Base delay (ms) between network restore retries (multiplied by attempt number) // Connection manager timing constants BalanceUpdateThrottleMs: 15000, // Update at most every 15 seconds to reduce state updates in PerpsConnectionManager From 8be47b33ba0fdb52bdcebb91ac348109d4fb4460 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Tue, 3 Mar 2026 17:33:50 +0000 Subject: [PATCH 082/131] [skip ci] Bump version number to 3877 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 529257ee66b..d9266d00ab9 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.68.0" - versionCode 3875 + versionCode 3877 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 556aef959f6..5dff75570ba 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.68.0 - opts: is_expand: false - VERSION_NUMBER: 3875 + VERSION_NUMBER: 3877 - opts: is_expand: false FLASK_VERSION_NAME: 7.68.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3875 + FLASK_VERSION_NUMBER: 3877 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index ad0a21720b8..9fee55a81cc 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3875; + CURRENT_PROJECT_VERSION = 3877; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3875; + CURRENT_PROJECT_VERSION = 3877; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3875; + CURRENT_PROJECT_VERSION = 3877; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3875; + CURRENT_PROJECT_VERSION = 3877; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3875; + CURRENT_PROJECT_VERSION = 3877; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3875; + CURRENT_PROJECT_VERSION = 3877; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From cfc36882f9405c9a794c3bd66f8b545eefc01340 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:08:21 +0000 Subject: [PATCH 083/131] chore(runway): cherry-pick fix(predict): prediction market positions not updating when switching accounts cp-7.68.0 (#26924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(predict): prediction market positions not updating when switching accounts cp-7.68.0 (#26916) ## **Description** When switching accounts, prediction market positions (and balance, activity, P&L) did not update — the UI kept showing data from the previous account until a manual pull-to-refresh. **Root cause:** Predict hooks use `getEvmAccountFromSelectedAccountGroup()` — a plain imperative function that reads from `Engine.context.AccountTreeController` but never triggers React re-renders. Combined with `useMemo`-ed tab elements in `WalletTokensTabView` (whose deps don't include account state), the Predict components never re-render when the account changes, so the React Query cache key stays pinned to the old address. **Fix:** Added `useSelector(selectSelectedAccountGroupId)` to all 7 affected hooks/components. This subscribes each to Redux account-group changes, triggering re-renders that cause `getEvmAccountFromSelectedAccountGroup()` to re-evaluate with the new account and produce a fresh React Query cache key. ## **Changelog** CHANGELOG entry: Fixed prediction market positions not updating when switching accounts ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/26719 ## **Manual testing steps** ```gherkin Feature: Prediction market positions update on account switch Scenario: user switches from an account with positions to another account Given user is on the home screen with an account that has prediction market positions And the prediction market positions are displayed When user switches to a different account Then the prediction market positions update to reflect the new account's data And if the new account has no positions, trending markets are shown instead Scenario: user switches accounts and pulls to refresh Given user is on the home screen after switching accounts When user swipes down to refresh Then the data shown matches the currently selected account ``` ## **Screenshots/Recordings** ### **Before** ### **After** https://www.loom.com/share/130885ec91ae41ecbfc6a2bbfa9e272d ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Low risk: changes are limited to adding Redux subscriptions to trigger re-renders on account-group changes, plus test-mock updates; no business logic or transaction flows are altered. > > **Overview** > Fixes stale Predict data after switching accounts by making affected Predict hooks/components re-render on account-group changes via `useSelector(selectSelectedAccountGroupId)`, ensuring React Query keys update to the new address. > > Updates unit tests for these hooks to mock `selectSelectedAccountGroupId` (and `useSelector`) so the new subscription doesn’t break existing test setups. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b6211c088abf80ddeadab7423cf1fe9f6bbd6df5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [f5ad1fe](https://github.com/MetaMask/metamask-mobile/commit/f5ad1febb2b0a0de12b5ee631a12e626aca3f093) Co-authored-by: Luis Taniça --- .../PredictPositionsHeader.tsx | 3 +++ .../UI/Predict/hooks/usePredictActivity.test.ts | 12 ++++++++++++ .../UI/Predict/hooks/usePredictActivity.ts | 5 ++++- .../UI/Predict/hooks/usePredictBalance.test.ts | 12 ++++++++++++ app/components/UI/Predict/hooks/usePredictBalance.ts | 5 ++++- .../UI/Predict/hooks/usePredictDeposit.test.ts | 7 +++++++ app/components/UI/Predict/hooks/usePredictDeposit.ts | 3 +++ .../UI/Predict/hooks/usePredictPositions.test.ts | 12 ++++++++++++ .../UI/Predict/hooks/usePredictPositions.ts | 4 ++++ .../hooks/usePredictToastRegistrations.test.tsx | 12 ++++++++++++ .../Predict/hooks/usePredictToastRegistrations.tsx | 4 ++++ app/components/UI/Predict/hooks/useUnrealizedPnL.tsx | 4 ++++ 12 files changed, 81 insertions(+), 2 deletions(-) diff --git a/app/components/UI/Predict/components/PredictPositionsHeader/PredictPositionsHeader.tsx b/app/components/UI/Predict/components/PredictPositionsHeader/PredictPositionsHeader.tsx index bc37ab225b9..339e05cbfb6 100644 --- a/app/components/UI/Predict/components/PredictPositionsHeader/PredictPositionsHeader.tsx +++ b/app/components/UI/Predict/components/PredictPositionsHeader/PredictPositionsHeader.tsx @@ -35,6 +35,7 @@ import { } from '../../../../../component-library/components/Texts/Text/Text.types'; import Routes from '../../../../../constants/navigation/Routes'; import { selectPrivacyMode } from '../../../../../selectors/preferencesController'; +import { selectSelectedAccountGroupId } from '../../../../../selectors/multichainAccounts/accountTreeController'; import { usePredictBalance } from '../../hooks/usePredictBalance'; import { usePredictClaim } from '../../hooks/usePredictClaim'; import { usePredictDeposit } from '../../hooks/usePredictDeposit'; @@ -79,6 +80,8 @@ const PredictPositionsHeader = forwardRef< isLoading: isBalanceLoading, error: balanceError, } = usePredictBalance(); + // Subscribe to account group changes so the component re-renders when the user switches accounts + useSelector(selectSelectedAccountGroupId); const evmAccount = getEvmAccountFromSelectedAccountGroup(); const selectedAddress = evmAccount?.address ?? '0x0'; const { isDepositPending } = usePredictDeposit(); diff --git a/app/components/UI/Predict/hooks/usePredictActivity.test.ts b/app/components/UI/Predict/hooks/usePredictActivity.test.ts index efd8b32a9f0..c154ca76af7 100644 --- a/app/components/UI/Predict/hooks/usePredictActivity.test.ts +++ b/app/components/UI/Predict/hooks/usePredictActivity.test.ts @@ -27,6 +27,18 @@ jest.mock('./usePredictNetworkManagement', () => ({ }), })); +jest.mock( + '../../../../selectors/multichainAccounts/accountTreeController', + () => ({ + selectSelectedAccountGroupId: jest.fn(() => 'mock-account-group-id'), + }), +); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: (selector: () => unknown) => selector(), +})); + const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity } }, diff --git a/app/components/UI/Predict/hooks/usePredictActivity.ts b/app/components/UI/Predict/hooks/usePredictActivity.ts index 66819feb2b7..1cfdd083060 100644 --- a/app/components/UI/Predict/hooks/usePredictActivity.ts +++ b/app/components/UI/Predict/hooks/usePredictActivity.ts @@ -1,16 +1,19 @@ import { useEffect } from 'react'; import { useQuery, type UseQueryResult } from '@tanstack/react-query'; +import { useSelector } from 'react-redux'; import Logger from '../../../../util/Logger'; import { PREDICT_CONSTANTS } from '../constants/errors'; import type { PredictActivity } from '../types'; import { usePredictNetworkManagement } from './usePredictNetworkManagement'; import { getEvmAccountFromSelectedAccountGroup } from '../utils/accounts'; import { predictQueries } from '../queries'; +import { selectSelectedAccountGroupId } from '../../../../selectors/multichainAccounts/accountTreeController'; import { ensureError } from '../utils/predictErrorHandler'; export function usePredictActivity(): UseQueryResult { const { ensurePolygonNetworkExists } = usePredictNetworkManagement(); - + // Subscribe to account group changes so the hook re-renders when the user switches accounts + useSelector(selectSelectedAccountGroupId); const evmAccount = getEvmAccountFromSelectedAccountGroup(); const address = evmAccount?.address ?? '0x0'; diff --git a/app/components/UI/Predict/hooks/usePredictBalance.test.ts b/app/components/UI/Predict/hooks/usePredictBalance.test.ts index e74c9967b58..ae5b7599b4d 100644 --- a/app/components/UI/Predict/hooks/usePredictBalance.test.ts +++ b/app/components/UI/Predict/hooks/usePredictBalance.test.ts @@ -37,6 +37,18 @@ jest.mock('./usePredictNetworkManagement', () => ({ }), })); +jest.mock( + '../../../../selectors/multichainAccounts/accountTreeController', + () => ({ + selectSelectedAccountGroupId: jest.fn(() => 'mock-account-group-id'), + }), +); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: (selector: () => unknown) => selector(), +})); + const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, diff --git a/app/components/UI/Predict/hooks/usePredictBalance.ts b/app/components/UI/Predict/hooks/usePredictBalance.ts index 4f45284ee6c..c079fb7a95c 100644 --- a/app/components/UI/Predict/hooks/usePredictBalance.ts +++ b/app/components/UI/Predict/hooks/usePredictBalance.ts @@ -1,12 +1,15 @@ import { useEffect } from 'react'; import { useQuery, type UseQueryResult } from '@tanstack/react-query'; +import { useSelector } from 'react-redux'; import { usePredictNetworkManagement } from './usePredictNetworkManagement'; import { getEvmAccountFromSelectedAccountGroup } from '../utils/accounts'; import { predictQueries } from '../queries'; +import { selectSelectedAccountGroupId } from '../../../../selectors/multichainAccounts/accountTreeController'; export function usePredictBalance(): UseQueryResult { const { ensurePolygonNetworkExists } = usePredictNetworkManagement(); - + // Subscribe to account group changes so the hook re-renders when the user switches accounts + useSelector(selectSelectedAccountGroupId); const evmAccount = getEvmAccountFromSelectedAccountGroup(); const address = evmAccount?.address ?? '0x0'; diff --git a/app/components/UI/Predict/hooks/usePredictDeposit.test.ts b/app/components/UI/Predict/hooks/usePredictDeposit.test.ts index fd320f06d66..02fed6799f3 100644 --- a/app/components/UI/Predict/hooks/usePredictDeposit.test.ts +++ b/app/components/UI/Predict/hooks/usePredictDeposit.test.ts @@ -79,6 +79,13 @@ jest.mock('../utils/accounts', () => ({ }), })); +jest.mock( + '../../../../selectors/multichainAccounts/accountTreeController', + () => ({ + selectSelectedAccountGroupId: jest.fn(() => 'mock-account-group-id'), + }), +); + jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useSelector: (selector: () => unknown) => selector(), diff --git a/app/components/UI/Predict/hooks/usePredictDeposit.ts b/app/components/UI/Predict/hooks/usePredictDeposit.ts index 7f7883bc061..ef355386fb6 100644 --- a/app/components/UI/Predict/hooks/usePredictDeposit.ts +++ b/app/components/UI/Predict/hooks/usePredictDeposit.ts @@ -15,6 +15,7 @@ import { selectPredictPendingDepositByAddress } from '../selectors/predictContro import { ensureError } from '../utils/predictErrorHandler'; import { usePredictTrading } from './usePredictTrading'; import { getEvmAccountFromSelectedAccountGroup } from '../utils/accounts'; +import { selectSelectedAccountGroupId } from '../../../../selectors/multichainAccounts/accountTreeController'; import { PredictEventValues, PredictTradeStatus, @@ -32,6 +33,8 @@ export const usePredictDeposit = () => { const { toastRef } = useContext(ToastContext); const navigation = useNavigation(); + // Subscribe to account group changes so the hook re-renders when the user switches accounts + useSelector(selectSelectedAccountGroupId); const evmAccount = getEvmAccountFromSelectedAccountGroup(); const selectedInternalAccountAddress = evmAccount?.address ?? '0x0'; diff --git a/app/components/UI/Predict/hooks/usePredictPositions.test.ts b/app/components/UI/Predict/hooks/usePredictPositions.test.ts index 1acedd49c24..0b334885b30 100644 --- a/app/components/UI/Predict/hooks/usePredictPositions.test.ts +++ b/app/components/UI/Predict/hooks/usePredictPositions.test.ts @@ -22,6 +22,18 @@ jest.mock('../utils/accounts', () => ({ mockGetEvmAccountFromSelectedAccountGroup(), })); +jest.mock( + '../../../../selectors/multichainAccounts/accountTreeController', + () => ({ + selectSelectedAccountGroupId: jest.fn(() => 'mock-account-group-id'), + }), +); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: (selector: () => unknown) => selector(), +})); + const mockGetPositions = jest.fn< Promise, [{ address: string }] diff --git a/app/components/UI/Predict/hooks/usePredictPositions.ts b/app/components/UI/Predict/hooks/usePredictPositions.ts index 8f2c7bc235a..9f7b4b0bd4e 100644 --- a/app/components/UI/Predict/hooks/usePredictPositions.ts +++ b/app/components/UI/Predict/hooks/usePredictPositions.ts @@ -1,9 +1,11 @@ import { useEffect } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useSelector } from 'react-redux'; import type { PredictPosition } from '../types'; import { usePredictNetworkManagement } from './usePredictNetworkManagement'; import { getEvmAccountFromSelectedAccountGroup } from '../utils/accounts'; import { predictQueries } from '../queries'; +import { selectSelectedAccountGroupId } from '../../../../selectors/multichainAccounts/accountTreeController'; const OPTIMISTIC_POLL_INTERVAL = 2_000; @@ -36,6 +38,8 @@ export function usePredictPositions(options: UsePredictPositionsOptions = {}) { const { enabled = true, refetchInterval, claimable, marketId } = options; const { ensurePolygonNetworkExists } = usePredictNetworkManagement(); + // Subscribe to account group changes so the hook re-renders when the user switches accounts + useSelector(selectSelectedAccountGroupId); const evmAccount = getEvmAccountFromSelectedAccountGroup(); const address = evmAccount?.address ?? '0x0'; const queryClient = useQueryClient(); diff --git a/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx b/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx index b0b980f665b..2c3401d2218 100644 --- a/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx +++ b/app/components/UI/Predict/hooks/usePredictToastRegistrations.test.tsx @@ -69,6 +69,18 @@ jest.mock('../utils/accounts', () => ({ })), })); +jest.mock( + '../../../../selectors/multichainAccounts/accountTreeController', + () => ({ + selectSelectedAccountGroupId: jest.fn(() => 'mock-account-group-id'), + }), +); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: (selector: () => unknown) => selector(), +})); + jest.mock('../../../../store', () => ({ store: { getState: jest.fn(() => ({})), diff --git a/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx b/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx index af1e687ffbe..cc82bb50ee0 100644 --- a/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx +++ b/app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx @@ -7,6 +7,7 @@ import { Spinner } from '@metamask/design-system-react-native/dist/components/te import { useNavigation } from '@react-navigation/native'; import { useQueryClient } from '@tanstack/react-query'; import React, { useCallback, useMemo } from 'react'; +import { useSelector } from 'react-redux'; import { strings } from '../../../../../locales/i18n'; import { IconName } from '../../../../component-library/components/Icons/Icon'; import { ToastVariants } from '../../../../component-library/components/Toast'; @@ -19,6 +20,7 @@ import type { PredictTransactionStatusChangedPayload } from '../controllers/Pred import { getEvmAccountFromSelectedAccountGroup } from '../utils/accounts'; import { formatPrice } from '../utils/format'; import { predictQueries } from '../queries'; +import { selectSelectedAccountGroupId } from '../../../../selectors/multichainAccounts/accountTreeController'; import type { Hex } from '@metamask/utils'; import { usePredictClaim } from './usePredictClaim'; import { usePredictDeposit } from './usePredictDeposit'; @@ -139,6 +141,8 @@ export const usePredictToastRegistrations = (): ToastRegistration[] => { const navigation = useNavigation(); const theme = useAppThemeFromContext(); + // Subscribe to account group changes so the hook re-renders when the user switches accounts + useSelector(selectSelectedAccountGroupId); const selectedAddress = getEvmAccountFromSelectedAccountGroup()?.address ?? '0x0'; const normalizedSelectedAddress = selectedAddress.toLowerCase(); diff --git a/app/components/UI/Predict/hooks/useUnrealizedPnL.tsx b/app/components/UI/Predict/hooks/useUnrealizedPnL.tsx index 7f0eb965b7f..ebd8639288e 100644 --- a/app/components/UI/Predict/hooks/useUnrealizedPnL.tsx +++ b/app/components/UI/Predict/hooks/useUnrealizedPnL.tsx @@ -1,4 +1,5 @@ import { useFocusEffect } from '@react-navigation/native'; +import { useSelector } from 'react-redux'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DevLogger } from '../../../../core/SDKConnect/utils/DevLogger'; import Logger from '../../../../util/Logger'; @@ -7,6 +8,7 @@ import { UnrealizedPnL } from '../types'; import { getEvmAccountFromSelectedAccountGroup } from '../utils/accounts'; import { PREDICT_CONSTANTS } from '../constants/errors'; import { ensureError } from '../utils/predictErrorHandler'; +import { selectSelectedAccountGroupId } from '../../../../selectors/multichainAccounts/accountTreeController'; import { usePredictPositions } from './usePredictPositions'; export interface UseUnrealizedPnLOptions { @@ -50,6 +52,8 @@ export const useUnrealizedPnL = ( const [error, setError] = useState(null); const isInitialMount = useRef(true); + // Subscribe to account group changes so the hook re-renders when the user switches accounts + useSelector(selectSelectedAccountGroupId); const evmAccount = getEvmAccountFromSelectedAccountGroup(); const selectedInternalAccountAddress = evmAccount?.address ?? '0x0'; From 80e69099b8c8e3062d65bd5b43d317f9de62f48f Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 4 Mar 2026 01:12:02 +0000 Subject: [PATCH 084/131] [skip ci] Bump version number to 3885 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d9266d00ab9..7a6d2fc2384 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.68.0" - versionCode 3877 + versionCode 3885 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 5dff75570ba..52cc97f4415 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.68.0 - opts: is_expand: false - VERSION_NUMBER: 3877 + VERSION_NUMBER: 3885 - opts: is_expand: false FLASK_VERSION_NAME: 7.68.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3877 + FLASK_VERSION_NUMBER: 3885 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 9fee55a81cc..02862ff3622 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3877; + CURRENT_PROJECT_VERSION = 3885; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3877; + CURRENT_PROJECT_VERSION = 3885; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3877; + CURRENT_PROJECT_VERSION = 3885; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3877; + CURRENT_PROJECT_VERSION = 3885; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3877; + CURRENT_PROJECT_VERSION = 3885; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3877; + CURRENT_PROJECT_VERSION = 3885; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From adb545e424a9d13fafd6e82277ed5b9d656f6c70 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:09:37 +0000 Subject: [PATCH 085/131] chore(runway): cherry-pick fix(perps): remove duplicate AppState listener causing reconnection race (#26998) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(perps): remove duplicate AppState listener causing reconnection race cp-7.67.2 (#26982) ## **Description** After long backgrounds (5+ min), Perps failed to reload positions, 24h price change was missing, and closing positions threw "BTC is not tradeable asset". After closing a position and backgrounding for >20s, the closed position would reappear on foreground. **Root causes:** 1. **Duplicate AppState listener race** — `usePerpsConnectionLifecycle` hook and `PerpsConnectionManager.setupStateMonitoring()` both fired on foreground. The `force` reconnect path cancelled the hook's in-flight `connect()` and ran a competing `performReconnection()`, cleaning up prewarm subscriptions mid-flight and leaving positions/prices without data. 2. **Stale cache after grace-period disconnect** — `performActualDisconnection()` (fires after 20s grace period) did not clear stream channel caches or reset subscriber state (`hasReceivedFirstUpdate`). On next foreground, the old closed position was served from cache until the throttle window passed. 3. **`isPreloading` not reset on disconnect** — If a preload was in-flight when the grace period fired, `isPreloading` stayed `true`. The next `connect()` would silently skip all subscription prewarm, leaving positions and prices with no data. **Fixes:** - Remove the manager-level AppState listener (hook already handles foreground recovery) - Fix `isInternetReachable` null handling in NetInfo listener (`null` treated as offline was blocking network-restore reconnects) - Add `isPreloading` concurrency guard to `preloadSubscriptions()` to prevent concurrent calls racing on reconnect - Call `clearCache()` on all stream channels in `performActualDisconnection()` — wipes stale positions, resets `hasReceivedFirstUpdate`, puts UI into loading state on next foreground - Reset `isPreloading = false` in `performActualDisconnection()` alongside `hasPreloaded` The NetInfo listener from #26780 is kept — it handles the distinct offline→online restore scenario. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: TAT-2598 ## **Manual testing steps** ```gherkin Feature: Perps reconnection after long background Scenario: user returns to Perps after long background Given the user has open Perps positions And the app has been backgrounded for 5+ minutes When user foregrounds the app Then positions load correctly And 24h price change is displayed And closing a position succeeds without "not tradeable asset" error Scenario: closed position does not reappear after background Given the user closes a Perps position and sees the success toast And the app has been backgrounded for more than 20 seconds When user foregrounds the app Then the closed position is NOT shown in the positions list And no stale "already closed" error appears on interaction Scenario: user returns after network loss Given the user has open Perps positions And airplane mode was enabled then disabled while app was backgrounded When user foregrounds the app Then the connection is restored And positions load correctly ``` ## **Screenshots/Recordings** ### **Before** Positions blank, 24h change missing, close position fails with "BTC is not tradeable asset". Closed positions reappear after >20s background. ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Touches Perps connection lifecycle (reconnect/disconnect, NetInfo handling, preload guards) and could impact data freshness and reconnection reliability if regressions occur, though changes are localized and well-covered by tests. > > **Overview** > Prevents a foreground reconnection race by **removing the manager-level `AppState` listener** so only the lifecycle hook initiates foreground recovery. > > Hardens reconnect/disconnect behavior by **clearing all Perps stream channel caches on grace-period expiry**, resetting subscriber `hasReceivedFirstUpdate`, and resetting the in-flight preload state (`isPreloading`) so subsequent `connect()` calls reliably prewarm subscriptions. > > Improves offline→online recovery by treating `NetInfo.isInternetReachable: null` as “unknown” and falling back to `isConnected`, and expands `PerpsConnectionManager` tests to cover these concurrency, cache-clearing, and network edge cases. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 72739b2b36696d7b562e08e47cfd2e6d37a9966e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [7c541c0](https://github.com/MetaMask/metamask-mobile/commit/7c541c06993ee89599816341502a6bdf1b35075d) --------- Co-authored-by: abretonc7s <107169956+abretonc7s@users.noreply.github.com> Co-authored-by: Alejandro Garcia --- .../UI/Perps/providers/PerpsStreamManager.tsx | 1 + .../services/PerpsConnectionManager.test.ts | 291 ++++++++++++++++-- .../Perps/services/PerpsConnectionManager.ts | 85 ++--- 3 files changed, 294 insertions(+), 83 deletions(-) diff --git a/app/components/UI/Perps/providers/PerpsStreamManager.tsx b/app/components/UI/Perps/providers/PerpsStreamManager.tsx index 1ea67125562..df53f331d99 100644 --- a/app/components/UI/Perps/providers/PerpsStreamManager.tsx +++ b/app/components/UI/Perps/providers/PerpsStreamManager.tsx @@ -317,6 +317,7 @@ abstract class StreamChannel { subscriber.timer = undefined; } subscriber.pendingUpdate = undefined; + subscriber.hasReceivedFirstUpdate = false; }); // Disconnect the old WebSocket subscription to stop receiving old account data diff --git a/app/components/UI/Perps/services/PerpsConnectionManager.test.ts b/app/components/UI/Perps/services/PerpsConnectionManager.test.ts index 08c9ca0ae74..66c5b080726 100644 --- a/app/components/UI/Perps/services/PerpsConnectionManager.test.ts +++ b/app/components/UI/Perps/services/PerpsConnectionManager.test.ts @@ -94,6 +94,7 @@ jest.mock('react-native-background-timer', () => ({ })); // Import non-singleton modules first +import { addEventListener as mockNetInfoAddEventListener } from '@react-native-community/netinfo'; import { DevLogger } from '../../../../core/SDKConnect/utils/DevLogger'; import Engine from '../../../../core/Engine'; import { store } from '../../../../store'; @@ -128,13 +129,20 @@ const resetManager = (manager: unknown) => { isInGracePeriod: boolean; gracePeriodTimer: number | null; hasPreloaded: boolean; + isPreloading: boolean; prewarmCleanups: (() => void)[]; - lastBalanceUpdateTime: number; + netInfoUnsubscribe: (() => void) | null; + wasOffline: boolean; }; // Call unsubscribe if it exists before resetting if (m.unsubscribeFromStore) { m.unsubscribeFromStore(); } + if (m.netInfoUnsubscribe) { + m.netInfoUnsubscribe(); + m.netInfoUnsubscribe = null; + } + m.wasOffline = false; // Clean up any prewarm subscriptions m.prewarmCleanups.forEach((cleanup) => cleanup()); m.prewarmCleanups = []; @@ -155,7 +163,7 @@ const resetManager = (manager: unknown) => { m.isInGracePeriod = false; m.gracePeriodTimer = null; m.hasPreloaded = false; - m.lastBalanceUpdateTime = 0; + m.isPreloading = false; }; describe('PerpsConnectionManager', () => { @@ -209,7 +217,7 @@ describe('PerpsConnectionManager', () => { }); describe('getInstance', () => { - it('should return the same instance when called multiple times', () => { + it('returns the same singleton instance on repeated calls', () => { const instance1 = PerpsConnectionManager; const instance2 = PerpsConnectionManager; @@ -218,7 +226,7 @@ describe('PerpsConnectionManager', () => { }); describe('connect', () => { - it('should initialize providers and connect successfully', async () => { + it('initializes providers and connects on first call', async () => { mockPerpsController.init.mockResolvedValueOnce(); await PerpsConnectionManager.connect(); @@ -229,7 +237,7 @@ describe('PerpsConnectionManager', () => { ); }); - it('should increment reference count on each connect call', async () => { + it('increments reference count on each connect call', async () => { mockPerpsController.init.mockResolvedValue(); await PerpsConnectionManager.connect(); @@ -243,7 +251,7 @@ describe('PerpsConnectionManager', () => { ); }); - it('should return existing promise if already connecting', async () => { + it('returns existing promise when connection is already in progress', async () => { // This test verifies that concurrent connect calls share the same promise // Track promises from both connect calls @@ -274,7 +282,7 @@ describe('PerpsConnectionManager', () => { ); }); - it('should handle connection failures', async () => { + it('sets error state and resets flags when connection fails', async () => { const error = new Error('Connection failed'); mockPerpsController.init.mockRejectedValueOnce(error); @@ -294,7 +302,7 @@ describe('PerpsConnectionManager', () => { expect(state.error).toBe('Connection failed'); }); - it('should skip reconnection if already connected', async () => { + it('skips reconnection when already connected', async () => { // First successful connection mockPerpsController.init.mockResolvedValueOnce(); await PerpsConnectionManager.connect(); @@ -308,7 +316,7 @@ describe('PerpsConnectionManager', () => { expect(mockPerpsController.init).toHaveBeenCalledTimes(1); }); - it('should handle rapid disconnect-connect cycles with grace period', async () => { + it('cancels grace period timer when reconnecting during grace period', async () => { // Setup initial connection mockPerpsController.init.mockResolvedValue(); mockPerpsController.getAccountState.mockResolvedValue({}); @@ -345,7 +353,7 @@ describe('PerpsConnectionManager', () => { mockPerpsController.disconnect.mockResolvedValue(); }); - it('should decrement reference count on disconnect', async () => { + it('decrements reference count on disconnect', async () => { await PerpsConnectionManager.connect(); await PerpsConnectionManager.connect(); @@ -359,7 +367,7 @@ describe('PerpsConnectionManager', () => { expect(mockPerpsController.disconnect).not.toHaveBeenCalled(); }); - it('should only start grace period when reference count reaches zero', async () => { + it('starts grace period only when reference count reaches zero', async () => { await PerpsConnectionManager.connect(); await PerpsConnectionManager.connect(); @@ -374,7 +382,7 @@ describe('PerpsConnectionManager', () => { ); }); - it('should handle disconnect gracefully with grace period', async () => { + it('starts grace period timer on disconnect instead of disconnecting immediately', async () => { await PerpsConnectionManager.connect(); // Disconnect should not throw even if controller would fail later @@ -386,7 +394,7 @@ describe('PerpsConnectionManager', () => { expect(mockPerpsController.disconnect).not.toHaveBeenCalled(); }); - it('should prevent negative reference count', async () => { + it('prevents reference count from going below zero', async () => { await PerpsConnectionManager.disconnect(); await PerpsConnectionManager.disconnect(); @@ -401,7 +409,7 @@ describe('PerpsConnectionManager', () => { expect(state.isConnected).toBe(false); }); - it('should maintain connection state during grace period', async () => { + it('maintains connected state during grace period', async () => { await PerpsConnectionManager.connect(); const connectedState = PerpsConnectionManager.getConnectionState(); @@ -418,7 +426,7 @@ describe('PerpsConnectionManager', () => { expect(gracePeriodState.isInGracePeriod).toBe(true); }); - it('should maintain state monitoring during grace period', async () => { + it('maintains state monitoring during grace period', async () => { // Connect to set up monitoring await PerpsConnectionManager.connect(); @@ -444,7 +452,7 @@ describe('PerpsConnectionManager', () => { }); describe('getConnectionState', () => { - it('should return initial state', () => { + it('returns initial disconnected state', () => { const state = PerpsConnectionManager.getConnectionState(); expect(state).toEqual({ @@ -457,7 +465,7 @@ describe('PerpsConnectionManager', () => { }); }); - it('should return connecting state during connection', async () => { + it('returns connecting state while connection is in progress', async () => { mockPerpsController.init.mockImplementation( () => new Promise((resolve) => setTimeout(resolve, 100)), ); @@ -556,7 +564,7 @@ describe('PerpsConnectionManager', () => { }); describe('concurrent operations', () => { - it('should handle multiple concurrent connect/disconnect operations', async () => { + it('serializes concurrent connect and disconnect operations', async () => { mockPerpsController.init.mockResolvedValue(); mockPerpsController.getAccountState.mockResolvedValue({}); mockPerpsController.disconnect.mockResolvedValue(); @@ -595,7 +603,7 @@ describe('PerpsConnectionManager', () => { (store.getState as jest.Mock).mockReturnValue({}); }); - it('should set up Redux store subscription on first connect', async () => { + it('sets up Redux store subscription on first connect', async () => { // Connect to trigger monitoring setup mockPerpsController.init.mockResolvedValue(); mockPerpsController.getAccountState.mockResolvedValue({}); @@ -610,7 +618,7 @@ describe('PerpsConnectionManager', () => { expect(typeof storeCallbacks[storeCallbacks.length - 1]).toBe('function'); }); - it('should detect account changes and trigger reconnection', async () => { + it('detects account changes and triggers reconnection', async () => { // Setup connected state mockPerpsController.init.mockResolvedValue(); mockPerpsController.getAccountState.mockResolvedValue({}); @@ -645,7 +653,7 @@ describe('PerpsConnectionManager', () => { ); }); - it('should detect network changes and trigger reconnection', async () => { + it('detects network changes and triggers reconnection', async () => { // Setup connected state mockPerpsController.init.mockResolvedValue(); mockPerpsController.getAccountState.mockResolvedValue({}); @@ -680,7 +688,7 @@ describe('PerpsConnectionManager', () => { ); }); - it('should continue monitoring during grace period', async () => { + it('continues monitoring state changes during grace period', async () => { // Setup but don't connect mockPerpsController.init.mockResolvedValue(); mockPerpsController.getAccountState.mockResolvedValue({}); @@ -718,7 +726,7 @@ describe('PerpsConnectionManager', () => { mockPerpsController.reconnectWithNewContext.mockResolvedValue(); }); - it('should clear StreamManager caches', async () => { + it('clears all StreamManager caches on reconnection', async () => { // Setup connected state first mockPerpsController.init.mockResolvedValue(); await PerpsConnectionManager.connect(); @@ -739,7 +747,7 @@ describe('PerpsConnectionManager', () => { expect(mockStreamManagerInstance.prices.clearCache).toHaveBeenCalled(); }); - it('should reinitialize controller with new context', async () => { + it('reinitializes controller with new account and network context', async () => { mockPerpsController.init.mockResolvedValue(); await ( @@ -753,7 +761,7 @@ describe('PerpsConnectionManager', () => { expect(mockPerpsController.init).toHaveBeenCalled(); }); - it('should handle reconnection errors gracefully', async () => { + it('logs error and resets connecting flag when reconnection fails', async () => { const error = new Error('Reconnection failed'); mockPerpsController.init.mockRejectedValueOnce(error); @@ -840,6 +848,84 @@ describe('PerpsConnectionManager', () => { }); }); + describe('preloadSubscriptions concurrency guard', () => { + it('skips concurrent preload when one is already in flight', async () => { + // Arrange + const m = PerpsConnectionManager as unknown as { + isPreloading: boolean; + hasPreloaded: boolean; + preloadSubscriptions: () => Promise; + }; + m.isPreloading = true; + + // Act + await m.preloadSubscriptions(); + + // Assert + expect(mockStreamManagerInstance.prices.prewarm).not.toHaveBeenCalled(); + }); + + it('allows fresh preload after performReconnection resets isPreloading', async () => { + // Arrange + mockPerpsController.init.mockResolvedValue(); + mockPerpsController.disconnect.mockResolvedValue(); + await PerpsConnectionManager.connect(); + expect(mockStreamManagerInstance.prices.prewarm).toHaveBeenCalledTimes(1); + + // Act + await ( + PerpsConnectionManager as unknown as { + reconnectWithNewContext: () => Promise; + } + ).reconnectWithNewContext(); + + // Assert + expect(mockStreamManagerInstance.prices.prewarm).toHaveBeenCalledTimes(2); + }); + }); + + describe('foreground reconnection — single reconnection flow', () => { + it('PerpsConnectionManager has no AppState listener — only the hook triggers foreground reconnect', () => { + // This test documents the fix for the race condition introduced by PR #26780. + // Previously, PerpsConnectionManager registered its own AppState listener in + // setupStateMonitoring(), which competed with usePerpsConnectionLifecycle hook. + // + // Both fired simultaneously on foreground: + // hook → connect() + // manager → reconnectWithNewContext({ force: true }) + // + // The force path cancelled the hook's in-flight connect() and cleaned up + // prewarm subscriptions mid-flight, leaving positions/prices without data. + // + // Fix: removed the manager-level AppState listener entirely. + // The hook is the sole owner of foreground recovery. + + const m = PerpsConnectionManager as unknown as Record; + + // appStateSubscription field must not exist on the manager + expect(m.appStateSubscription).toBeUndefined(); + + // handleAppStateChange method must not exist on the manager + expect(typeof m.handleAppStateChange).not.toBe('function'); + }); + + it('connect() on foreground completes without interference', async () => { + mockPerpsController.init.mockResolvedValue(); + + await PerpsConnectionManager.connect(); + + const state = PerpsConnectionManager.getConnectionState(); + expect(state.isConnected).toBe(true); + expect(state.isConnecting).toBe(false); + expect(state.error).toBeNull(); + // Prewarm subscriptions fired exactly once + expect(mockStreamManagerInstance.prices.prewarm).toHaveBeenCalledTimes(1); + expect(mockStreamManagerInstance.positions.prewarm).toHaveBeenCalledTimes( + 1, + ); + }); + }); + describe('waitForConnection', () => { it('awaits resolving initPromise', async () => { // Arrange — set initPromise to a resolved promise @@ -905,4 +991,159 @@ describe('PerpsConnectionManager', () => { m.pendingReconnectPromise = null; }); }); + + describe('performActualDisconnection — grace period expiry', () => { + beforeEach(async () => { + mockPerpsController.init.mockResolvedValue(); + mockPerpsController.disconnect.mockResolvedValue(); + await PerpsConnectionManager.connect(); + // Clear cache mock calls from connect/prewarm so we can assert specifically + Object.values(mockStreamManagerInstance).forEach(({ clearCache }) => + clearCache.mockClear(), + ); + }); + + it('clears all stream channel caches when grace period fires', async () => { + // Arrange + const m = PerpsConnectionManager as unknown as { + isConnected: boolean; + isInitialized: boolean; + connectionRefCount: number; + isPreloading: boolean; + performActualDisconnection: () => Promise; + }; + m.connectionRefCount = 0; + + // Act + await m.performActualDisconnection(); + + // Assert + expect(mockStreamManagerInstance.positions.clearCache).toHaveBeenCalled(); + expect(mockStreamManagerInstance.orders.clearCache).toHaveBeenCalled(); + expect(mockStreamManagerInstance.account.clearCache).toHaveBeenCalled(); + expect(mockStreamManagerInstance.prices.clearCache).toHaveBeenCalled(); + expect( + mockStreamManagerInstance.marketData.clearCache, + ).toHaveBeenCalled(); + expect(mockStreamManagerInstance.oiCaps.clearCache).toHaveBeenCalled(); + expect(mockStreamManagerInstance.fills.clearCache).toHaveBeenCalled(); + expect(mockStreamManagerInstance.topOfBook.clearCache).toHaveBeenCalled(); + expect(mockStreamManagerInstance.candles.clearCache).toHaveBeenCalled(); + }); + + it('resets isPreloading flag so next connect can prewarm', async () => { + // Arrange + const m = PerpsConnectionManager as unknown as { + connectionRefCount: number; + isPreloading: boolean; + performActualDisconnection: () => Promise; + }; + m.connectionRefCount = 0; + m.isPreloading = true; + + // Act + await m.performActualDisconnection(); + + // Assert + expect(m.isPreloading).toBe(false); + }); + + it('resets connection state flags after disconnecting', async () => { + // Arrange + const m = PerpsConnectionManager as unknown as { + connectionRefCount: number; + performActualDisconnection: () => Promise; + }; + m.connectionRefCount = 0; + + // Act + await m.performActualDisconnection(); + + // Assert + const state = PerpsConnectionManager.getConnectionState(); + expect(state.isConnected).toBe(false); + expect(state.isInitialized).toBe(false); + expect(state.isConnecting).toBe(false); + }); + + it('skips disconnection when reference count is still positive', async () => { + // Arrange — refCount > 0 means another consumer reconnected during grace period + const m = PerpsConnectionManager as unknown as { + connectionRefCount: number; + performActualDisconnection: () => Promise; + }; + m.connectionRefCount = 1; + + // Act + await m.performActualDisconnection(); + + // Assert — controller not called, caches not cleared + expect(mockPerpsController.disconnect).not.toHaveBeenCalled(); + expect( + mockStreamManagerInstance.positions.clearCache, + ).not.toHaveBeenCalled(); + }); + }); + + describe('NetInfo isInternetReachable null handling', () => { + type NetInfoCallback = (state: { + isInternetReachable: boolean | null; + isConnected: boolean | null; + }) => void; + + const mockAddEventListener = mockNetInfoAddEventListener as jest.Mock; + + let capturedCallback: NetInfoCallback | null = null; + + beforeEach(async () => { + // Set up to capture the listener, then connect so it registers + mockAddEventListener.mockImplementation((cb: NetInfoCallback) => { + capturedCallback = cb; + return jest.fn(); + }); + mockPerpsController.init.mockResolvedValue(); + await PerpsConnectionManager.connect(); + }); + + it('treats isInternetReachable null as online when isConnected is true', () => { + // Arrange — start disconnected so wasOffline is not set by online path + const m = PerpsConnectionManager as unknown as { + wasOffline: boolean; + isConnected: boolean; + }; + m.isConnected = false; // not connected — online path won't clear wasOffline + m.wasOffline = false; + + // Act — null isInternetReachable falls back to isConnected=true → isOnline=true + // Since isOnline=true and !wasOffline, the "set wasOffline=true" branch is skipped + capturedCallback?.({ isInternetReachable: null, isConnected: true }); + + // Assert — wasOffline stays false (not offline path, and not connected to clear it) + expect(m.wasOffline).toBe(false); + }); + + it('treats isInternetReachable null as offline when isConnected is false', () => { + // Arrange + const m = PerpsConnectionManager as unknown as { wasOffline: boolean }; + m.wasOffline = false; + + // Act — null isInternetReachable falls back to isConnected=false → offline + capturedCallback?.({ isInternetReachable: null, isConnected: false }); + + // Assert + expect(m.wasOffline).toBe(true); + }); + + it('treats isInternetReachable false as offline', () => { + // Arrange + const m = PerpsConnectionManager as unknown as { wasOffline: boolean }; + m.wasOffline = false; + + // Act + capturedCallback?.({ isInternetReachable: false, isConnected: true }); + + // Assert + expect(m.wasOffline).toBe(true); + }); + }); }); diff --git a/app/components/UI/Perps/services/PerpsConnectionManager.ts b/app/components/UI/Perps/services/PerpsConnectionManager.ts index 4632db78eb2..cf01bbce026 100644 --- a/app/components/UI/Perps/services/PerpsConnectionManager.ts +++ b/app/components/UI/Perps/services/PerpsConnectionManager.ts @@ -1,4 +1,3 @@ -import { AppState, type AppStateStatus } from 'react-native'; import { addEventListener as netInfoAddEventListener, type NetInfoState, @@ -52,6 +51,7 @@ class PerpsConnectionManagerClass { private initPromise: Promise | null = null; private disconnectPromise: Promise | null = null; private hasPreloaded = false; + private isPreloading = false; private prewarmCleanups: (() => void)[] = []; private unsubscribeFromStore: (() => void) | null = null; private previousAddress: string | undefined; @@ -62,9 +62,6 @@ class PerpsConnectionManagerClass { private isInGracePeriod = false; private pendingReconnectPromise: Promise | null = null; private connectionTimeoutRef: ReturnType | null = null; - private appStateSubscription: ReturnType< - typeof AppState.addEventListener - > | null = null; private netInfoUnsubscribe: (() => void) | null = null; private wasOffline = false; private networkRestoreRetryTimer: ReturnType | null = null; @@ -190,32 +187,12 @@ class PerpsConnectionManagerClass { this.previousHip3Version = currentHip3Version; }); - // Listen for app state changes to reconnect after background - if (!this.appStateSubscription) { - this.appStateSubscription = AppState.addEventListener( - 'change', - (nextAppState: AppStateStatus) => { - this.handleAppStateChange(nextAppState).catch((error) => { - Logger.error( - ensureError(error, 'PerpsConnectionManager.appStateListener'), - { - tags: { feature: PERPS_CONSTANTS.FeatureName }, - context: { - name: 'PerpsConnectionManager.appStateListener', - data: { message: 'Error handling app state change' }, - }, - }, - ); - }); - }, - ); - } - // Listen for connectivity changes (WiFi off/on, airplane mode, etc.) if (!this.netInfoUnsubscribe) { this.netInfoUnsubscribe = netInfoAddEventListener( (netState: NetInfoState) => { - const isOnline = netState.isInternetReachable === true; + const isOnline = + netState.isInternetReachable ?? netState.isConnected ?? false; if (isOnline && this.wasOffline) { DevLogger.log( @@ -238,8 +215,7 @@ class PerpsConnectionManagerClass { /** * Validate the WebSocket connection and force-reconnect if it is stale. - * Shared by both AppState (background→foreground) and NetInfo (offline→online) - * recovery paths. + * Used by the NetInfo (offline→online) recovery path. * * @param context - Caller identifier for error reporting * @param skipPing - Skip ping and force reconnect after known network loss @@ -335,37 +311,10 @@ class PerpsConnectionManagerClass { this.networkRestoreRetryCount = 0; } - /** - * Handle app state transitions to recover from stale WebSocket connections. - * When the app returns from background, the OS may have silently killed the - * WebSocket. A health-check ping detects this and triggers reconnection. - */ - private async handleAppStateChange( - nextAppState: AppStateStatus, - ): Promise { - if (nextAppState !== 'active') { - return; - } - - // Cancel any pending grace period — user is back - if (this.isInGracePeriod) { - DevLogger.log( - 'PerpsConnectionManager: App resumed - cancelling grace period', - ); - this.cancelGracePeriod(); - } - - await this.validateAndReconnect('appResume'); - } - /** * Clean up state monitoring */ private cleanupStateMonitoring(): void { - if (this.appStateSubscription) { - this.appStateSubscription.remove(); - this.appStateSubscription = null; - } if (this.netInfoUnsubscribe) { this.netInfoUnsubscribe(); this.netInfoUnsubscribe = null; @@ -519,11 +468,25 @@ class PerpsConnectionManagerClass { // Clean up preloaded subscriptions this.cleanupPreloadedSubscriptions(); + // Clear all stream caches so subscribers reset to loading state + // and hasReceivedFirstUpdate is reset for clean reconnection + const streamManager = getStreamManagerInstance(); + streamManager.positions.clearCache(); + streamManager.orders.clearCache(); + streamManager.account.clearCache(); + streamManager.prices.clearCache(); + streamManager.marketData.clearCache(); + streamManager.oiCaps.clearCache(); + streamManager.fills.clearCache(); + streamManager.topOfBook.clearCache(); + streamManager.candles.clearCache(); + // Reset state before disconnecting to prevent race conditions this.isConnected = false; this.isInitialized = false; this.isConnecting = false; this.hasPreloaded = false; // Reset pre-load flag on disconnect + this.isPreloading = false; // Reset in-flight preload guard on disconnect this.clearError(); // Clear any errors on disconnect await Engine.context.PerpsController.disconnect(); @@ -930,6 +893,7 @@ class PerpsConnectionManagerClass { this.isConnected = false; this.isInitialized = false; this.hasPreloaded = false; + this.isPreloading = false; // Clear previous errors when starting reconnection attempt this.clearError(); @@ -1097,12 +1061,15 @@ class PerpsConnectionManagerClass { * Also sets up balance update subscriptions for portfolio integration */ private async preloadSubscriptions(): Promise { - // Only pre-load once per session - if (this.hasPreloaded) { - DevLogger.log('PerpsConnectionManager: Already pre-loaded, skipping'); + // Only pre-load once per session, and guard against concurrent calls + if (this.hasPreloaded || this.isPreloading) { + DevLogger.log( + 'PerpsConnectionManager: Already pre-loaded or preloading, skipping', + ); return; } + this.isPreloading = true; try { DevLogger.log( 'PerpsConnectionManager: Pre-loading WebSocket subscriptions via StreamManager', @@ -1156,6 +1123,8 @@ class PerpsConnectionManagerClass { }, ); // Non-critical error - components will still work with on-demand subscriptions + } finally { + this.isPreloading = false; } } From 66ff68986eeab69b16ff0934d9ae7d0593c074e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:31:01 +0000 Subject: [PATCH 086/131] OTA update 7.67.2 --- app/constants/ota.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/constants/ota.ts b/app/constants/ota.ts index e43c9c02236..842c7c14ef2 100644 --- a/app/constants/ota.ts +++ b/app/constants/ota.ts @@ -6,7 +6,7 @@ import otaConfig from '../../ota.config.js'; * Reset to v0 when releasing a new native build * We keep this OTA_VERSION here to because changes in ota.config.js will affect the fingerprint and break the workflow in Github Actions */ -export const OTA_VERSION: string = 'v7.67.1'; +export const OTA_VERSION: string = 'v7.67.2'; export const RUNTIME_VERSION = otaConfig.RUNTIME_VERSION; export const PROJECT_ID = otaConfig.PROJECT_ID; export const UPDATE_URL = otaConfig.UPDATE_URL; From 9214d78b6de4ad3efa0999c8743f36cccfef7b79 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 5 Mar 2026 07:45:09 +0000 Subject: [PATCH 087/131] bump semvar version to 7.67.3 && build version to 3896 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 532cbb43739..179f5db160f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -187,7 +187,7 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionName "7.67.0" + versionName "7.67.3" versionCode 3844 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bitrise.yml b/bitrise.yml index f0f5aa5dccf..66b2b00785b 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3516,13 +3516,13 @@ app: PROJECT_LOCATION_IOS: ios - opts: is_expand: false - VERSION_NAME: 7.67.0 + VERSION_NAME: 7.67.3 - opts: is_expand: false VERSION_NUMBER: 3844 - opts: is_expand: false - FLASK_VERSION_NAME: 7.67.0 + FLASK_VERSION_NAME: 7.67.3 - opts: is_expand: false FLASK_VERSION_NUMBER: 3844 diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 71640427dcf..5ed0927466d 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1319,7 +1319,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.3; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1385,7 +1385,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.3; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1454,7 +1454,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.3; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1518,7 +1518,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.3; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1684,7 +1684,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.3; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -1751,7 +1751,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.67.0; + MARKETING_VERSION = 7.67.3; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/package.json b/package.json index 90abdc8bcc8..dee636d1ffb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "7.67.0", + "version": "7.67.3", "private": true, "scripts": { "install:foundryup": "yarn mm-foundryup", From 0243c56b1bdd7369ad7b84324a7ac12eda9c71d0 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:04:04 +0100 Subject: [PATCH 088/131] chore(runway): cherry-pick 02ac109 (#27041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: Ensure `redux-persist-filesystem-storage` returns a promise and throws correctly cp-7.67.2 (#26979) ## **Description** This PR adjusts our existing patch for `redux-persist-filesystem-storage` to always return a promise for `setItem` so it can be awaited correctly, fixing potential race conditions. This was broken when the patch originally was created by adding brackets to an arrow function and not returning. The PR also fixes a bug where not passing in a callback would cause errors to be swallowed. Additionally, this PR moves our existing patch to use the built-in Yarn patch feature (because creating new patches with `patch-package` seems broken). ## **Changelog** CHANGELOG entry: Improve reliability of persistence ## **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. ## **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** > Touches persistence write behavior by changing the patched `redux-persist-filesystem-storage` `setItem` control flow and error propagation, which could affect app startup/migrations if any callers depend on the old callback-only semantics. Also introduces an iOS-only side effect (exclude-from-backup) after writes. > > **Overview** > This PR updates the Yarn patch for `redux-persist-filesystem-storage` so `setItem` **always returns a Promise** that can be awaited, rather than potentially relying on callback chaining. > > It also fixes error handling so write failures are **thrown when no callback is provided** (instead of being swallowed), and adds an optional `isIOS` flag to run `ReactNativeBlobUtil.ios.excludeFromBackupKey(...)` after successful writes on iOS. > > Dependency wiring is switched from a normal semver dependency to Yarn’s built-in `patch:` reference for `redux-persist-filesystem-storage@4.2.0`, with corresponding `yarn.lock` updates. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 192a5c846d0e01487929a506d71542523b2a96cf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [02ac109](https://github.com/MetaMask/metamask-mobile/commit/02ac109fb36974ecfe3dbbdecc15b529bc2a8d17) Co-authored-by: Frederik Bolding --- ...esystem-storage-npm-4.2.0-3a6fff24ab.patch | 28 +++++++++++-------- package.json | 2 +- yarn.lock | 13 +++++++-- 3 files changed, 29 insertions(+), 14 deletions(-) rename patches/redux-persist-filesystem-storage+4.2.0.patch => .yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch (56%) diff --git a/patches/redux-persist-filesystem-storage+4.2.0.patch b/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch similarity index 56% rename from patches/redux-persist-filesystem-storage+4.2.0.patch rename to .yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch index 37aa4a293c5..dda65f1830d 100644 --- a/patches/redux-persist-filesystem-storage+4.2.0.patch +++ b/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch @@ -1,7 +1,7 @@ -diff --git a/node_modules/redux-persist-filesystem-storage/index.d.ts b/node_modules/redux-persist-filesystem-storage/index.d.ts -index b0caa94..76b0442 100644 ---- a/node_modules/redux-persist-filesystem-storage/index.d.ts -+++ b/node_modules/redux-persist-filesystem-storage/index.d.ts +diff --git a/index.d.ts b/index.d.ts +index b0caa94ceaa9afaa6c112947a328887e580f76a2..76b0442a7367f39d8ae9300825815edda5b02c44 100644 +--- a/index.d.ts ++++ b/index.d.ts @@ -12,6 +12,7 @@ declare module 'redux-persist-filesystem-storage' { setItem: ( key: string, @@ -10,24 +10,30 @@ index b0caa94..76b0442 100644 callback?: (error?: Error) => void, ) => Promise -diff --git a/node_modules/redux-persist-filesystem-storage/index.js b/node_modules/redux-persist-filesystem-storage/index.js -index d69afb6..0ca3a25 100644 ---- a/node_modules/redux-persist-filesystem-storage/index.js -+++ b/node_modules/redux-persist-filesystem-storage/index.js -@@ -41,11 +41,14 @@ const FilesystemStorage = { +diff --git a/index.js b/index.js +index d69afb678b3d06760ad59831457cfd5c51fdb89b..8d5ecb060a91f137c865ed21f891b640d3cc65fe 100644 +--- a/index.js ++++ b/index.js +@@ -41,11 +41,19 @@ const FilesystemStorage = { onStorageReady = onStorageReadyFactory(options.storagePath); }, - setItem: (key: string, value: string, callback?: (error: ?Error) => void) => +- ReactNativeBlobUtil.fs + setItem: (key: string, value: string, isIOS: boolean = false, callback?: (error: ?Error) => void) => { - ReactNativeBlobUtil.fs ++ return ReactNativeBlobUtil.fs .writeFile(pathForKey(key), value, options.encoding) - .then(() => callback && callback()) - .catch(error => callback && callback(error)), + .then(() => { + if (isIOS) ReactNativeBlobUtil.ios.excludeFromBackupKey(pathForKey(key)); + callback && callback(); -+ }).catch(error => callback && callback(error)); ++ }).catch((error) => { ++ if (!callback) { ++ throw error; ++ } ++ callback(error); ++ }); + }, getItem: onStorageReady( diff --git a/package.json b/package.json index dee636d1ffb..f56c1d5310a 100644 --- a/package.json +++ b/package.json @@ -473,7 +473,7 @@ "redux": "^4.2.1", "redux-mock-store": "1.5.4", "redux-persist": "6.0.0", - "redux-persist-filesystem-storage": "^4.2.0", + "redux-persist-filesystem-storage": "patch:redux-persist-filesystem-storage@npm%3A4.2.0#~/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch", "redux-saga": "^1.3.0", "redux-thunk": "^2.4.2", "reselect": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index cb7ebdcdf5d..75da9206fc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35982,7 +35982,7 @@ __metadata: redux-devtools-expo-dev-plugin: "npm:^1.0.0" redux-mock-store: "npm:1.5.4" redux-persist: "npm:6.0.0" - redux-persist-filesystem-storage: "npm:^4.2.0" + redux-persist-filesystem-storage: "patch:redux-persist-filesystem-storage@npm%3A4.2.0#~/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch" redux-saga: "npm:^1.3.0" redux-saga-test-plan: "npm:^4.0.6" redux-thunk: "npm:^2.4.2" @@ -41724,7 +41724,7 @@ __metadata: languageName: node linkType: hard -"redux-persist-filesystem-storage@npm:^4.2.0": +"redux-persist-filesystem-storage@npm:4.2.0": version: 4.2.0 resolution: "redux-persist-filesystem-storage@npm:4.2.0" dependencies: @@ -41733,6 +41733,15 @@ __metadata: languageName: node linkType: hard +"redux-persist-filesystem-storage@patch:redux-persist-filesystem-storage@npm%3A4.2.0#~/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch": + version: 4.2.0 + resolution: "redux-persist-filesystem-storage@patch:redux-persist-filesystem-storage@npm%3A4.2.0#~/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch::version=4.2.0&hash=4dfd27" + dependencies: + react-native-blob-util: "npm:^0.18.0" + checksum: 10/fa556e2d1784a5e664e2e7024fa2255b08334e0dacf8993acca676cb912ad82c0f8ef3ba9ec2597d455f9dded83acf3343cc0a66d4e2fc14486e31dd9efe6def + languageName: node + linkType: hard + "redux-persist@npm:6.0.0": version: 6.0.0 resolution: "redux-persist@npm:6.0.0" From c194f0bcd868392ba68940e27a3fe240f4bb4d8d Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:05:19 +0100 Subject: [PATCH 089/131] chore(runway): cherry-pick chore: Bump `snaps-controllers` cp-7.67.2 (#27042) - chore: Bump `snaps-controllers` cp-7.67.2 (#26992) ## **Description** Bump `snaps-controllers` to the latest version which includes a mitigation for production issues where the source code of Snaps is unavailable. ## **Changelog** CHANGELOG entry: Fixed an issue with running Snaps ## **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. ## **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 Snaps dependency used at runtime; even a patch bump can affect Snap execution/permissions and should be regression-tested with common Snaps flows. > > **Overview** > Bumps `@metamask/snaps-controllers` from `^18.0.1` to `^18.0.2` (with corresponding `yarn.lock` updates) to pick up the latest fixes. > > Includes a small transitive dependency update within that package (notably `@metamask/json-rpc-engine` to `^10.2.3`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 67b823f187c316717d04e2c63f0ea6e5733b661e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [4b57dcd](https://github.com/MetaMask/metamask-mobile/commit/4b57dcdeacb085c4ce41a33bb9ef3cbc2d26b7d2) Co-authored-by: Frederik Bolding --- package.json | 2 +- yarn.lock | 31 +++++++++++++++---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index f56c1d5310a..a0ef2cc8f45 100644 --- a/package.json +++ b/package.json @@ -280,7 +280,7 @@ "@metamask/signature-controller": "^35.0.0", "@metamask/slip44": "^4.2.0", "@metamask/smart-transactions-controller": "^22.5.0", - "@metamask/snaps-controllers": "^18.0.0", + "@metamask/snaps-controllers": "^18.0.2", "@metamask/snaps-execution-environments": "^11.0.0", "@metamask/snaps-rpc-methods": "^14.3.0", "@metamask/snaps-sdk": "^10.4.0", diff --git a/yarn.lock b/yarn.lock index 75da9206fc9..53eb1c2ab2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8875,9 +8875,9 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@npm:^10.2.0, @metamask/json-rpc-engine@npm:^10.2.1, @metamask/json-rpc-engine@npm:^10.2.2": - version: 10.2.2 - resolution: "@metamask/json-rpc-engine@npm:10.2.2" +"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.0.3, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@npm:^10.2.0, @metamask/json-rpc-engine@npm:^10.2.1, @metamask/json-rpc-engine@npm:^10.2.2, @metamask/json-rpc-engine@npm:^10.2.3": + version: 10.2.3 + resolution: "@metamask/json-rpc-engine@npm:10.2.3" dependencies: "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/safe-event-emitter": "npm:^3.0.0" @@ -8885,7 +8885,7 @@ __metadata: "@types/deep-freeze-strict": "npm:^1.1.0" deep-freeze-strict: "npm:^1.1.1" klona: "npm:^2.0.6" - checksum: 10/e2449e80f8ca3aed58d0778c220eba6c98e0848359da2703bcb68879c1b315774a1a8a90b2a7cd8d3eb3e0f022f9d0e30503e75a2645ec32cbfc5ba2e537f807 + checksum: 10/8895ffcfc0dbf5542476dfd9771cb288feaf6fd7e9628e02c10232b3b8f0feabe3a0ad3e3480e3260a69aaafcf8f58d1d89410e7f43e97a08350b3ec3e767b1d languageName: node linkType: hard @@ -9972,19 +9972,18 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^18.0.0": - version: 18.0.0 - resolution: "@metamask/snaps-controllers@npm:18.0.0" +"@metamask/snaps-controllers@npm:^18.0.2": + version: 18.0.2 + resolution: "@metamask/snaps-controllers@npm:18.0.2" dependencies: "@metamask/approval-controller": "npm:^8.0.0" "@metamask/base-controller": "npm:^9.0.0" - "@metamask/json-rpc-engine": "npm:^10.2.1" + "@metamask/json-rpc-engine": "npm:^10.2.3" "@metamask/json-rpc-middleware-stream": "npm:^8.0.8" "@metamask/key-tree": "npm:^10.1.1" "@metamask/messenger": "npm:^0.3.0" "@metamask/object-multiplex": "npm:^2.1.0" "@metamask/permission-controller": "npm:^12.2.0" - "@metamask/phishing-controller": "npm:^16.1.0" "@metamask/post-message-stream": "npm:^10.0.0" "@metamask/rpc-errors": "npm:^7.0.3" "@metamask/snaps-registry": "npm:^4.0.0" @@ -9993,7 +9992,7 @@ __metadata: "@metamask/snaps-utils": "npm:^12.1.0" "@metamask/storage-service": "npm:^1.0.0" "@metamask/superstruct": "npm:^3.2.1" - "@metamask/utils": "npm:^11.9.0" + "@metamask/utils": "npm:^11.10.0" "@xstate/fsm": "npm:^2.0.0" async-mutex: "npm:^0.5.0" concat-stream: "npm:^2.0.0" @@ -10012,7 +10011,7 @@ __metadata: peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/f0fb53c59a801118f29f28222c44ea0927cfae004cd5d9f5f33e7e4eef3a31e495c91b120e49cf310b6990e853090013e7a6ed10fac2dfb197410d073c54c4d4 + checksum: 10/3d8f88ff926b2918b1632dc9920c94871df85146a66d4d6dbb8fb31ee241c7012e85d86bdf30be47aec97fc2fecf724f5e235f30a1550bedf4084586175e05eb languageName: node linkType: hard @@ -10433,9 +10432,9 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^11.0.0, @metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.5.0, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0": - version: 11.9.0 - resolution: "@metamask/utils@npm:11.9.0" +"@metamask/utils@npm:^11.0.0, @metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.5.0, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0": + version: 11.10.0 + resolution: "@metamask/utils@npm:11.10.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/superstruct": "npm:^3.1.0" @@ -10448,7 +10447,7 @@ __metadata: pony-cause: "npm:^2.1.10" semver: "npm:^7.5.4" uuid: "npm:^9.0.1" - checksum: 10/f8f5e99ba6c6de0395ed4e0acc82ee9c0dca26991ea6a8f10b3896e72745790966a8eded8c42be905d9f01fa99c1fd29a7f68541e2ef9854fc14984a0b514ad3 + checksum: 10/691a268af66593b60e9807a069127993cea3cdc941f99d5d7ca4664868754f08945821f1787b2f3e99e4497df63ceb0af37a2419ad494da29a1fddffe94f5797 languageName: node linkType: hard @@ -35678,7 +35677,7 @@ __metadata: "@metamask/signature-controller": "npm:^35.0.0" "@metamask/slip44": "npm:^4.2.0" "@metamask/smart-transactions-controller": "npm:^22.5.0" - "@metamask/snaps-controllers": "npm:^18.0.0" + "@metamask/snaps-controllers": "npm:^18.0.2" "@metamask/snaps-execution-environments": "npm:^11.0.0" "@metamask/snaps-rpc-methods": "npm:^14.3.0" "@metamask/snaps-sdk": "npm:^10.4.0" From dcf639d38af852022e4591cf0346277c8bd82271 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 5 Mar 2026 10:06:59 +0000 Subject: [PATCH 090/131] [skip ci] Bump version number to 3899 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 179f5db160f..8342234dcfe 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.67.3" - versionCode 3844 + versionCode 3899 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 66b2b00785b..d8babf9a216 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.3 - opts: is_expand: false - VERSION_NUMBER: 3844 + VERSION_NUMBER: 3899 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.3 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3844 + FLASK_VERSION_NUMBER: 3899 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 5ed0927466d..c926db08f8d 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3844; + CURRENT_PROJECT_VERSION = 3899; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3844; + CURRENT_PROJECT_VERSION = 3899; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3844; + CURRENT_PROJECT_VERSION = 3899; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3844; + CURRENT_PROJECT_VERSION = 3899; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3844; + CURRENT_PROJECT_VERSION = 3899; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3844; + CURRENT_PROJECT_VERSION = 3899; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From bd044f7c516728d2ff82c3896ca0eeeac18c9f52 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 5 Mar 2026 14:16:30 +0000 Subject: [PATCH 091/131] [skip ci] Bump version number to 3901 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 8342234dcfe..09b47fb4703 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.67.3" - versionCode 3899 + versionCode 3901 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index d8babf9a216..8d5c36924eb 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.67.3 - opts: is_expand: false - VERSION_NUMBER: 3899 + VERSION_NUMBER: 3901 - opts: is_expand: false FLASK_VERSION_NAME: 7.67.3 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3899 + FLASK_VERSION_NUMBER: 3901 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index c926db08f8d..0aadd8fbc70 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3899; + CURRENT_PROJECT_VERSION = 3901; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3899; + CURRENT_PROJECT_VERSION = 3901; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3899; + CURRENT_PROJECT_VERSION = 3901; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3899; + CURRENT_PROJECT_VERSION = 3901; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3899; + CURRENT_PROJECT_VERSION = 3901; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3899; + CURRENT_PROJECT_VERSION = 3901; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From a282970b29e72f3c7082025849f4764b19369a95 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:21:13 +0000 Subject: [PATCH 092/131] chore(runway): cherry-pick fix: MUSD-450 extract Merkl bonus claim logic from TokenListItem components (#27027) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: cp-7.68.0 MUSD-450 extract Merkl bonus claim logic from TokenListItem components (#26953) ## **Description** This PR fixes an issue where the mUSD "Claim bonus" was displayed for geo-blocked users. ### Changes: - Create `selectMerklClaimTransactions` selector with deep-equal memoization to prevent re-render cascades from unrelated transaction updates - Create `useMerklBonusClaim` hook to replace `MerklClaimHandler` headless component pattern, composing `useMerklRewards`, `usePendingMerklClaim`, and `useMerklClaim` with internal eligibility gating - Update `usePendingMerklClaim` to use targeted Merkl transaction selector - Refactor `TokenListItem` and `TokenListItemV2` to use `useMerklBonusClaim` directly - Rename `isEligibleForMerklRewards` to `isTokenEligibleForMerklRewards` - Remove `MerklClaimHandler` - Rename `useMerklClaim` to `useMerklClaimTransaction` to better differentiate against `useMerklBonusClaim` ## **Changelog** CHANGELOG entry: refactored merkl claim headless structure in token list item components and prevent "claim bonus" cta from rendering when users are geo-blocked ## **Related issues** Fixes: [MUSD-450: Claim bonus visible for geo-blocked users](https://consensyssoftware.atlassian.net/browse/MUSD-450) ## **Manual testing steps** ```gherkin Feature: Geo-blocked users cannot see Merkl "Claim bonus" CTA Scenario: user in allowed region sees "Claim bonus" CTA Given user is in a non-blocked region And user holds a Merkl-eligible token with claimable rewards And the Merkl campaign claiming feature flag is enabled When user views the token in the token list Then the "Claim bonus" CTA is displayed on the token row Scenario: user in blocked region does not see "Claim bonus" CTA Given user is in a geo-blocked region And user holds a Merkl-eligible token with claimable rewards And the Merkl campaign claiming feature flag is enabled When user views the token in the token list Then the "Claim bonus" CTA is not displayed And the standard percentage change is shown instead ``` ## **Screenshots/Recordings** ### **Before** `"Claim bonus"` CTA is displayed for geo-blocked users. ### **After** `"Claim bonus"` CTA is hidden for geo-blocked users. Recording to show claim experience this functions correctly after the refactor https://github.com/user-attachments/assets/6e6af59f-5f0a-4c69-8ad7-bc1a75eae61c ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Medium Risk** > Changes the eligibility and pending-transaction plumbing that controls when the Merkl “Claim bonus” CTA is shown and when claim spinners/pending state appear, so regressions could hide/show the CTA incorrectly. Scope is limited to Merkl claim UI/hooks and is covered by new/updated unit tests. > > **Overview** > Fixes Merkl “Claim bonus” CTA eligibility by replacing the `MerklClaimHandler` headless component/state-sync pattern with a new `useMerklBonusClaim` hook that composes claimable rewards, pending-claim status, and claim transaction submission while **no-op’ing for ineligible/geo-blocked users**. > > Adds `selectMerklClaimTransactions` (deep-equal memoized) and updates `usePendingMerklClaim` to consume this filtered selector instead of scanning all transactions, reducing unrelated re-render cascades. > > Renames `isEligibleForMerklRewards` to `isTokenEligibleForMerklRewards`, renames `useMerklClaim` to `useMerklClaimTransaction`, and refactors `TokenListItem`/`TokenListItemV2` (and tests) to rely solely on the composed hook for CTA/spinner behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 820459d0e021c1092720b6175e6489f0de5f16d2. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [05b4da9](https://github.com/MetaMask/metamask-mobile/commit/05b4da9b227220dce1a5880385607afa399ba430) Co-authored-by: Matthew Grainger <46547583+Matt561@users.noreply.github.com> --- .../MerklRewards/hooks/MerklClaimHandler.tsx | 73 ---- .../hooks/useMerklBonusClaim.test.ts | 159 ++++++++ .../MerklRewards/hooks/useMerklBonusClaim.ts | 88 +++++ ...st.ts => useMerklClaimTransaction.test.ts} | 38 +- ...klClaim.ts => useMerklClaimTransaction.ts} | 2 +- .../hooks/useMerklRewards.test.ts | 19 +- .../MerklRewards/hooks/useMerklRewards.ts | 4 +- .../hooks/usePendingMerklClaim.test.ts | 60 ++- .../hooks/usePendingMerklClaim.ts | 25 +- .../components/MerklRewards/merkl-client.ts | 2 +- .../selectors/merklClaimTransactions.test.ts | 64 ++++ .../Earn/selectors/merklClaimTransactions.ts | 17 + .../TokenListItem/TokenListItem.test.tsx | 154 +------- .../TokenList/TokenListItem/TokenListItem.tsx | 192 ++++------ .../TokenListItemV2/TokenListItemV2.test.tsx | 70 +--- .../TokenListItemV2/TokenListItemV2.tsx | 345 ++++++++---------- 16 files changed, 651 insertions(+), 661 deletions(-) delete mode 100644 app/components/UI/Earn/components/MerklRewards/hooks/MerklClaimHandler.tsx create mode 100644 app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.test.ts create mode 100644 app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.ts rename app/components/UI/Earn/components/MerklRewards/hooks/{useMerklClaim.test.ts => useMerklClaimTransaction.test.ts} (91%) rename app/components/UI/Earn/components/MerklRewards/hooks/{useMerklClaim.ts => useMerklClaimTransaction.ts} (98%) create mode 100644 app/components/UI/Earn/selectors/merklClaimTransactions.test.ts create mode 100644 app/components/UI/Earn/selectors/merklClaimTransactions.ts diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/MerklClaimHandler.tsx b/app/components/UI/Earn/components/MerklRewards/hooks/MerklClaimHandler.tsx deleted file mode 100644 index 687473503d7..00000000000 --- a/app/components/UI/Earn/components/MerklRewards/hooks/MerklClaimHandler.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useEffect } from 'react'; -import { TokenI } from '../../../../Tokens/types'; -import { useMerklRewards } from './useMerklRewards'; -import { usePendingMerklClaim } from './usePendingMerklClaim'; -import { useMerklClaim } from './useMerklClaim'; - -export interface MerklClaimData { - claimableReward: string | null; - hasPendingClaim: boolean; - isClaiming: boolean; - claimRewards: () => Promise< - | { - txHash: string; - transactionMeta: Record; - } - | undefined - >; -} - -export const DEFAULT_MERKL_CLAIM_DATA: MerklClaimData = { - claimableReward: null, - hasPendingClaim: false, - isClaiming: false, - claimRewards: async () => undefined, -}; - -interface MerklClaimHandlerProps { - asset: TokenI | undefined; - onDataChange: (data: MerklClaimData) => void; -} - -/** - * Headless component that manages Merkl claim hooks and syncs state to parent. - * Only mount this for tokens eligible for Merkl rewards to avoid unnecessary - * hook overhead for non-eligible tokens. - * - * Renders null — no visual output. - */ -export const MerklClaimHandler = React.memo( - ({ asset, onDataChange }: MerklClaimHandlerProps) => { - const { claimableReward } = useMerklRewards({ asset }); - const { hasPendingClaim } = usePendingMerklClaim(); - const { claimRewards, isClaiming } = useMerklClaim(asset); - - useEffect(() => { - onDataChange({ - claimableReward, - hasPendingClaim, - claimRewards, - isClaiming, - }); - }, [ - claimableReward, - hasPendingClaim, - claimRewards, - isClaiming, - onDataChange, - ]); - - // Reset parent state to defaults when this handler unmounts - // so the parent doesn't retain stale data (e.g. isClaiming: true) - useEffect( - () => () => { - onDataChange(DEFAULT_MERKL_CLAIM_DATA); - }, - [onDataChange], - ); - - return null; - }, -); - -MerklClaimHandler.displayName = 'MerklClaimHandler'; diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.test.ts b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.test.ts new file mode 100644 index 00000000000..d1c0d9257f2 --- /dev/null +++ b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.test.ts @@ -0,0 +1,159 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { useMerklBonusClaim } from './useMerklBonusClaim'; +import { TokenI } from '../../../../Tokens/types'; +import { AGLAMERKL_ADDRESS_MAINNET } from '../constants'; + +const mockClaimRewards = jest.fn().mockResolvedValue(undefined); + +const mockUseMerklRewards = jest.fn((_opts?: unknown) => ({ + claimableReward: null as string | null, +})); +jest.mock('./useMerklRewards', () => ({ + useMerklRewards: (...args: [unknown]) => mockUseMerklRewards(...args), + isTokenEligibleForMerklRewards: + jest.requireActual('./useMerklRewards').isTokenEligibleForMerklRewards, +})); + +const mockUsePendingMerklClaim = jest.fn(() => ({ + hasPendingClaim: false, +})); +jest.mock('./usePendingMerklClaim', () => ({ + usePendingMerklClaim: () => mockUsePendingMerklClaim(), +})); + +const mockUseMerklClaimTransaction = jest.fn((_asset?: unknown) => ({ + claimRewards: mockClaimRewards, + isClaiming: false, + error: null, +})); +jest.mock('./useMerklClaimTransaction', () => ({ + useMerklClaimTransaction: (...args: [unknown]) => + mockUseMerklClaimTransaction(...args), +})); + +let mockIsMerklCampaignClaimingEnabled = true; +jest.mock('../../../selectors/featureFlags', () => ({ + selectMerklCampaignClaimingEnabledFlag: () => + mockIsMerklCampaignClaimingEnabled, +})); + +let mockIsGeoEligible = true; +jest.mock('../../../hooks/useMusdConversionEligibility', () => ({ + useMusdConversionEligibility: () => ({ + isEligible: mockIsGeoEligible, + isLoading: false, + geolocation: 'US', + blockedCountries: [], + }), +})); + +jest.mock('react-redux', () => ({ + useSelector: (selector: (state: unknown) => unknown) => selector({}), +})); + +const eligibleAsset: TokenI = { + address: AGLAMERKL_ADDRESS_MAINNET, + chainId: CHAIN_IDS.MAINNET, + symbol: 'AGLA', + decimals: 18, + name: 'AGLA Token', + balance: '1', + balanceFiat: '$1.00', + isNative: false, + isETH: false, + isStaked: false, + image: '', + aggregators: [], + logo: '', +}; + +const ineligibleAsset: TokenI = { + address: '0x0000000000000000000000000000000000000001', + chainId: CHAIN_IDS.MAINNET, + symbol: 'NOPE', + decimals: 18, + name: 'Ineligible Token', + balance: '1', + balanceFiat: '$1.00', + isNative: false, + isETH: false, + isStaked: false, + image: '', + aggregators: [], + logo: '', +}; + +describe('useMerklBonusClaim', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockIsMerklCampaignClaimingEnabled = true; + mockIsGeoEligible = true; + }); + + it('returns default claim data when asset is undefined', () => { + const { result } = renderHook(() => useMerklBonusClaim(undefined)); + + expect(result.current.claimableReward).toBeNull(); + expect(result.current.hasPendingClaim).toBe(false); + expect(result.current.isClaiming).toBe(false); + }); + + it('returns default claim data when feature flag is disabled', () => { + mockIsMerklCampaignClaimingEnabled = false; + + const { result } = renderHook(() => useMerklBonusClaim(eligibleAsset)); + + expect(result.current.claimableReward).toBeNull(); + expect(result.current.hasPendingClaim).toBe(false); + }); + + it('returns default claim data when user is geo-blocked', () => { + mockIsGeoEligible = false; + + const { result } = renderHook(() => useMerklBonusClaim(eligibleAsset)); + + expect(result.current.claimableReward).toBeNull(); + expect(result.current.hasPendingClaim).toBe(false); + }); + + it('returns default claim data for ineligible token', () => { + const { result } = renderHook(() => useMerklBonusClaim(ineligibleAsset)); + + expect(result.current.claimableReward).toBeNull(); + expect(result.current.hasPendingClaim).toBe(false); + }); + + it('passes undefined to underlying hooks when asset is ineligible', () => { + renderHook(() => useMerklBonusClaim(ineligibleAsset)); + + expect(mockUseMerklRewards).toHaveBeenCalledWith({ asset: undefined }); + expect(mockUseMerklClaimTransaction).toHaveBeenCalledWith(undefined); + }); + + it('passes eligible asset to underlying hooks', () => { + renderHook(() => useMerklBonusClaim(eligibleAsset)); + + expect(mockUseMerklRewards).toHaveBeenCalledWith({ + asset: eligibleAsset, + }); + expect(mockUseMerklClaimTransaction).toHaveBeenCalledWith(eligibleAsset); + }); + + it('returns composed data from underlying hooks for eligible asset', () => { + mockUseMerklRewards.mockReturnValue({ claimableReward: '1.50' }); + mockUsePendingMerklClaim.mockReturnValue({ hasPendingClaim: true }); + mockUseMerklClaimTransaction.mockReturnValue({ + claimRewards: mockClaimRewards, + isClaiming: true, + error: null, + }); + + const { result } = renderHook(() => useMerklBonusClaim(eligibleAsset)); + + expect(result.current.claimableReward).toBe('1.50'); + expect(result.current.hasPendingClaim).toBe(true); + expect(result.current.isClaiming).toBe(true); + expect(result.current.claimRewards).toBe(mockClaimRewards); + }); +}); diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.ts b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.ts new file mode 100644 index 00000000000..0fe71201fba --- /dev/null +++ b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim.ts @@ -0,0 +1,88 @@ +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; +import { TokenI } from '../../../../Tokens/types'; +import { + useMerklRewards, + isTokenEligibleForMerklRewards, +} from './useMerklRewards'; +import { usePendingMerklClaim } from './usePendingMerklClaim'; +import { useMerklClaimTransaction } from './useMerklClaimTransaction'; +import { selectMerklCampaignClaimingEnabledFlag } from '../../../selectors/featureFlags'; +import { useMusdConversionEligibility } from '../../../hooks/useMusdConversionEligibility'; + +export interface MerklClaimData { + claimableReward: string | null; + hasPendingClaim: boolean; + isClaiming: boolean; + claimRewards: () => Promise< + | { + txHash: string; + transactionMeta: Record; + } + | undefined + >; +} + +const DEFAULT_MERKL_CLAIM_DATA: MerklClaimData = { + claimableReward: null, + hasPendingClaim: false, + isClaiming: false, + claimRewards: async () => undefined, +}; + +/** + * Combines `useMerklRewards`, `usePendingMerklClaim`, and `useMerklClaimTransaction` + * into a single hook that can be called unconditionally in token list items. + * + * For ineligible or geo-blocked assets, `undefined` is passed to the underlying + * hooks which causes them to no-op (no API calls, no side effects). + * + * @param asset - The token to check for Merkl bonus claim eligibility + * @returns MerklClaimData with claim state and actions + */ +export const useMerklBonusClaim = ( + asset: TokenI | undefined, +): MerklClaimData => { + const isMerklCampaignClaimingEnabled = useSelector( + selectMerklCampaignClaimingEnabledFlag, + ); + const { isEligible: isGeoEligible } = useMusdConversionEligibility(); + + const isEligible = useMemo(() => { + if (!isMerklCampaignClaimingEnabled || !isGeoEligible) { + return false; + } + if (!asset?.chainId || !asset?.address) { + return false; + } + return isTokenEligibleForMerklRewards( + asset.chainId as Hex, + asset.address as Hex | undefined, + ); + }, [ + isMerklCampaignClaimingEnabled, + isGeoEligible, + asset?.chainId, + asset?.address, + ]); + + const eligibleAsset = isEligible ? asset : undefined; + + const { claimableReward } = useMerklRewards({ asset: eligibleAsset }); + const { hasPendingClaim } = usePendingMerklClaim(); + const { claimRewards, isClaiming } = useMerklClaimTransaction(eligibleAsset); + + return useMemo(() => { + if (!isEligible) { + return DEFAULT_MERKL_CLAIM_DATA; + } + + return { + claimableReward, + hasPendingClaim, + claimRewards, + isClaiming, + }; + }, [isEligible, claimableReward, hasPendingClaim, claimRewards, isClaiming]); +}; diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaim.test.ts b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaimTransaction.test.ts similarity index 91% rename from app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaim.test.ts rename to app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaimTransaction.test.ts index a5c7117485f..b3f70055ef9 100644 --- a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaim.test.ts +++ b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaimTransaction.test.ts @@ -1,6 +1,6 @@ import { renderHook, act } from '@testing-library/react-native'; import { useSelector } from 'react-redux'; -import { useMerklClaim } from './useMerklClaim'; +import { useMerklClaimTransaction } from './useMerklClaimTransaction'; import { addTransaction } from '../../../../../../util/transaction-controller'; import { selectSelectedInternalAccountFormattedAddress } from '../../../../../../selectors/accountsController'; import { TokenI } from '../../../../Tokens/types'; @@ -99,7 +99,7 @@ const createMockRewardData = (overrides?: { recipient: mockSelectedAddress, }); -describe('useMerklClaim', () => { +describe('useMerklClaimTransaction', () => { beforeEach(() => { jest.clearAllMocks(); mockFetchMerklRewardsForAsset.mockReset(); @@ -136,7 +136,7 @@ describe('useMerklClaim', () => { }); it('initializes with correct default values', () => { - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); expect(result.current.isClaiming).toBe(false); expect(result.current.error).toBe(null); @@ -151,7 +151,7 @@ describe('useMerklClaim', () => { return undefined; }); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); let claimResult: unknown; await act(async () => { @@ -178,7 +178,7 @@ describe('useMerklClaim', () => { return undefined; }); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); let claimResult: unknown; await act(async () => { @@ -198,7 +198,7 @@ describe('useMerklClaim', () => { transactionMeta: { id: 'tx-123' }, } as never); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); let claimResult: { txHash: string } | undefined; await act(async () => { @@ -227,7 +227,7 @@ describe('useMerklClaim', () => { new Error('Failed to fetch Merkl rewards: 500'), ); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); await act(async () => { await result.current.claimRewards(); @@ -241,7 +241,7 @@ describe('useMerklClaim', () => { // fetchMerklRewardsForAsset returns null when no matching reward exists mockFetchMerklRewardsForAsset.mockResolvedValueOnce(null); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); await act(async () => { await result.current.claimRewards(); @@ -256,7 +256,7 @@ describe('useMerklClaim', () => { new Error('Network error'), ); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); let claimResult: unknown; await act(async () => { @@ -276,7 +276,7 @@ describe('useMerklClaim', () => { transactionMeta: { id: 'tx-123' }, } as never); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); // Start claim and capture promise - isClaiming becomes true synchronously let claimPromise: Promise<{ txHash: string } | undefined> | undefined; @@ -316,7 +316,7 @@ describe('useMerklClaim', () => { transactionMeta: { id: 'tx-123' }, } as never); - const { result } = renderHook(() => useMerklClaim(lineaAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(lineaAsset)); await act(async () => { await result.current.claimRewards(); @@ -342,7 +342,7 @@ describe('useMerklClaim', () => { transactionMeta: { id: 'tx-123' }, } as never); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); await act(async () => { await result.current.claimRewards(); @@ -364,7 +364,7 @@ describe('useMerklClaim', () => { transactionMeta: { id: 'tx-456' }, } as never); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); let claimResult: { txHash: string } | undefined; await act(async () => { @@ -386,7 +386,7 @@ describe('useMerklClaim', () => { ); mockAddTransaction.mockRejectedValueOnce(userRejectionError); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); await act(async () => { try { @@ -407,7 +407,7 @@ describe('useMerklClaim', () => { // Error without code 4001 should set error state mockAddTransaction.mockRejectedValueOnce(new Error('Network error')); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); await act(async () => { try { @@ -424,7 +424,7 @@ describe('useMerklClaim', () => { describe('undefined asset handling', () => { it('initializes with correct default values when asset is undefined', () => { - const { result } = renderHook(() => useMerklClaim(undefined)); + const { result } = renderHook(() => useMerklClaimTransaction(undefined)); expect(result.current.isClaiming).toBe(false); expect(result.current.error).toBe(null); @@ -432,7 +432,7 @@ describe('useMerklClaim', () => { }); it('sets error and returns undefined when claimRewards is called with undefined asset', async () => { - const { result } = renderHook(() => useMerklClaim(undefined)); + const { result } = renderHook(() => useMerklClaimTransaction(undefined)); let claimResult: unknown; await act(async () => { @@ -462,7 +462,7 @@ describe('useMerklClaim', () => { transactionMeta: { id: 'tx-123' }, } as never); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); // Start claim let claimPromise: Promise; @@ -501,7 +501,7 @@ describe('useMerklClaim', () => { transactionMeta: undefined, } as never); - const { result } = renderHook(() => useMerklClaim(mockAsset)); + const { result } = renderHook(() => useMerklClaimTransaction(mockAsset)); let claimResult: unknown; await act(async () => { diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaim.ts b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaimTransaction.ts similarity index 98% rename from app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaim.ts rename to app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaimTransaction.ts index ff7378d797a..3e40ec6e7c1 100644 --- a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaim.ts +++ b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklClaimTransaction.ts @@ -25,7 +25,7 @@ import { * After successful submission, user is navigated to home page. * Toast notifications and balance refresh are handled globally by useMerklClaimStatus. */ -export const useMerklClaim = (asset: TokenI | undefined) => { +export const useMerklClaimTransaction = (asset: TokenI | undefined) => { const [isClaiming, setIsClaiming] = useState(false); const [error, setError] = useState(null); const abortControllerRef = useRef(null); diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.test.ts b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.test.ts index 1650d4aa776..0191d2045c8 100644 --- a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.test.ts +++ b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.test.ts @@ -1,7 +1,10 @@ import { renderHook, act, waitFor } from '@testing-library/react-native'; import { useSelector } from 'react-redux'; import { Hex } from '@metamask/utils'; -import { isEligibleForMerklRewards, useMerklRewards } from './useMerklRewards'; +import { + isTokenEligibleForMerklRewards, + useMerklRewards, +} from './useMerklRewards'; import { selectSelectedInternalAccountFormattedAddress } from '../../../../../../selectors/accountsController'; import { renderFromTokenMinimalUnit } from '../../../../../../util/number'; import { TokenI } from '../../../../Tokens/types'; @@ -86,9 +89,9 @@ const mockAsset: TokenI = { isNative: false, }; -describe('isEligibleForMerklRewards', () => { +describe('isTokenEligibleForMerklRewards', () => { it('returns false for native tokens with undefined address', () => { - const result = isEligibleForMerklRewards( + const result = isTokenEligibleForMerklRewards( CHAIN_IDS.MAINNET, undefined as Hex | undefined, ); @@ -97,7 +100,7 @@ describe('isEligibleForMerklRewards', () => { }); it('returns false for native tokens with null address', () => { - const result = isEligibleForMerklRewards( + const result = isTokenEligibleForMerklRewards( CHAIN_IDS.MAINNET, null as Hex | null, ); @@ -107,7 +110,7 @@ describe('isEligibleForMerklRewards', () => { it('returns false for unsupported chains', () => { const unsupportedChainId = '0x999' as Hex; - const result = isEligibleForMerklRewards( + const result = isTokenEligibleForMerklRewards( unsupportedChainId, AGLAMERKL_ADDRESS_MAINNET as Hex, ); @@ -118,7 +121,7 @@ describe('isEligibleForMerklRewards', () => { it('returns false for non-eligible tokens', () => { const nonEligibleAddress = '0x1111111111111111111111111111111111111111' as Hex; - const result = isEligibleForMerklRewards( + const result = isTokenEligibleForMerklRewards( CHAIN_IDS.MAINNET, nonEligibleAddress, ); @@ -128,7 +131,7 @@ describe('isEligibleForMerklRewards', () => { it('returns true for eligible tokens on mainnet', () => { const eligibleAddress = AGLAMERKL_ADDRESS_MAINNET as Hex; - const result = isEligibleForMerklRewards( + const result = isTokenEligibleForMerklRewards( CHAIN_IDS.MAINNET, eligibleAddress, ); @@ -138,7 +141,7 @@ describe('isEligibleForMerklRewards', () => { it('performs case-insensitive address comparison', () => { const upperCaseAddress = AGLAMERKL_ADDRESS_MAINNET.toUpperCase() as Hex; - const result = isEligibleForMerklRewards( + const result = isTokenEligibleForMerklRewards( CHAIN_IDS.MAINNET, upperCaseAddress, ); diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.ts b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.ts index 6afcb20bf7b..0e401dd75a7 100644 --- a/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.ts +++ b/app/components/UI/Earn/components/MerklRewards/hooks/useMerklRewards.ts @@ -33,7 +33,7 @@ export const eligibleTokens: Record = { * Compares addresses case-insensitively since Ethereum addresses are case-insensitive * Returns false for native tokens (undefined/null address) */ -export const isEligibleForMerklRewards = ( +export const isTokenEligibleForMerklRewards = ( chainId: Hex, address: Hex | undefined | null, ): boolean => { @@ -81,7 +81,7 @@ export const useMerklRewards = ({ return; } - const isEligible = isEligibleForMerklRewards( + const isEligible = isTokenEligibleForMerklRewards( asset.chainId as Hex, asset.address as Hex | undefined, ); diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/usePendingMerklClaim.test.ts b/app/components/UI/Earn/components/MerklRewards/hooks/usePendingMerklClaim.test.ts index 8d13de2ca12..c82bcf13904 100644 --- a/app/components/UI/Earn/components/MerklRewards/hooks/usePendingMerklClaim.test.ts +++ b/app/components/UI/Earn/components/MerklRewards/hooks/usePendingMerklClaim.test.ts @@ -3,10 +3,10 @@ import { TransactionStatus } from '@metamask/transaction-controller'; import { usePendingMerklClaim } from './usePendingMerklClaim'; import { MERKL_CLAIM_ORIGIN } from '../constants'; -// Mock the selector -const mockSelectTransactions = jest.fn(); -jest.mock('../../../../../../selectors/transactionController', () => ({ - selectTransactions: (state: unknown) => mockSelectTransactions(state), +const mockSelectMerklClaimTransactions = jest.fn(); +jest.mock('../../../selectors/merklClaimTransactions', () => ({ + selectMerklClaimTransactions: (state: unknown) => + mockSelectMerklClaimTransactions(state), })); // Mock react-redux @@ -44,20 +44,7 @@ describe('usePendingMerklClaim', () => { }); it('returns hasPendingClaim as false when no transactions exist', () => { - mockSelectTransactions.mockReturnValue([]); - - const { result } = renderHook(() => usePendingMerklClaim()); - - expect(result.current.hasPendingClaim).toBe(false); - }); - - it('returns hasPendingClaim as false when no merkl claim transactions exist', () => { - mockSelectTransactions.mockReturnValue([ - createMockTransaction({ - origin: 'other-origin', - status: TransactionStatus.submitted, - }), - ]); + mockSelectMerklClaimTransactions.mockReturnValue([]); const { result } = renderHook(() => usePendingMerklClaim()); @@ -65,7 +52,7 @@ describe('usePendingMerklClaim', () => { }); it('returns hasPendingClaim as true when approved merkl claim transaction exists', () => { - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ status: TransactionStatus.approved }), ]); @@ -75,7 +62,7 @@ describe('usePendingMerklClaim', () => { }); it('returns hasPendingClaim as true when signed merkl claim transaction exists', () => { - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ status: TransactionStatus.signed }), ]); @@ -85,7 +72,7 @@ describe('usePendingMerklClaim', () => { }); it('returns hasPendingClaim as true when submitted merkl claim transaction exists', () => { - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ status: TransactionStatus.submitted }), ]); @@ -95,7 +82,7 @@ describe('usePendingMerklClaim', () => { }); it('returns hasPendingClaim as false when merkl claim transaction is confirmed', () => { - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ status: TransactionStatus.confirmed }), ]); @@ -105,7 +92,7 @@ describe('usePendingMerklClaim', () => { }); it('returns hasPendingClaim as false when merkl claim transaction failed', () => { - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ status: TransactionStatus.failed }), ]); @@ -115,7 +102,7 @@ describe('usePendingMerklClaim', () => { }); it('returns hasPendingClaim as false when merkl claim transaction was dropped', () => { - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ status: TransactionStatus.dropped }), ]); @@ -125,7 +112,7 @@ describe('usePendingMerklClaim', () => { }); it('returns hasPendingClaim as false when merkl claim transaction is unapproved', () => { - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ status: TransactionStatus.unapproved }), ]); @@ -135,18 +122,13 @@ describe('usePendingMerklClaim', () => { }); it('returns hasPendingClaim as true when at least one in-flight merkl claim exists among multiple transactions', () => { - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ id: 'tx-1', - origin: 'other-origin', - status: TransactionStatus.submitted, - }), - createMockTransaction({ - id: 'tx-2', status: TransactionStatus.confirmed, }), createMockTransaction({ - id: 'tx-3', + id: 'tx-2', status: TransactionStatus.submitted, }), ]); @@ -162,7 +144,7 @@ describe('usePendingMerklClaim', () => { const txId = 'tx-pending-to-confirmed'; // Start with pending transaction - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ id: txId, status: TransactionStatus.submitted, @@ -176,7 +158,7 @@ describe('usePendingMerklClaim', () => { expect(onClaimConfirmed).not.toHaveBeenCalled(); // Transaction becomes confirmed - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ id: txId, status: TransactionStatus.confirmed, @@ -192,7 +174,7 @@ describe('usePendingMerklClaim', () => { const onClaimConfirmed = jest.fn(); // Start with already confirmed transaction (not tracked as pending) - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ id: 'tx-already-confirmed', status: TransactionStatus.confirmed, @@ -209,7 +191,7 @@ describe('usePendingMerklClaim', () => { const txId = 'tx-pending-to-failed'; // Start with pending transaction - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ id: txId, status: TransactionStatus.submitted, @@ -221,7 +203,7 @@ describe('usePendingMerklClaim', () => { ); // Transaction fails - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ id: txId, status: TransactionStatus.failed, @@ -237,7 +219,7 @@ describe('usePendingMerklClaim', () => { const txId = 'tx-no-callback'; // Start with pending transaction - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ id: txId, status: TransactionStatus.submitted, @@ -247,7 +229,7 @@ describe('usePendingMerklClaim', () => { const { rerender } = renderHook(() => usePendingMerklClaim()); // Transaction becomes confirmed - should not throw - mockSelectTransactions.mockReturnValue([ + mockSelectMerklClaimTransactions.mockReturnValue([ createMockTransaction({ id: txId, status: TransactionStatus.confirmed, diff --git a/app/components/UI/Earn/components/MerklRewards/hooks/usePendingMerklClaim.ts b/app/components/UI/Earn/components/MerklRewards/hooks/usePendingMerklClaim.ts index 453871b3961..92a3409ccab 100644 --- a/app/components/UI/Earn/components/MerklRewards/hooks/usePendingMerklClaim.ts +++ b/app/components/UI/Earn/components/MerklRewards/hooks/usePendingMerklClaim.ts @@ -1,8 +1,7 @@ import { useMemo, useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { TransactionStatus } from '@metamask/transaction-controller'; -import { selectTransactions } from '../../../../../../selectors/transactionController'; -import { MERKL_CLAIM_ORIGIN } from '../constants'; +import { selectMerklClaimTransactions } from '../../../selectors/merklClaimTransactions'; /** * Transaction statuses that indicate a claim is "in flight" @@ -34,19 +33,15 @@ export const usePendingMerklClaim = ( options: UsePendingMerklClaimOptions = {}, ) => { const { onClaimConfirmed } = options; - const transactions = useSelector(selectTransactions); + const claimTransactions = useSelector(selectMerklClaimTransactions); // Track the IDs of pending claims we've seen const pendingClaimIdsRef = useRef>(new Set()); const hasPendingClaim = useMemo( () => - transactions.some( - (tx) => - tx.origin === MERKL_CLAIM_ORIGIN && - IN_FLIGHT_STATUSES.includes(tx.status), - ), - [transactions], + claimTransactions.some((tx) => IN_FLIGHT_STATUSES.includes(tx.status)), + [claimTransactions], ); // Stable callback ref to avoid effect re-running @@ -57,19 +52,13 @@ export const usePendingMerklClaim = ( // Detect when a pending claim becomes confirmed useEffect(() => { - const merklClaimTxs = transactions.filter( - (tx) => tx.origin === MERKL_CLAIM_ORIGIN, - ); - - // Get current pending claim IDs const currentPendingIds = new Set( - merklClaimTxs + claimTransactions .filter((tx) => IN_FLIGHT_STATUSES.includes(tx.status)) .map((tx) => tx.id), ); - // Check if any previously pending claims are now confirmed - const confirmedIds = merklClaimTxs + const confirmedIds = claimTransactions .filter((tx) => tx.status === TransactionStatus.confirmed) .map((tx) => tx.id); @@ -84,7 +73,7 @@ export const usePendingMerklClaim = ( if (hadPendingThatConfirmed && onClaimConfirmedRef.current) { onClaimConfirmedRef.current(); } - }, [transactions]); + }, [claimTransactions]); return { hasPendingClaim }; }; diff --git a/app/components/UI/Earn/components/MerklRewards/merkl-client.ts b/app/components/UI/Earn/components/MerklRewards/merkl-client.ts index 0f3b92fbef0..d4f9db5eb84 100644 --- a/app/components/UI/Earn/components/MerklRewards/merkl-client.ts +++ b/app/components/UI/Earn/components/MerklRewards/merkl-client.ts @@ -55,7 +55,7 @@ export interface MerklRewardData { // Follows the Sentinel API caching pattern (TTL + request deduplication). // // The display hook (useMerklRewards) fetches on mount, warming the cache. -// When the user taps "Claim bonus", useMerklClaim hits the warm cache +// When the user taps "Claim bonus", useMerklClaimTransaction hits the warm cache // instead of waiting for a fresh network round-trip. // --------------------------------------------------------------------------- diff --git a/app/components/UI/Earn/selectors/merklClaimTransactions.test.ts b/app/components/UI/Earn/selectors/merklClaimTransactions.test.ts new file mode 100644 index 00000000000..2f9a06d0a5c --- /dev/null +++ b/app/components/UI/Earn/selectors/merklClaimTransactions.test.ts @@ -0,0 +1,64 @@ +import { RootState } from '../../../../reducers'; +import { selectMerklClaimTransactions } from './merklClaimTransactions'; +import { MERKL_CLAIM_ORIGIN } from '../components/MerklRewards/constants'; + +const createState = (transactions: unknown[]): RootState => + ({ + engine: { + backgroundState: { + TransactionController: { + transactions, + }, + }, + }, + }) as unknown as RootState; + +describe('selectMerklClaimTransactions', () => { + it('returns only transactions with Merkl claim origin', () => { + const transactions = [ + { id: 'tx-1', origin: MERKL_CLAIM_ORIGIN }, + { id: 'tx-2', origin: 'other-origin' }, + { id: 'tx-3', origin: MERKL_CLAIM_ORIGIN }, + ]; + const state = createState(transactions); + + const result = selectMerklClaimTransactions(state); + + expect(result).toHaveLength(2); + expect(result[0]).toMatchObject({ id: 'tx-1', origin: MERKL_CLAIM_ORIGIN }); + expect(result[1]).toMatchObject({ id: 'tx-3', origin: MERKL_CLAIM_ORIGIN }); + }); + + it('returns empty array when no Merkl claim transactions exist', () => { + const transactions = [ + { id: 'tx-1', origin: 'metamask' }, + { id: 'tx-2', origin: 'dapp-origin' }, + ]; + const state = createState(transactions); + + const result = selectMerklClaimTransactions(state); + + expect(result).toEqual([]); + }); + + it('returns empty array when transactions array is empty', () => { + const state = createState([]); + + const result = selectMerklClaimTransactions(state); + + expect(result).toEqual([]); + }); + + it('filters out transactions with undefined origin', () => { + const transactions = [ + { id: 'tx-1' }, + { id: 'tx-2', origin: MERKL_CLAIM_ORIGIN }, + ]; + const state = createState(transactions); + + const result = selectMerklClaimTransactions(state); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ id: 'tx-2' }); + }); +}); diff --git a/app/components/UI/Earn/selectors/merklClaimTransactions.ts b/app/components/UI/Earn/selectors/merklClaimTransactions.ts new file mode 100644 index 00000000000..cbb7bb88daa --- /dev/null +++ b/app/components/UI/Earn/selectors/merklClaimTransactions.ts @@ -0,0 +1,17 @@ +import { TransactionMeta } from '@metamask/transaction-controller'; +import { createDeepEqualSelector } from '../../../../selectors/util'; +import { selectTransactions } from '../../../../selectors/transactionController'; +import { MERKL_CLAIM_ORIGIN } from '../components/MerklRewards/constants'; + +/** + * Selects only Merkl claim transactions from the full transaction list. + * + * Uses deep-equal memoization so that consumers only re-render when + * Merkl-specific transactions actually change, not on every unrelated + * transaction status update. + */ +export const selectMerklClaimTransactions = createDeepEqualSelector( + [selectTransactions], + (transactions): TransactionMeta[] => + transactions.filter((tx) => tx.origin === MERKL_CLAIM_ORIGIN), +); diff --git a/app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.test.tsx b/app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.test.tsx index ab480a7bdce..068b2c0f74e 100644 --- a/app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.test.tsx +++ b/app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.test.tsx @@ -21,11 +21,9 @@ import { useMusdConversionTokens } from '../../../Earn/hooks/useMusdConversionTo import { useMusdConversionEligibility } from '../../../Earn/hooks/useMusdConversionEligibility'; import { selectIsMusdConversionFlowEnabledFlag, - selectMerklCampaignClaimingEnabledFlag, selectMusdQuickConvertEnabledFlag, selectStablecoinLendingEnabledFlag, } from '../../../Earn/selectors/featureFlags'; -import { isEligibleForMerklRewards } from '../../../Earn/components/MerklRewards/hooks/useMerklRewards'; import { MUSD_CONVERSION_APY } from '../../../Earn/constants/musd'; import { EARN_EXPERIENCES } from '../../../Earn/constants/experiences'; import { MUSD_CONVERSION_NAVIGATION_OVERRIDE } from '../../../Earn/types/musd.types'; @@ -155,34 +153,16 @@ jest.mock('../../../Stake/hooks/useStakingChain', () => ({ })); const mockClaimRewards = jest.fn(); -const mockUseMerklClaim = jest.fn((_asset?: unknown) => ({ - claimRewards: mockClaimRewards, - isClaiming: false, - error: null, -})); -jest.mock('../../../Earn/components/MerklRewards/hooks/useMerklClaim', () => ({ - useMerklClaim: (...args: [unknown]) => mockUseMerklClaim(...args), -})); - -const mockUsePendingMerklClaim = jest.fn(() => ({ - hasPendingClaim: false, -})); -jest.mock( - '../../../Earn/components/MerklRewards/hooks/usePendingMerklClaim', - () => ({ - usePendingMerklClaim: () => mockUsePendingMerklClaim(), - }), -); - -const mockUseMerklRewards = jest.fn((_opts?: unknown) => ({ +const mockUseMerklBonusClaim = jest.fn((_asset?: unknown) => ({ claimableReward: null as string | null, - isLoading: false, + hasPendingClaim: false, + isClaiming: false, + claimRewards: mockClaimRewards, })); jest.mock( - '../../../Earn/components/MerklRewards/hooks/useMerklRewards', + '../../../Earn/components/MerklRewards/hooks/useMerklBonusClaim', () => ({ - useMerklRewards: (...args: [unknown]) => mockUseMerklRewards(...args), - isEligibleForMerklRewards: jest.fn(() => false), + useMerklBonusClaim: (...args: [unknown]) => mockUseMerklBonusClaim(...args), }), ); @@ -192,7 +172,6 @@ jest.mock('../../../Earn/selectors/featureFlags', () => ({ selectIsMusdConversionFlowEnabledFlag: jest.fn(() => false), selectMusdQuickConvertEnabledFlag: jest.fn(() => false), selectMusdConversionPaymentTokensAllowlist: jest.fn(() => ({})), - selectMerklCampaignClaimingEnabledFlag: jest.fn(() => false), })); const mockSelectIsMusdConversionFlowEnabledFlag = @@ -337,9 +316,7 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { isStockToken?: boolean; isStablecoinLendingEnabled?: boolean; earnToken?: Record | null; - isMerklCampaignClaimingEnabled?: boolean; claimableReward?: string | null; - isMerklEligible?: boolean; hasPendingClaim?: boolean; isClaiming?: boolean; } @@ -354,9 +331,7 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { isStockToken = false, isStablecoinLendingEnabled = false, earnToken, - isMerklCampaignClaimingEnabled = false, claimableReward = null, - isMerklEligible = false, hasPendingClaim = false, isClaiming = false, }: PrepareMocksOptions = {}) { @@ -367,17 +342,11 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { isStablecoinLendingEnabled, ); - // Merkl claim mocks - mockUseMerklRewards.mockReturnValue({ + mockUseMerklBonusClaim.mockReturnValue({ claimableReward, - isLoading: false, - }); - (isEligibleForMerklRewards as jest.Mock).mockReturnValue(isMerklEligible); - mockUsePendingMerklClaim.mockReturnValue({ hasPendingClaim }); - mockUseMerklClaim.mockReturnValue({ - claimRewards: mockClaimRewards, + hasPendingClaim, isClaiming, - error: null, + claimRewards: mockClaimRewards, }); // Stock token mocks @@ -440,10 +409,6 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { return isQuickConvertEnabled; } - if (selector === selectMerklCampaignClaimingEnabledFlag) { - return isMerklCampaignClaimingEnabled; - } - const selectorString = selector.toString(); // TokenListItem selectors @@ -1258,7 +1223,7 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { }); describe('Merkl Claim Bonus', () => { - // Use an address that isEligibleForMerklRewards would accept + // Use an address that isTokenEligibleForMerklRewards would accept const claimableAsset = { ...defaultAsset, address: '0x8d652c6d4A8F3Db96Cd866C1a9220B1447F29898', @@ -1271,14 +1236,10 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { isStaked: false, }; - it('shows "Claim bonus" CTA when all conditions are met', () => { + it('shows "Claim bonus" CTA when claimableReward exists and no pending claim', () => { prepareMocks({ asset: claimableAsset, - isMerklCampaignClaimingEnabled: true, claimableReward: '1000000000000000000', - isMerklEligible: true, - hasPendingClaim: false, - isClaiming: false, }); const { getByText } = renderWithProvider( @@ -1293,15 +1254,11 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { expect(getByText(strings('earn.claim_bonus'))).toBeOnTheScreen(); }); - it('hides "Claim bonus" CTA when hasPendingClaim is true', () => { + it('hides "Claim bonus" CTA when claimableReward is null', () => { prepareMocks({ asset: claimableAsset, pricePercentChange1d: 2.0, - isMerklCampaignClaimingEnabled: true, - claimableReward: '1000000000000000000', - isMerklEligible: true, - hasPendingClaim: true, - isClaiming: false, + claimableReward: null, }); const { queryByText, getByText } = renderWithProvider( @@ -1313,7 +1270,6 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { />, ); - // Should fall through to percentage display instead expect(queryByText(strings('earn.claim_bonus'))).toBeNull(); expect(getByText('+2.00%')).toBeOnTheScreen(); }); @@ -1321,9 +1277,7 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { it('calls claimRewards when "Claim bonus" CTA is pressed', async () => { prepareMocks({ asset: claimableAsset, - isMerklCampaignClaimingEnabled: true, claimableReward: '1000000000000000000', - isMerklEligible: true, }); const { getByTestId, getByText } = renderWithProvider( @@ -1349,9 +1303,7 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { it('tracks mUSD Claim Bonus Button Clicked event when claim bonus is pressed', async () => { prepareMocks({ asset: claimableAsset, - isMerklCampaignClaimingEnabled: true, claimableReward: '1000000000000000000', - isMerklEligible: true, }); const { getByTestId } = renderWithProvider( @@ -1394,12 +1346,10 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { expect(mockTrackEvent).toHaveBeenCalledWith({ name: 'mock-built-event' }); }); - it('shows ActivityIndicator instead of text when isClaiming is true', () => { + it('shows Spinner instead of text when isClaiming is true', () => { prepareMocks({ asset: claimableAsset, - isMerklCampaignClaimingEnabled: true, claimableReward: '1000000000000000000', - isMerklEligible: true, isClaiming: true, }); @@ -1412,65 +1362,17 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { />, ); - // "Claim bonus" text should not be rendered expect(queryByText(strings('earn.claim_bonus'))).toBeNull(); - // Spinner should be rendered const { Spinner } = jest.requireActual( '@metamask/design-system-react-native/dist/components/temp-components/Spinner/index.cjs', ); expect(UNSAFE_getByType(Spinner)).toBeTruthy(); }); - it('hides "Claim bonus" when merkl campaign claiming is disabled', () => { - prepareMocks({ - asset: claimableAsset, - pricePercentChange1d: 1.5, - isMerklCampaignClaimingEnabled: false, - claimableReward: '1000000000000000000', - isMerklEligible: true, - }); - - const { queryByText, getByText } = renderWithProvider( - , - ); - - expect(queryByText(strings('earn.claim_bonus'))).toBeNull(); - expect(getByText('+1.50%')).toBeOnTheScreen(); - }); - - it('hides "Claim bonus" when there is no claimable reward', () => { - prepareMocks({ - asset: claimableAsset, - pricePercentChange1d: 3.0, - isMerklCampaignClaimingEnabled: true, - claimableReward: null, - isMerklEligible: true, - }); - - const { queryByText, getByText } = renderWithProvider( - , - ); - - expect(queryByText(strings('earn.claim_bonus'))).toBeNull(); - expect(getByText('+3.00%')).toBeOnTheScreen(); - }); - - it('passes asset to useMerklClaim hook via MerklClaimHandler', () => { + it('passes asset to useMerklBonusClaim hook', () => { prepareMocks({ asset: claimableAsset, - isMerklCampaignClaimingEnabled: true, - isMerklEligible: true, }); renderWithProvider( @@ -1482,31 +1384,7 @@ describe('TokenListItem - Component Rendering Tests for Coverage', () => { />, ); - expect(mockUseMerklClaim).toHaveBeenCalledWith(claimableAsset); - }); - - it('does not mount MerklClaimHandler when asset is not eligible', () => { - prepareMocks({ - asset: undefined, - isMerklEligible: false, - }); - - const emptyAssetKey: FlashListAssetKey = { - address: '0x999', - chainId: '0x1', - isStaked: false, - }; - - renderWithProvider( - , - ); - - expect(mockUseMerklClaim).not.toHaveBeenCalled(); + expect(mockUseMerklBonusClaim).toHaveBeenCalledWith(claimableAsset); }); }); }); diff --git a/app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.tsx b/app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.tsx index 1fe1f77ff72..dc03ac83f4b 100644 --- a/app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.tsx +++ b/app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.tsx @@ -1,6 +1,6 @@ import { Hex } from '@metamask/utils'; import { useNavigation } from '@react-navigation/native'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { StyleSheet, View } from 'react-native'; import { Spinner } from '@metamask/design-system-react-native/dist/components/temp-components/Spinner/index.cjs'; import { useSelector } from 'react-redux'; @@ -26,7 +26,6 @@ import { TokenI } from '../../types'; import { ScamWarningIcon } from './ScamWarningIcon/ScamWarningIcon'; import { FlashListAssetKey } from '../TokenList'; import { - selectMerklCampaignClaimingEnabledFlag, selectMusdQuickConvertEnabledFlag, selectStablecoinLendingEnabledFlag, } from '../../../Earn/selectors/featureFlags'; @@ -55,12 +54,7 @@ import Logger from '../../../../../util/Logger'; import { useNetworkName } from '../../../../Views/confirmations/hooks/useNetworkName'; import { MUSD_EVENTS_CONSTANTS } from '../../../Earn/constants/events'; import { MUSD_CONVERSION_APY, isMusdToken } from '../../../Earn/constants/musd'; -import { isEligibleForMerklRewards } from '../../../Earn/components/MerklRewards/hooks/useMerklRewards'; -import { - MerklClaimHandler, - DEFAULT_MERKL_CLAIM_DATA, - type MerklClaimData, -} from '../../../Earn/components/MerklRewards/hooks/MerklClaimHandler'; +import { useMerklBonusClaim } from '../../../Earn/components/MerklRewards/hooks/useMerklBonusClaim'; import useEarnTokens from '../../../Earn/hooks/useEarnTokens'; import { EARN_EXPERIENCES } from '../../../Earn/constants/experiences'; import { EVENT_LOCATIONS as EARN_EVENT_LOCATIONS } from '../../../Earn/constants/events/earnEvents'; @@ -165,36 +159,11 @@ export const TokenListItem = React.memo( [asset, shouldShowTokenListItemCta], ); - // Check for claimable Merkl rewards - const isMerklCampaignClaimingEnabled = useSelector( - selectMerklCampaignClaimingEnabledFlag, - ); - - const isEligibleForMerkl = useMemo( - () => - asset?.chainId && asset?.address - ? isEligibleForMerklRewards( - asset.chainId as Hex, - asset.address as Hex | undefined, - ) - : false, - [asset?.chainId, asset?.address], - ); - - // Merkl hooks are only mounted for eligible tokens via MerklClaimHandler - // to avoid unnecessary hook overhead for non-eligible tokens - const [merklData, setMerklData] = useState( - DEFAULT_MERKL_CLAIM_DATA, - ); + const merklClaimData = useMerklBonusClaim(asset); + const { claimRewards, claimableReward, hasPendingClaim } = merklClaimData; - const hasClaimableBonus = Boolean( - isMerklCampaignClaimingEnabled && - merklData.claimableReward && - isEligibleForMerkl && - !merklData.hasPendingClaim, - ); + const hasClaimableBonus = !!claimableReward && !hasPendingClaim; - const { claimRewards } = merklData; const handleClaimBonus = useCallback(() => { trackEvent( createEventBuilder(MetaMetricsEvents.MUSD_CLAIM_BONUS_BUTTON_CLICKED) @@ -318,9 +287,11 @@ export const TokenListItem = React.memo( const secondaryBalanceDisplay = useMemo(() => { if (hasClaimableBonus) { return { - text: merklData.isClaiming ? undefined : strings('earn.claim_bonus'), + text: merklClaimData.isClaiming + ? undefined + : strings('earn.claim_bonus'), color: TextColor.Primary, - onPress: merklData.isClaiming ? undefined : handleClaimBonus, + onPress: merklClaimData.isClaiming ? undefined : handleClaimBonus, }; } @@ -372,7 +343,7 @@ export const TokenListItem = React.memo( earnToken?.experience?.type, hasPercentageChange, pricePercentChange1d, - merklData.isClaiming, + merklClaimData.isClaiming, handleClaimBonus, handleConvertToMUSD, handleLendingRedirect, @@ -410,87 +381,78 @@ export const TokenListItem = React.memo( : undefined; return ( - <> - {isEligibleForMerkl && isMerklCampaignClaimingEnabled && ( - - )} - - ) : undefined + : undefined + } + > + + ) : null } > - - ) : null - } - > - - - - {/* - * The name of the token must callback to the symbol - * The reason for this is that the wallet_watchAsset doesn't return the name - * more info: https://docs.metamask.io/guide/rpc-api.html#wallet-watchasset - */} - - - - {asset.name || asset.symbol} - - {label && ( - - )} - - - {renderEarnCta()} - - - { - - {asset.balance} {asset.symbol} - - } - {isStockToken(asset as BridgeToken) && ( - + + + + {/* + * The name of the token must callback to the symbol + * The reason for this is that the wallet_watchAsset doesn't return the name + * more info: https://docs.metamask.io/guide/rpc-api.html#wallet-watchasset + */} + + + + {asset.name || asset.symbol} + + {label && ( + )} + + {renderEarnCta()} + + + { + + {asset.balance} {asset.symbol} + + } + {isStockToken(asset as BridgeToken) && ( + + )} - - - + + + ); }, ); diff --git a/app/components/UI/Tokens/TokenList/TokenListItemV2/TokenListItemV2.test.tsx b/app/components/UI/Tokens/TokenList/TokenListItemV2/TokenListItemV2.test.tsx index 02c71e35a33..613ac343641 100644 --- a/app/components/UI/Tokens/TokenList/TokenListItemV2/TokenListItemV2.test.tsx +++ b/app/components/UI/Tokens/TokenList/TokenListItemV2/TokenListItemV2.test.tsx @@ -27,11 +27,9 @@ import { useMusdConversionTokens } from '../../../Earn/hooks/useMusdConversionTo import { useMusdConversionEligibility } from '../../../Earn/hooks/useMusdConversionEligibility'; import { selectIsMusdConversionFlowEnabledFlag, - selectMerklCampaignClaimingEnabledFlag, selectMusdQuickConvertEnabledFlag, selectStablecoinLendingEnabledFlag, } from '../../../Earn/selectors/featureFlags'; -import { isEligibleForMerklRewards } from '../../../Earn/components/MerklRewards/hooks/useMerklRewards'; import { MUSD_CONVERSION_APY } from '../../../Earn/constants/musd'; import { EARN_EXPERIENCES } from '../../../Earn/constants/experiences'; import { MUSD_CONVERSION_NAVIGATION_OVERRIDE } from '../../../Earn/types/musd.types'; @@ -167,34 +165,16 @@ jest.mock('../../../Stake/hooks/useStakingChain', () => ({ })); const mockClaimRewards = jest.fn(); -const mockUseMerklClaim = jest.fn((_asset?: unknown) => ({ - claimRewards: mockClaimRewards, - isClaiming: false, - error: null, -})); -jest.mock('../../../Earn/components/MerklRewards/hooks/useMerklClaim', () => ({ - useMerklClaim: (...args: [unknown]) => mockUseMerklClaim(...args), -})); - -const mockUsePendingMerklClaim = jest.fn(() => ({ - hasPendingClaim: false, -})); -jest.mock( - '../../../Earn/components/MerklRewards/hooks/usePendingMerklClaim', - () => ({ - usePendingMerklClaim: () => mockUsePendingMerklClaim(), - }), -); - -const mockUseMerklRewards = jest.fn((_opts?: unknown) => ({ +const mockUseMerklBonusClaim = jest.fn((_asset?: unknown) => ({ claimableReward: null as string | null, - isLoading: false, + hasPendingClaim: false, + isClaiming: false, + claimRewards: mockClaimRewards, })); jest.mock( - '../../../Earn/components/MerklRewards/hooks/useMerklRewards', + '../../../Earn/components/MerklRewards/hooks/useMerklBonusClaim', () => ({ - useMerklRewards: (...args: [unknown]) => mockUseMerklRewards(...args), - isEligibleForMerklRewards: jest.fn(() => false), + useMerklBonusClaim: (...args: [unknown]) => mockUseMerklBonusClaim(...args), }), ); @@ -204,7 +184,6 @@ jest.mock('../../../Earn/selectors/featureFlags', () => ({ selectIsMusdConversionFlowEnabledFlag: jest.fn(() => false), selectMusdQuickConvertEnabledFlag: jest.fn(() => false), selectMusdConversionPaymentTokensAllowlist: jest.fn(() => ({})), - selectMerklCampaignClaimingEnabledFlag: jest.fn(() => false), })); const mockSelectIsMusdConversionFlowEnabledFlag = @@ -222,11 +201,6 @@ const mockSelectMusdQuickConvertEnabledFlag = typeof selectMusdQuickConvertEnabledFlag >; -const mockSelectMerklCampaignClaimingEnabledFlag = - selectMerklCampaignClaimingEnabledFlag as jest.MockedFunction< - typeof selectMerklCampaignClaimingEnabledFlag - >; - jest.mock('../../util/deriveBalanceFromAssetMarketDetails', () => ({ deriveBalanceFromAssetMarketDetails: jest.fn(() => ({ balanceFiat: '$100.00', @@ -354,9 +328,7 @@ describe('TokenListItemV2 - Component Rendering Tests for Coverage', () => { isStockToken?: boolean; isStablecoinLendingEnabled?: boolean; earnToken?: Record | null; - isMerklClaimingEnabled?: boolean; claimableReward?: string | null; - isMerklEligible?: boolean; tokenMarketData?: Record>; currencyRatesData?: Record; nativeCurrency?: string; @@ -375,9 +347,7 @@ describe('TokenListItemV2 - Component Rendering Tests for Coverage', () => { isStockToken = false, isStablecoinLendingEnabled = false, earnToken, - isMerklClaimingEnabled = false, claimableReward = null, - isMerklEligible = false, tokenMarketData, currencyRatesData, nativeCurrency, @@ -391,14 +361,12 @@ describe('TokenListItemV2 - Component Rendering Tests for Coverage', () => { mockSelectStablecoinLendingEnabledFlag.mockReturnValue( isStablecoinLendingEnabled ?? false, ); - mockSelectMerklCampaignClaimingEnabledFlag.mockReturnValue( - isMerklClaimingEnabled, - ); - mockUseMerklRewards.mockReturnValue({ + mockUseMerklBonusClaim.mockReturnValue({ claimableReward, - isLoading: false, + hasPendingClaim: false, + isClaiming: false, + claimRewards: mockClaimRewards, }); - (isEligibleForMerklRewards as jest.Mock).mockReturnValue(isMerklEligible); // Stock token mocks mockIsStockToken.mockReturnValue(isStockToken); @@ -454,10 +422,6 @@ describe('TokenListItemV2 - Component Rendering Tests for Coverage', () => { return isQuickConvertEnabled; } - if (selector === selectMerklCampaignClaimingEnabledFlag) { - return isMerklClaimingEnabled; - } - if (selector === selectTokenMarketData) { return tokenMarketData ?? {}; } @@ -1244,13 +1208,11 @@ describe('TokenListItemV2 - Component Rendering Tests for Coverage', () => { isStaked: false, }; - it('shows "Claim bonus" replacing percentage when all conditions are met', () => { + it('shows "Claim bonus" replacing percentage when claimableReward exists', () => { prepareMocks({ asset: claimableAsset, pricePercentChange1d: 5.0, - isMerklClaimingEnabled: true, claimableReward: '1000000000000000000', - isMerklEligible: true, }); const { getByText, queryByText } = renderWithProvider( @@ -1270,9 +1232,7 @@ describe('TokenListItemV2 - Component Rendering Tests for Coverage', () => { prepareMocks({ asset: claimableAsset, pricePercentChange1d: 5.0, - isMerklClaimingEnabled: true, claimableReward: '1000000000000000000', - isMerklEligible: true, }); const { getByTestId, getByText } = renderWithProvider( @@ -1331,9 +1291,7 @@ describe('TokenListItemV2 - Component Rendering Tests for Coverage', () => { prepareMocks({ asset: claimableAsset, pricePercentChange1d: 5.0, - isMerklClaimingEnabled: true, claimableReward: '1000000000000000000', - isMerklEligible: true, }); const { getByTestId, getByText } = renderWithProvider( @@ -1361,13 +1319,11 @@ describe('TokenListItemV2 - Component Rendering Tests for Coverage', () => { expect(mockClaimRewards).toHaveBeenCalledTimes(1); }); - it('falls back to percentage when merkl claiming is disabled', () => { + it('falls back to percentage when claimableReward is null', () => { prepareMocks({ asset: claimableAsset, pricePercentChange1d: 1.5, - isMerklClaimingEnabled: false, - claimableReward: '1000000000000000000', - isMerklEligible: true, + claimableReward: null, }); const { queryByText, getByText } = renderWithProvider( diff --git a/app/components/UI/Tokens/TokenList/TokenListItemV2/TokenListItemV2.tsx b/app/components/UI/Tokens/TokenList/TokenListItemV2/TokenListItemV2.tsx index 3095a123474..05fce7c5cc9 100644 --- a/app/components/UI/Tokens/TokenList/TokenListItemV2/TokenListItemV2.tsx +++ b/app/components/UI/Tokens/TokenList/TokenListItemV2/TokenListItemV2.tsx @@ -1,6 +1,6 @@ import { CaipAssetType, Hex } from '@metamask/utils'; import { useNavigation } from '@react-navigation/native'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native'; import { Spinner } from '@metamask/design-system-react-native/dist/components/temp-components/Spinner/index.cjs'; import { useSelector } from 'react-redux'; @@ -25,7 +25,6 @@ import { TokenI } from '../../types'; import { ScamWarningIcon } from '../TokenListItem/ScamWarningIcon/ScamWarningIcon'; import { FlashListAssetKey } from '../TokenList'; import { - selectMerklCampaignClaimingEnabledFlag, selectMusdQuickConvertEnabledFlag, selectStablecoinLendingEnabledFlag, } from '../../../Earn/selectors/featureFlags'; @@ -54,12 +53,7 @@ import Logger from '../../../../../util/Logger'; import { useNetworkName } from '../../../../Views/confirmations/hooks/useNetworkName'; import { MUSD_EVENTS_CONSTANTS } from '../../../Earn/constants/events'; import { MUSD_CONVERSION_APY, isMusdToken } from '../../../Earn/constants/musd'; -import { isEligibleForMerklRewards } from '../../../Earn/components/MerklRewards/hooks/useMerklRewards'; -import { - MerklClaimHandler, - DEFAULT_MERKL_CLAIM_DATA, - type MerklClaimData, -} from '../../../Earn/components/MerklRewards/hooks/MerklClaimHandler'; +import { useMerklBonusClaim } from '../../../Earn/components/MerklRewards/hooks/useMerklBonusClaim'; import useEarnTokens from '../../../Earn/hooks/useEarnTokens'; import { EARN_EXPERIENCES } from '../../../Earn/constants/experiences'; import { EVENT_LOCATIONS as EARN_EVENT_LOCATIONS } from '../../../Earn/constants/events/earnEvents'; @@ -215,34 +209,10 @@ export const TokenListItemV2 = React.memo( [asset, shouldShowTokenListItemCta], ); - // Check for claimable Merkl rewards - const isMerklCampaignClaimingEnabled = useSelector( - selectMerklCampaignClaimingEnabledFlag, - ); - - const isEligibleForMerkl = useMemo( - () => - asset?.chainId && asset?.address - ? isEligibleForMerklRewards( - asset.chainId as Hex, - asset.address as Hex | undefined, - ) - : false, - [asset?.chainId, asset?.address], - ); - - // Merkl hooks are only mounted for eligible tokens via MerklClaimHandler - // to avoid unnecessary hook overhead for non-eligible tokens - const [merklData, setMerklData] = useState( - DEFAULT_MERKL_CLAIM_DATA, - ); + const merklClaimData = useMerklBonusClaim(asset); + const { claimRewards, claimableReward, hasPendingClaim } = merklClaimData; - const hasClaimableBonus = Boolean( - isMerklCampaignClaimingEnabled && - merklData.claimableReward && - isEligibleForMerkl && - !merklData.hasPendingClaim, - ); + const hasClaimableBonus = !!claimableReward && !hasPendingClaim; const handleClaimBonus = useCallback(() => { trackEvent( @@ -257,14 +227,14 @@ export const TokenListItemV2 = React.memo( }) .build(), ); - merklData.claimRewards(); + claimRewards(); }, [ trackEvent, createEventBuilder, asset?.chainId, asset?.symbol, networkName, - merklData, + claimRewards, ]); const pricePercentChange1d = useTokenPricePercentageChange(asset); @@ -511,170 +481,165 @@ export const TokenListItemV2 = React.memo( const tokenBalance = `${asset.balance} ${asset.symbol}`; return ( - <> - {isEligibleForMerkl && isMerklCampaignClaimingEnabled && ( - - )} - { - onItemPress?.(asset); - }} - onLongPress={() => { - const onLongPress = - asset.isNative || isMusdToken(asset.address) - ? null - : showRemoveMenu; - onLongPress?.(asset); - }} - style={styles.itemWrapper} - {...generateTestId(Platform, getAssetTestId(asset.symbol))} + { + onItemPress?.(asset); + }} + onLongPress={() => { + const onLongPress = + asset.isNative || isMusdToken(asset.address) + ? null + : showRemoveMenu; + onLongPress?.(asset); + }} + style={styles.itemWrapper} + {...generateTestId(Platform, getAssetTestId(asset.symbol))} + > + {/* Column: 1 - Token logo */} + + ) + } > - {/* Column: 1 - Token logo */} - - ) - } + + + + {/* Column 2*/} + + {/* Row: 1 - Token name, label, earn CTA, stock badge */} + - - - - {/* Column 2*/} - - {/* Row: 1 - Token name, label, earn CTA, stock badge */} - - {/* - * Token name and label - * The name of the token must callback to the symbol - * The reason for this is that the wallet_watchAsset doesn't return the name - * more info: https://docs.metamask.io/guide/rpc-api.html#wallet-watchasset - */} - - - - {asset.name || asset.symbol} - - {label && ( - - )} - - - {renderEarnCta()} - - {isStockToken(asset as BridgeToken) && ( - + {/* + * Token name and label + * The name of the token must callback to the symbol + * The reason for this is that the wallet_watchAsset doesn't return the name + * more info: https://docs.metamask.io/guide/rpc-api.html#wallet-watchasset + */} + + + + {asset.name || asset.symbol} + + {label && ( + )} - {/* Fiat Balance */} - - {fiatBalance === TOKEN_BALANCE_LOADING || - fiatBalance === TOKEN_BALANCE_LOADING_UPPERCASE ? ( - - ) : ( - fiatBalance - )} - - + {renderEarnCta()} - {/* Row: 2 - Token price and percentage change and token balance */} - + )} + + + {/* Fiat Balance */} + - {/* Token price and percentage change — or claim bonus CTA */} - - {merklData.isClaiming ? ( - - ) : ( - <> - {!hasClaimableBonus && ( - - {tokenPriceInFiat - ? addCurrencySymbol( - tokenPriceInFiat, - currentCurrency, - true, - true, - ) - : '-'} - {' • '} - - )} - - + ) : ( + fiatBalance + )} + + + + {/* Row: 2 - Token price and percentage change and token balance */} + + {/* Token price and percentage change — or claim bonus CTA */} + + {merklClaimData.isClaiming ? ( + + ) : ( + <> + {!hasClaimableBonus && ( + - - {secondaryBalanceDisplay.text || '-'} - - - - )} - + {tokenPriceInFiat + ? addCurrencySymbol( + tokenPriceInFiat, + currentCurrency, + true, + true, + ) + : '-'} + {' • '} + + )} - {/* Token balance */} - - - {tokenBalance} - - + + + {secondaryBalanceDisplay.text || '-'} + + + + )} + + + {/* Token balance */} + + + {tokenBalance} + - - {/* Scam warning icon */} - - - + + + {/* Scam warning icon */} + + ); }, ); From c6b2b411a76197a174276cd0c5c5197ce38934c5 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 5 Mar 2026 15:22:52 +0000 Subject: [PATCH 093/131] [skip ci] Bump version number to 3902 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7a6d2fc2384..d39d0f8238b 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.68.0" - versionCode 3885 + versionCode 3902 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 52cc97f4415..90a39190290 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.68.0 - opts: is_expand: false - VERSION_NUMBER: 3885 + VERSION_NUMBER: 3902 - opts: is_expand: false FLASK_VERSION_NAME: 7.68.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3885 + FLASK_VERSION_NUMBER: 3902 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 02862ff3622..0126ee00139 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3885; + CURRENT_PROJECT_VERSION = 3902; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3885; + CURRENT_PROJECT_VERSION = 3902; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3885; + CURRENT_PROJECT_VERSION = 3902; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3885; + CURRENT_PROJECT_VERSION = 3902; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3885; + CURRENT_PROJECT_VERSION = 3902; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3885; + CURRENT_PROJECT_VERSION = 3902; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 99754f166a6655d05502717912cabe87849bf99f Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:40:38 +0000 Subject: [PATCH 094/131] chore(runway): cherry-pick chore: Bump snaps-controllers (#26992) (#27079) - chore: Bump `snaps-controllers` cp-7.67.2 (#26992) ## **Description** Bump `snaps-controllers` to the latest version which includes a mitigation for production issues where the source code of Snaps is unavailable. ## **Changelog** CHANGELOG entry: Fixed an issue with running Snaps ## **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. ## **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 Snaps dependency used at runtime; even a patch bump can affect Snap execution/permissions and should be regression-tested with common Snaps flows. > > **Overview** > Bumps `@metamask/snaps-controllers` from `^18.0.1` to `^18.0.2` (with corresponding `yarn.lock` updates) to pick up the latest fixes. > > Includes a small transitive dependency update within that package (notably `@metamask/json-rpc-engine` to `^10.2.3`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 67b823f187c316717d04e2c63f0ea6e5733b661e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [4b57dcd](https://github.com/MetaMask/metamask-mobile/commit/4b57dcdeacb085c4ce41a33bb9ef3cbc2d26b7d2) Co-authored-by: Frederik Bolding --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index f355fe525da..82858106331 100644 --- a/package.json +++ b/package.json @@ -282,7 +282,7 @@ "@metamask/signature-controller": "^35.0.0", "@metamask/slip44": "^4.2.0", "@metamask/smart-transactions-controller": "^22.6.0", - "@metamask/snaps-controllers": "^18.0.1", + "@metamask/snaps-controllers": "^18.0.2", "@metamask/snaps-execution-environments": "^11.0.0", "@metamask/snaps-rpc-methods": "^14.3.0", "@metamask/snaps-sdk": "^10.4.0", diff --git a/yarn.lock b/yarn.lock index 52d487ddc1f..44503431acb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9746,13 +9746,13 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^18.0.1": - version: 18.0.1 - resolution: "@metamask/snaps-controllers@npm:18.0.1" +"@metamask/snaps-controllers@npm:^18.0.2": + version: 18.0.2 + resolution: "@metamask/snaps-controllers@npm:18.0.2" dependencies: "@metamask/approval-controller": "npm:^8.0.0" "@metamask/base-controller": "npm:^9.0.0" - "@metamask/json-rpc-engine": "npm:^10.2.2" + "@metamask/json-rpc-engine": "npm:^10.2.3" "@metamask/json-rpc-middleware-stream": "npm:^8.0.8" "@metamask/key-tree": "npm:^10.1.1" "@metamask/messenger": "npm:^0.3.0" @@ -9785,7 +9785,7 @@ __metadata: peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/0b9c3f8097cda0621020bde7e63a74a20f7623a9926d2d102942df56f9b9fefbf0900f7a3a17b175f8648b1b93fc373f5c736ee88e517ee624ed6161985742cb + checksum: 10/3d8f88ff926b2918b1632dc9920c94871df85146a66d4d6dbb8fb31ee241c7012e85d86bdf30be47aec97fc2fecf724f5e235f30a1550bedf4084586175e05eb languageName: node linkType: hard @@ -35462,7 +35462,7 @@ __metadata: "@metamask/signature-controller": "npm:^35.0.0" "@metamask/slip44": "npm:^4.2.0" "@metamask/smart-transactions-controller": "npm:^22.6.0" - "@metamask/snaps-controllers": "npm:^18.0.1" + "@metamask/snaps-controllers": "npm:^18.0.2" "@metamask/snaps-execution-environments": "npm:^11.0.0" "@metamask/snaps-rpc-methods": "npm:^14.3.0" "@metamask/snaps-sdk": "npm:^10.4.0" From a7e73d4efbddc5850fd093655fc8a21f5a36310e Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Thu, 5 Mar 2026 19:43:46 +0000 Subject: [PATCH 095/131] [skip ci] Bump version number to 3904 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d39d0f8238b..68646b90243 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.68.0" - versionCode 3902 + versionCode 3904 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders.MM_BRANCH_KEY_TEST = "$System.env.MM_BRANCH_KEY_TEST" diff --git a/bitrise.yml b/bitrise.yml index 90a39190290..99be84e6979 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3519,13 +3519,13 @@ app: VERSION_NAME: 7.68.0 - opts: is_expand: false - VERSION_NUMBER: 3902 + VERSION_NUMBER: 3904 - opts: is_expand: false FLASK_VERSION_NAME: 7.68.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 3902 + FLASK_VERSION_NUMBER: 3904 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 0126ee00139..17bce9c74b8 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -1281,7 +1281,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3902; + CURRENT_PROJECT_VERSION = 3904; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1350,7 +1350,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3902; + CURRENT_PROJECT_VERSION = 3904; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1416,7 +1416,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3902; + CURRENT_PROJECT_VERSION = 3904; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1483,7 +1483,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3902; + CURRENT_PROJECT_VERSION = 3904; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1646,7 +1646,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3902; + CURRENT_PROJECT_VERSION = 3904; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1716,7 +1716,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 3902; + CURRENT_PROJECT_VERSION = 3904; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From fce0e5c81dcd83d1bcfc86dd67d8b2bfe14c235f Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:01:34 +0000 Subject: [PATCH 096/131] chore(runway): cherry-pick fix: Ensure redux-persist-filesystem-storage returns a promise and throws correctly (#26979) (#27080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: Ensure `redux-persist-filesystem-storage` returns a promise and throws correctly cp-7.67.2 (#26979) ## **Description** This PR adjusts our existing patch for `redux-persist-filesystem-storage` to always return a promise for `setItem` so it can be awaited correctly, fixing potential race conditions. This was broken when the patch originally was created by adding brackets to an arrow function and not returning. The PR also fixes a bug where not passing in a callback would cause errors to be swallowed. Additionally, this PR moves our existing patch to use the built-in Yarn patch feature (because creating new patches with `patch-package` seems broken). ## **Changelog** CHANGELOG entry: Improve reliability of persistence ## **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. ## **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** > Touches persistence write behavior by changing the patched `redux-persist-filesystem-storage` `setItem` control flow and error propagation, which could affect app startup/migrations if any callers depend on the old callback-only semantics. Also introduces an iOS-only side effect (exclude-from-backup) after writes. > > **Overview** > This PR updates the Yarn patch for `redux-persist-filesystem-storage` so `setItem` **always returns a Promise** that can be awaited, rather than potentially relying on callback chaining. > > It also fixes error handling so write failures are **thrown when no callback is provided** (instead of being swallowed), and adds an optional `isIOS` flag to run `ReactNativeBlobUtil.ios.excludeFromBackupKey(...)` after successful writes on iOS. > > Dependency wiring is switched from a normal semver dependency to Yarn’s built-in `patch:` reference for `redux-persist-filesystem-storage@4.2.0`, with corresponding `yarn.lock` updates. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 192a5c846d0e01487929a506d71542523b2a96cf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [02ac109](https://github.com/MetaMask/metamask-mobile/commit/02ac109fb36974ecfe3dbbdecc15b529bc2a8d17) Co-authored-by: Frederik Bolding --- ...esystem-storage-npm-4.2.0-3a6fff24ab.patch | 28 +++++++++++-------- package.json | 2 +- yarn.lock | 13 +++++++-- 3 files changed, 29 insertions(+), 14 deletions(-) rename patches/redux-persist-filesystem-storage+4.2.0.patch => .yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch (56%) diff --git a/patches/redux-persist-filesystem-storage+4.2.0.patch b/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch similarity index 56% rename from patches/redux-persist-filesystem-storage+4.2.0.patch rename to .yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch index 37aa4a293c5..dda65f1830d 100644 --- a/patches/redux-persist-filesystem-storage+4.2.0.patch +++ b/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch @@ -1,7 +1,7 @@ -diff --git a/node_modules/redux-persist-filesystem-storage/index.d.ts b/node_modules/redux-persist-filesystem-storage/index.d.ts -index b0caa94..76b0442 100644 ---- a/node_modules/redux-persist-filesystem-storage/index.d.ts -+++ b/node_modules/redux-persist-filesystem-storage/index.d.ts +diff --git a/index.d.ts b/index.d.ts +index b0caa94ceaa9afaa6c112947a328887e580f76a2..76b0442a7367f39d8ae9300825815edda5b02c44 100644 +--- a/index.d.ts ++++ b/index.d.ts @@ -12,6 +12,7 @@ declare module 'redux-persist-filesystem-storage' { setItem: ( key: string, @@ -10,24 +10,30 @@ index b0caa94..76b0442 100644 callback?: (error?: Error) => void, ) => Promise -diff --git a/node_modules/redux-persist-filesystem-storage/index.js b/node_modules/redux-persist-filesystem-storage/index.js -index d69afb6..0ca3a25 100644 ---- a/node_modules/redux-persist-filesystem-storage/index.js -+++ b/node_modules/redux-persist-filesystem-storage/index.js -@@ -41,11 +41,14 @@ const FilesystemStorage = { +diff --git a/index.js b/index.js +index d69afb678b3d06760ad59831457cfd5c51fdb89b..8d5ecb060a91f137c865ed21f891b640d3cc65fe 100644 +--- a/index.js ++++ b/index.js +@@ -41,11 +41,19 @@ const FilesystemStorage = { onStorageReady = onStorageReadyFactory(options.storagePath); }, - setItem: (key: string, value: string, callback?: (error: ?Error) => void) => +- ReactNativeBlobUtil.fs + setItem: (key: string, value: string, isIOS: boolean = false, callback?: (error: ?Error) => void) => { - ReactNativeBlobUtil.fs ++ return ReactNativeBlobUtil.fs .writeFile(pathForKey(key), value, options.encoding) - .then(() => callback && callback()) - .catch(error => callback && callback(error)), + .then(() => { + if (isIOS) ReactNativeBlobUtil.ios.excludeFromBackupKey(pathForKey(key)); + callback && callback(); -+ }).catch(error => callback && callback(error)); ++ }).catch((error) => { ++ if (!callback) { ++ throw error; ++ } ++ callback(error); ++ }); + }, getItem: onStorageReady( diff --git a/package.json b/package.json index 82858106331..01377135d37 100644 --- a/package.json +++ b/package.json @@ -476,7 +476,7 @@ "redux": "^4.2.1", "redux-mock-store": "1.5.4", "redux-persist": "6.0.0", - "redux-persist-filesystem-storage": "^4.2.0", + "redux-persist-filesystem-storage": "patch:redux-persist-filesystem-storage@npm%3A4.2.0#~/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch", "redux-saga": "^1.3.0", "redux-thunk": "^2.4.2", "reselect": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index 44503431acb..ce7001c5c99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35767,7 +35767,7 @@ __metadata: redux-devtools-expo-dev-plugin: "npm:^1.0.0" redux-mock-store: "npm:1.5.4" redux-persist: "npm:6.0.0" - redux-persist-filesystem-storage: "npm:^4.2.0" + redux-persist-filesystem-storage: "patch:redux-persist-filesystem-storage@npm%3A4.2.0#~/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch" redux-saga: "npm:^1.3.0" redux-saga-test-plan: "npm:^4.0.6" redux-thunk: "npm:^2.4.2" @@ -41509,7 +41509,7 @@ __metadata: languageName: node linkType: hard -"redux-persist-filesystem-storage@npm:^4.2.0": +"redux-persist-filesystem-storage@npm:4.2.0": version: 4.2.0 resolution: "redux-persist-filesystem-storage@npm:4.2.0" dependencies: @@ -41518,6 +41518,15 @@ __metadata: languageName: node linkType: hard +"redux-persist-filesystem-storage@patch:redux-persist-filesystem-storage@npm%3A4.2.0#~/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch": + version: 4.2.0 + resolution: "redux-persist-filesystem-storage@patch:redux-persist-filesystem-storage@npm%3A4.2.0#~/.yarn/patches/redux-persist-filesystem-storage-npm-4.2.0-3a6fff24ab.patch::version=4.2.0&hash=4dfd27" + dependencies: + react-native-blob-util: "npm:^0.18.0" + checksum: 10/fa556e2d1784a5e664e2e7024fa2255b08334e0dacf8993acca676cb912ad82c0f8ef3ba9ec2597d455f9dded83acf3343cc0a66d4e2fc14486e31dd9efe6def + languageName: node + linkType: hard + "redux-persist@npm:6.0.0": version: 6.0.0 resolution: "redux-persist@npm:6.0.0" From d35f89bccad683bfd1c5e0163273e8c822151b50 Mon Sep 17 00:00:00 2001 From: "runway-github[bot]" <73448015+runway-github[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:12:01 +0000 Subject: [PATCH 097/131] chore(runway): cherry-pick 86cb8a3 (#27081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - feat: MUSD-379 fixed off center buy/get mUSD button in Primary conversion CTA (#27015) ## **Description** Fixes off center "Buy/Get mUSD" button for primary mUSD conversion CTA ## **Changelog** CHANGELOG entry: fixed off center "Buy/Get mUSD" button for primary mUSD conversion CTA ## **Related issues** Fixes: [MUSD-379: Get mUSD CTA button off center in long token list](https://consensyssoftware.atlassian.net/browse/MUSD-379) ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** image ### **After** image ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Low risk UI-only tweak that applies an existing `styles.button` rule to the CTA `Button` to correct alignment; no business logic or data flow changes. > > **Overview** > Fixes the primary mUSD conversion asset-list CTA layout by applying `styles.button` to the `Button`, centering the “Buy/Get mUSD” button within the row for long token lists. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7b9c21596891414757b6c49ab5f8017a89dee4a9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). [86cb8a3](https://github.com/MetaMask/metamask-mobile/commit/86cb8a381441ec80b6b12eec827ec8878790eb05) Co-authored-by: Matthew Grainger <46547583+Matt561@users.noreply.github.com> --- .../UI/Earn/components/Musd/MusdConversionAssetListCta/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/UI/Earn/components/Musd/MusdConversionAssetListCta/index.tsx b/app/components/UI/Earn/components/Musd/MusdConversionAssetListCta/index.tsx index d31e4c240c0..6a9305e9df8 100644 --- a/app/components/UI/Earn/components/Musd/MusdConversionAssetListCta/index.tsx +++ b/app/components/UI/Earn/components/Musd/MusdConversionAssetListCta/index.tsx @@ -205,6 +205,7 @@ const MusdConversionAssetListCta = () => { + )} + + + ); +}; + +CashGetMusdEmptyState.displayName = 'CashGetMusdEmptyState'; + +export default CashGetMusdEmptyState; +export { CashGetMusdEmptyStateSelectors }; diff --git a/app/components/Views/Homepage/Sections/Cash/CashSection.test.tsx b/app/components/Views/Homepage/Sections/Cash/CashSection.test.tsx new file mode 100644 index 00000000000..52f455181a9 --- /dev/null +++ b/app/components/Views/Homepage/Sections/Cash/CashSection.test.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { screen, fireEvent } from '@testing-library/react-native'; +import renderWithProvider from '../../../../../util/test/renderWithProvider'; +import CashSection from './CashSection'; +import Routes from '../../../../../constants/navigation/Routes'; + +const mockNavigate = jest.fn(); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: mockNavigate, + }), +})); + +jest.mock('../../../../UI/Earn/selectors/featureFlags', () => ({ + selectIsMusdConversionFlowEnabledFlag: jest.fn(() => true), +})); + +const mockUseMusdConversionEligibility = jest.fn(() => ({ isEligible: true })); +jest.mock('../../../../UI/Earn/hooks/useMusdConversionEligibility', () => ({ + useMusdConversionEligibility: () => mockUseMusdConversionEligibility(), +})); + +const mockUseMusdBalance = jest.fn(() => ({ + hasMusdBalanceOnAnyChain: false, + tokenBalanceAggregated: '0', + fiatBalanceAggregatedFormatted: '$0.00', +})); +jest.mock('../../../../UI/Earn/hooks/useMusdBalance', () => ({ + useMusdBalance: () => mockUseMusdBalance(), +})); + +jest.mock('../../hooks/useHomeViewedEvent', () => ({ + __esModule: true, + default: jest.fn(), + HomeSectionNames: { + CASH: 'cash', + TOKENS: 'tokens', + PERPS: 'perps', + DEFI: 'defi', + PREDICT: 'predict', + NFTS: 'nfts', + }, +})); + +jest.mock('./MusdAggregatedRow', () => { + const { Text } = jest.requireActual('react-native'); + const ReactActual = jest.requireActual('react'); + return { + __esModule: true, + default: () => ReactActual.createElement(Text, null, 'MusdAggregatedRow'), + }; +}); + +jest.mock('./CashGetMusdEmptyState', () => { + const { Text, View } = jest.requireActual('react-native'); + const ReactActual = jest.requireActual('react'); + return { + __esModule: true, + default: () => + ReactActual.createElement( + View, + { testID: 'cash-get-musd-empty-state' }, + ReactActual.createElement(Text, null, 'Get mUSD'), + ), + }; +}); + +describe('CashSection', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest + .requireMock('../../../../UI/Earn/selectors/featureFlags') + .selectIsMusdConversionFlowEnabledFlag.mockReturnValue(true); + mockUseMusdConversionEligibility.mockReturnValue({ isEligible: true }); + mockUseMusdBalance.mockReturnValue({ + hasMusdBalanceOnAnyChain: false, + tokenBalanceAggregated: '0', + fiatBalanceAggregatedFormatted: '$0.00', + }); + }); + + it('returns null when mUSD conversion is disabled', () => { + jest + .requireMock('../../../../UI/Earn/selectors/featureFlags') + .selectIsMusdConversionFlowEnabledFlag.mockReturnValue(false); + + const { queryByText } = renderWithProvider( + , + ); + + expect(queryByText('Cash')).toBeNull(); + }); + + it('returns null when geo is ineligible', () => { + mockUseMusdConversionEligibility.mockReturnValue({ isEligible: false }); + + const { queryByText } = renderWithProvider( + , + ); + + expect(queryByText('Cash')).toBeNull(); + }); + + it('renders Cash title when enabled', () => { + renderWithProvider( + , + ); + + expect(screen.getByText('Cash')).toBeOnTheScreen(); + }); + + it('navigates to CASH_TOKENS_FULL_VIEW when section header is pressed', () => { + renderWithProvider( + , + ); + + fireEvent.press(screen.getByText('Cash')); + + expect(mockNavigate).toHaveBeenCalledWith( + Routes.WALLET.CASH_TOKENS_FULL_VIEW, + ); + }); + + it('shows Get mUSD empty state when user has no mUSD balance', () => { + renderWithProvider( + , + ); + + expect(screen.getByTestId('cash-get-musd-empty-state')).toBeOnTheScreen(); + expect(screen.getByText('Get mUSD')).toBeOnTheScreen(); + }); + + it('renders MusdAggregatedRow when user has mUSD balance', () => { + mockUseMusdBalance.mockReturnValue({ + hasMusdBalanceOnAnyChain: true, + tokenBalanceAggregated: '1800', + fiatBalanceAggregatedFormatted: '$1,800.00', + }); + + renderWithProvider( + , + ); + + expect(screen.getByText('MusdAggregatedRow')).toBeOnTheScreen(); + }); +}); diff --git a/app/components/Views/Homepage/Sections/Cash/CashSection.tsx b/app/components/Views/Homepage/Sections/Cash/CashSection.tsx new file mode 100644 index 00000000000..eb388d3d792 --- /dev/null +++ b/app/components/Views/Homepage/Sections/Cash/CashSection.tsx @@ -0,0 +1,88 @@ +import React, { useCallback, useRef } from 'react'; +import { View } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { useSelector } from 'react-redux'; +import { Box } from '@metamask/design-system-react-native'; +import SectionHeader from '../../../../../component-library/components-temp/SectionHeader'; +import SectionRow from '../../components/SectionRow'; +import Routes from '../../../../../constants/navigation/Routes'; +import { strings } from '../../../../../../locales/i18n'; +import useHomeViewedEvent, { + HomeSectionNames, +} from '../../hooks/useHomeViewedEvent'; +import { selectIsMusdConversionFlowEnabledFlag } from '../../../../UI/Earn/selectors/featureFlags'; +import { useMusdConversionEligibility } from '../../../../UI/Earn/hooks/useMusdConversionEligibility'; +import { useMusdBalance } from '../../../../UI/Earn/hooks/useMusdBalance'; +import MusdAggregatedRow from './MusdAggregatedRow'; + +import CashGetMusdEmptyState from './CashGetMusdEmptyState'; +import Logger from '../../../../../util/Logger'; + +interface CashSectionProps { + sectionIndex: number; + totalSectionsLoaded: number; +} + +/** + * CashSection - Displays mUSD (MetaMask USD) as the first homepage section. + * Shows aggregated mUSD balance across supported networks and optional "Claim bonus". + * Section header navigates to the Cash token list page (mUSD-only, per network). + */ +const CashSection = ({ + sectionIndex, + totalSectionsLoaded, +}: CashSectionProps) => { + const sectionViewRef = useRef(null); + const navigation = useNavigation(); + const isMusdConversionEnabled = useSelector( + selectIsMusdConversionFlowEnabledFlag, + ); + const { isEligible: isGeoEligible } = useMusdConversionEligibility(); + const { hasMusdBalanceOnAnyChain } = useMusdBalance(); + + const isCashSectionEnabled = isMusdConversionEnabled && isGeoEligible; + + const handleViewCashTokens = useCallback(() => { + navigation.navigate(Routes.WALLET.CASH_TOKENS_FULL_VIEW as never); + }, [navigation]); + + useHomeViewedEvent({ + sectionRef: sectionViewRef, + isLoading: false, + sectionName: HomeSectionNames.CASH, + sectionIndex, + totalSectionsLoaded, + isEmpty: !hasMusdBalanceOnAnyChain, + itemCount: hasMusdBalanceOnAnyChain ? 1 : 0, + }); + + if (!isCashSectionEnabled) { + Logger.log( + `[CashSection] not rendered flag=${isMusdConversionEnabled} geo=${isGeoEligible} reason=${!isMusdConversionEnabled ? 'flag_off' : 'geo_ineligible'}`, + ); + return null; + } + + const title = strings('homepage.sections.cash'); + + return ( + + + + {!hasMusdBalanceOnAnyChain ? ( + + + + ) : ( + + + + )} + + + ); +}; + +CashSection.displayName = 'CashSection'; + +export default CashSection; diff --git a/app/components/Views/Homepage/Sections/Cash/MusdAggregatedRow.test.tsx b/app/components/Views/Homepage/Sections/Cash/MusdAggregatedRow.test.tsx new file mode 100644 index 00000000000..aead65d138c --- /dev/null +++ b/app/components/Views/Homepage/Sections/Cash/MusdAggregatedRow.test.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react-native'; +import renderWithProvider from '../../../../../util/test/renderWithProvider'; +import MusdAggregatedRow from './MusdAggregatedRow'; + +const mockClaimRewards = jest.fn(); +const mockTrackEvent = jest.fn(); +const mockCreateEventBuilder = jest.fn(() => ({ + addProperties: jest.fn().mockReturnThis(), + build: jest.fn(), +})); + +jest.mock('../../../../UI/Earn/hooks/useMusdBalance', () => ({ + useMusdBalance: () => ({ + tokenBalanceAggregated: '1800.5', + fiatBalanceAggregatedFormatted: '$1,800.50', + }), +})); + +const mockUseMerklBonusClaim = jest.fn(() => ({ + claimableReward: { amount: '10' } as { amount: string } | null, + hasPendingClaim: false, + claimRewards: mockClaimRewards, + isClaiming: false, +})); +jest.mock( + '../../../../UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim', + () => ({ + useMerklBonusClaim: () => mockUseMerklBonusClaim(), + }), +); + +jest.mock('../../../../../selectors/preferencesController', () => ({ + selectPrivacyMode: () => false, +})); + +jest.mock('../../../../Views/confirmations/hooks/useNetworkName', () => ({ + useNetworkName: () => 'Linea Mainnet', +})); + +jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({ + useAnalytics: () => ({ + trackEvent: mockTrackEvent, + createEventBuilder: mockCreateEventBuilder, + }), +})); + +describe('MusdAggregatedRow', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseMerklBonusClaim.mockReturnValue({ + claimableReward: { amount: '10' }, + hasPendingClaim: false, + claimRewards: mockClaimRewards, + isClaiming: false, + }); + }); + + it('renders token name and balances', () => { + renderWithProvider(); + + expect(screen.getByText('MetaMask USD')).toBeOnTheScreen(); + expect(screen.getByText('$1,800.50')).toBeOnTheScreen(); + expect(screen.getByText(/1,800\.5\s*mUSD/)).toBeOnTheScreen(); + }); + + it('renders Claim bonus when claimable and taps call claimRewards and trackEvent', () => { + renderWithProvider(); + + const claimButton = screen.getByText('Claim bonus'); + expect(claimButton).toBeOnTheScreen(); + + fireEvent.press(claimButton); + + expect(mockClaimRewards).toHaveBeenCalled(); + expect(mockTrackEvent).toHaveBeenCalled(); + expect(mockCreateEventBuilder).toHaveBeenCalled(); + }); + + it('has cash-section-musd-row testID', () => { + renderWithProvider(); + expect(screen.getByTestId('cash-section-musd-row')).toBeOnTheScreen(); + }); + + it('shows Spinner when isClaiming is true', () => { + mockUseMerklBonusClaim.mockReturnValue({ + claimableReward: { amount: '10' }, + hasPendingClaim: false, + claimRewards: mockClaimRewards, + isClaiming: true, + }); + + renderWithProvider(); + + expect(screen.getByTestId('cash-section-musd-row')).toBeOnTheScreen(); + expect(screen.queryByText('Claim bonus')).toBeNull(); + }); + + it('shows green "3% bonus" when not claimable', () => { + mockUseMerklBonusClaim.mockReturnValue({ + claimableReward: null, + hasPendingClaim: false, + claimRewards: mockClaimRewards, + isClaiming: false, + }); + + renderWithProvider(); + + expect(screen.queryByText('Claim bonus')).toBeNull(); + expect(screen.getByText('3% bonus')).toBeOnTheScreen(); + }); +}); diff --git a/app/components/Views/Homepage/Sections/Cash/MusdAggregatedRow.tsx b/app/components/Views/Homepage/Sections/Cash/MusdAggregatedRow.tsx new file mode 100644 index 00000000000..2fc9e5ac0de --- /dev/null +++ b/app/components/Views/Homepage/Sections/Cash/MusdAggregatedRow.tsx @@ -0,0 +1,191 @@ +import React, { useCallback } from 'react'; +import { Pressable } from 'react-native'; +import { useTailwind } from '@metamask/design-system-twrnc-preset'; +import { + Box, + Text, + TextVariant, + TextColor, + FontWeight, + BoxFlexDirection, + BoxAlignItems, + BoxJustifyContent, + AvatarToken, + AvatarTokenSize, +} from '@metamask/design-system-react-native'; +import SensitiveText, { + SensitiveTextLength, +} from '../../../../../component-library/components/Texts/SensitiveText'; +import { + TextVariant as CLTextVariant, + TextColor as CLTextColor, +} from '../../../../../component-library/components/Texts/Text/Text.types'; +import AnimatedSpinner, { SpinnerSize } from '../../../../UI/AnimatedSpinner'; +import { useSelector } from 'react-redux'; +import I18n, { strings } from '../../../../../../locales/i18n'; +import { getIntlNumberFormatter } from '../../../../../util/intl'; +import { + MUSD_CONVERSION_APY, + MUSD_TOKEN, + MUSD_TOKEN_ADDRESS, +} from '../../../../UI/Earn/constants/musd'; +import { MUSD_EVENTS_CONSTANTS } from '../../../../UI/Earn/constants/events'; +import { useNetworkName } from '../../../../Views/confirmations/hooks/useNetworkName'; +import type { Hex } from '@metamask/utils'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { useMusdBalance } from '../../../../UI/Earn/hooks/useMusdBalance'; +import { useMerklBonusClaim } from '../../../../UI/Earn/components/MerklRewards/hooks/useMerklBonusClaim'; +import { TokenI } from '../../../../UI/Tokens/types'; +import { selectPrivacyMode } from '../../../../../selectors/preferencesController'; +import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics'; +import { MetaMetricsEvents } from '../../../../../core/Analytics'; +import { MUSD_MAINNET_ASSET_FOR_DETAILS } from './CashGetMusdEmptyState.constants'; +import NavigationService from '../../../../../core/NavigationService'; +import { TokenDetailsSource } from '../../../../UI/TokenDetails/constants/constants'; + +/** + * Minimal mUSD asset for useMerklBonusClaim (claim runs on Linea). + * Only chainId and address are required for the claim flow. + */ +const LINEA_MUSD_ASSET: TokenI = { + chainId: CHAIN_IDS.LINEA_MAINNET as string, + address: MUSD_TOKEN_ADDRESS, + symbol: MUSD_TOKEN.symbol, + name: MUSD_TOKEN.name, + decimals: MUSD_TOKEN.decimals, + image: '', + balance: '0', + isETH: false, + logo: undefined, +}; + +const MusdAggregatedRow = () => { + const tw = useTailwind(); + const privacyMode = useSelector(selectPrivacyMode); + const { tokenBalanceAggregated, fiatBalanceAggregatedFormatted } = + useMusdBalance(); + const { claimableReward, hasPendingClaim, claimRewards, isClaiming } = + useMerklBonusClaim(LINEA_MUSD_ASSET); + const { trackEvent, createEventBuilder } = useAnalytics(); + const networkName = useNetworkName(LINEA_MUSD_ASSET.chainId as Hex); + + const hasClaimableBonus = Boolean(claimableReward) && !hasPendingClaim; + + const handleClaimBonus = useCallback(() => { + trackEvent( + createEventBuilder(MetaMetricsEvents.MUSD_CLAIM_BONUS_BUTTON_CLICKED) + .addProperties({ + action_type: 'claim_bonus', + button_text: strings('earn.claim_bonus'), + location: MUSD_EVENTS_CONSTANTS.EVENT_LOCATIONS.HOME_CASH_SECTION, + network_chain_id: LINEA_MUSD_ASSET.chainId, + network_name: networkName ?? undefined, + asset_symbol: LINEA_MUSD_ASSET.symbol, + }) + .build(), + ); + claimRewards(); + }, [trackEvent, createEventBuilder, networkName, claimRewards]); + + const handleTokenRowPress = useCallback(() => { + NavigationService.navigation.navigate( + 'Asset' as never, + { + ...MUSD_MAINNET_ASSET_FOR_DETAILS, + source: TokenDetailsSource.MobileTokenListPage, + } as never, + ); + }, []); + + const tokenBalanceDisplay = `${getIntlNumberFormatter(I18n.locale, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }).format(Number(tokenBalanceAggregated))} ${MUSD_TOKEN.symbol}`; + + return ( + + tw.style('flex-row items-center py-1', pressed && 'opacity-80') + } + testID="cash-section-musd-row" + onPress={handleTokenRowPress} + > + + + + + + {MUSD_TOKEN.name} + + + {fiatBalanceAggregatedFormatted} + + + + {isClaiming ? ( + + ) : hasClaimableBonus ? ( + + + {strings('earn.claim_bonus')} + + + ) : ( + + {strings('earn.musd_conversion.percentage_bonus', { + percentage: MUSD_CONVERSION_APY, + })} + + )} + + {tokenBalanceDisplay} + + + + + + ); +}; + +export default MusdAggregatedRow; diff --git a/app/components/Views/Homepage/Sections/Cash/index.ts b/app/components/Views/Homepage/Sections/Cash/index.ts new file mode 100644 index 00000000000..53f4fa56a27 --- /dev/null +++ b/app/components/Views/Homepage/Sections/Cash/index.ts @@ -0,0 +1,2 @@ +export { default as CashSection } from './CashSection'; +export { default as MusdAggregatedRow } from './MusdAggregatedRow'; diff --git a/app/components/Views/Homepage/Sections/Tokens/TokensSection.test.tsx b/app/components/Views/Homepage/Sections/Tokens/TokensSection.test.tsx index d0a76d1d8ae..af873319c30 100644 --- a/app/components/Views/Homepage/Sections/Tokens/TokensSection.test.tsx +++ b/app/components/Views/Homepage/Sections/Tokens/TokensSection.test.tsx @@ -78,6 +78,15 @@ jest.mock('../../../../../selectors/networkController', () => ({ selectNetworkConfigurations: jest.fn(() => mockNetworkConfigurations), })); +jest.mock('../../../../UI/Earn/selectors/featureFlags', () => ({ + selectIsMusdConversionFlowEnabledFlag: jest.fn(() => false), +})); + +const mockUseMusdConversionEligibility = jest.fn(() => ({ isEligible: false })); +jest.mock('../../../../UI/Earn/hooks/useMusdConversionEligibility', () => ({ + useMusdConversionEligibility: () => mockUseMusdConversionEligibility(), +})); + const mockRefreshTokens = jest.fn().mockResolvedValue(undefined); jest.mock('../../../../UI/Tokens/util/refreshTokens', () => ({ refreshTokens: (...args: unknown[]) => mockRefreshTokens(...args), @@ -349,6 +358,11 @@ describe('TokensSection', () => { error: null, refetch: jest.fn(), }); + // Cash section disabled by default so TokensSection shows all tokens (including mUSD) unless a test opts in. + jest + .requireMock('../../../../UI/Earn/selectors/featureFlags') + .selectIsMusdConversionFlowEnabledFlag.mockReturnValue(false); + mockUseMusdConversionEligibility.mockReturnValue({ isEligible: false }); }); it('renders section title for account with balance', () => { @@ -460,6 +474,30 @@ describe('TokensSection', () => { expect(screen.queryByTestId('token-item-0xtoken7')).toBeNull(); }); + it('filters out mUSD from displayed tokens (mUSD is shown only in Cash section)', () => { + const MUSD_ADDRESS = '0xaca92e438df0b2401ff60da7e4337b687a2435da'; + jest + .requireMock('../../../../UI/Earn/selectors/featureFlags') + .selectIsMusdConversionFlowEnabledFlag.mockReturnValue(true); + mockUseMusdConversionEligibility.mockReturnValue({ isEligible: true }); + mockUseIsZeroBalanceAccount.mockReturnValue(false); + mockSortedTokenKeys.mockReturnValue([ + { chainId: '0x1', address: MUSD_ADDRESS, isStaked: false }, + { chainId: '0x1', address: '0xtoken1', isStaked: false }, + ]); + + renderWithProvider( + , + ); + + expect(screen.queryByTestId(`token-item-${MUSD_ADDRESS}`)).toBeNull(); + expect(screen.queryByTestId(`token-item-v2-${MUSD_ADDRESS}`)).toBeNull(); + const otherToken = + screen.queryByTestId('token-item-0xtoken1') ?? + screen.queryByTestId('token-item-v2-0xtoken1'); + expect(otherToken).toBeOnTheScreen(); + }); + it('navigates to tokens full view on title press', () => { mockUseIsZeroBalanceAccount.mockReturnValue(false); diff --git a/app/components/Views/Homepage/Sections/Tokens/TokensSection.tsx b/app/components/Views/Homepage/Sections/Tokens/TokensSection.tsx index 9fd10a20e70..0f57ac50976 100644 --- a/app/components/Views/Homepage/Sections/Tokens/TokensSection.tsx +++ b/app/components/Views/Homepage/Sections/Tokens/TokensSection.tsx @@ -41,6 +41,9 @@ import useHomeViewedEvent, { HomeSectionNames, } from '../../hooks/useHomeViewedEvent'; import { useMusdCtaVisibility } from '../../../../UI/Earn/hooks/useMusdCtaVisibility'; +import { isMusdToken } from '../../../../UI/Earn/constants/musd'; +import { selectIsMusdConversionFlowEnabledFlag } from '../../../../UI/Earn/selectors/featureFlags'; +import { useMusdConversionEligibility } from '../../../../UI/Earn/hooks/useMusdConversionEligibility'; interface TokensSectionProps { sectionIndex: number; @@ -117,21 +120,37 @@ const TokensSection = forwardRef( } }, [selectedAccountId]); + const isMusdConversionFlowEnabled = useSelector( + selectIsMusdConversionFlowEnabledFlag, + ); + const { isEligible: isGeoEligible } = useMusdConversionEligibility(); + const isCashSectionEnabled = isMusdConversionFlowEnabled && isGeoEligible; + const title = strings('homepage.sections.tokens'); + // Only exclude mUSD when Cash section is enabled (then mUSD is shown there). Otherwise include all. const displayTokenKeys = useMemo( - () => sortedTokenKeys.slice(0, MAX_TOKENS_DISPLAYED), - [sortedTokenKeys], + () => + sortedTokenKeys + .filter((key) => + isCashSectionEnabled ? !isMusdToken(key.address) : true, + ) + .slice(0, MAX_TOKENS_DISPLAYED), + [sortedTokenKeys, isCashSectionEnabled], ); // Show error when an explicit refresh failed, or when balance data has loaded // and the account has balance but the selector returned no tokens (controllers // failed to load data). The accountGroupBalance null-check prevents a false // positive on cold start or for legitimately empty token lists. + // When Cash section is enabled, displayTokenKeys can be empty because we filter + // out mUSD (shown in Cash section); do not treat "balance but no non-mUSD tokens" + // as an error. const hasBalanceButNoTokens = accountGroupBalance != null && accountGroupBalance.totalBalanceInUserCurrency > 0 && - displayTokenKeys.length === 0; + displayTokenKeys.length === 0 && + (!isCashSectionEnabled || sortedTokenKeys.length === 0); const showTokensError = hasTokensError || hasBalanceButNoTokens; const refresh = useCallback(async () => { diff --git a/app/components/Views/Homepage/Sections/Tokens/hooks/usePopularTokens.test.ts b/app/components/Views/Homepage/Sections/Tokens/hooks/usePopularTokens.test.ts index 6604909280a..a58319dbd84 100644 --- a/app/components/Views/Homepage/Sections/Tokens/hooks/usePopularTokens.test.ts +++ b/app/components/Views/Homepage/Sections/Tokens/hooks/usePopularTokens.test.ts @@ -20,6 +20,8 @@ jest.mock('../../../../../../selectors/currencyRateController', () => ({ jest.mock('../../../../../UI/Earn/constants/musd', () => ({ MUSD_CONVERSION_APY: 3, MUSD_TOKEN_ADDRESS: '0xaca92e438df0b2401ff60da7e4337b687a2435da', + isMusdToken: (address?: string) => + address?.toLowerCase() === '0xaca92e438df0b2401ff60da7e4337b687a2435da', })); // Mock locales to avoid deep import chain issues @@ -31,6 +33,15 @@ jest.mock('../../../../../../../locales/i18n', () => ({ ), })); +const mockUseMusdConversionEligibility = jest.fn(() => ({ isEligible: false })); +jest.mock('../../../../../UI/Earn/hooks/useMusdConversionEligibility', () => ({ + useMusdConversionEligibility: () => mockUseMusdConversionEligibility(), +})); + +jest.mock('../../../../../UI/Earn/selectors/featureFlags', () => ({ + selectIsMusdConversionFlowEnabledFlag: jest.fn(), +})); + const mockUseSelector = useSelector as jest.MockedFunction; const mockHandleFetch = handleFetch as jest.MockedFunction; @@ -193,4 +204,27 @@ describe('usePopularTokens', () => { expect(mockHandleFetch).toHaveBeenCalledTimes(2); }); + + it('excludes mUSD from tokens when Cash section is enabled', async () => { + mockHandleFetch.mockResolvedValue({}); + mockUseMusdConversionEligibility.mockReturnValue({ isEligible: true }); + // First call: selectCurrentCurrency → 'usd'; second: selectIsMusdConversionFlowEnabledFlag → true. + // Later calls (re-renders) keep Cash enabled so mUSD stays filtered. + mockUseSelector + .mockReturnValueOnce('usd') + .mockReturnValueOnce(true) + .mockReturnValue(true); + + const { result } = renderHook(() => usePopularTokens()); + + await waitFor(() => { + expect(result.current.isInitialLoading).toBe(false); + }); + + expect(result.current.tokens).toHaveLength(4); + expect( + result.current.tokens.find((t) => t.symbol === 'mUSD'), + ).toBeUndefined(); + expect(result.current.tokens[0].name).toBe('Ethereum'); + }); }); diff --git a/app/components/Views/Homepage/Sections/Tokens/hooks/usePopularTokens.ts b/app/components/Views/Homepage/Sections/Tokens/hooks/usePopularTokens.ts index a5d257573f0..15ed0abb5ba 100644 --- a/app/components/Views/Homepage/Sections/Tokens/hooks/usePopularTokens.ts +++ b/app/components/Views/Homepage/Sections/Tokens/hooks/usePopularTokens.ts @@ -6,7 +6,10 @@ import { strings } from '../../../../../../../locales/i18n'; import { MUSD_CONVERSION_APY, MUSD_TOKEN_ADDRESS, + isMusdToken, } from '../../../../../UI/Earn/constants/musd'; +import { selectIsMusdConversionFlowEnabledFlag } from '../../../../../UI/Earn/selectors/featureFlags'; +import { useMusdConversionEligibility } from '../../../../../UI/Earn/hooks/useMusdConversionEligibility'; /** * Popular token metadata with CAIP-19 asset IDs @@ -115,6 +118,11 @@ const getTokenDescription = ( */ export const usePopularTokens = () => { const currentCurrency = useSelector(selectCurrentCurrency); + const isMusdConversionFlowEnabled = useSelector( + selectIsMusdConversionFlowEnabledFlag, + ); + const { isEligible: isGeoEligible } = useMusdConversionEligibility(); + const isCashSectionEnabled = isMusdConversionFlowEnabled && isGeoEligible; const [rawTokens, setRawTokens] = useState< { assetId: string; @@ -209,25 +217,28 @@ export const usePopularTokens = () => { [], ); - // Add descriptions dynamically (localized strings must be called within component) - const tokens: PopularToken[] = useMemo( - () => - rawTokens.map((token) => { - const baseToken = POPULAR_TOKENS.find( - (t) => t.assetId === token.assetId, - ); - return { - assetId: token.assetId, - name: token.name, - symbol: token.symbol, - iconUrl: token.iconUrl, - price: token.price, - priceChange1d: token.priceChange1d, - description: baseToken ? getTokenDescription(baseToken) : undefined, - }; - }), - [rawTokens], - ); + // Add descriptions dynamically (localized strings must be called within component). + // When Cash section is enabled, exclude mUSD from this list (it is shown in Cash section). + const tokens: PopularToken[] = useMemo(() => { + const mapped = rawTokens.map((token) => { + const baseToken = POPULAR_TOKENS.find((t) => t.assetId === token.assetId); + return { + assetId: token.assetId, + name: token.name, + symbol: token.symbol, + iconUrl: token.iconUrl, + price: token.price, + priceChange1d: token.priceChange1d, + description: baseToken ? getTokenDescription(baseToken) : undefined, + }; + }); + return isCashSectionEnabled + ? mapped.filter((t) => { + const address = t.assetId.split(':').pop(); + return !isMusdToken(address); + }) + : mapped; + }, [rawTokens, isCashSectionEnabled]); return { tokens, diff --git a/app/components/Views/Homepage/hooks/useHomeSessionSummary.ts b/app/components/Views/Homepage/hooks/useHomeSessionSummary.ts index 24008d7649e..0166cfdfd05 100644 --- a/app/components/Views/Homepage/hooks/useHomeSessionSummary.ts +++ b/app/components/Views/Homepage/hooks/useHomeSessionSummary.ts @@ -51,7 +51,6 @@ const useHomeSessionSummary = ({ useFocusEffect( useCallback( () => () => { - // Blur — user is leaving the homepage. Skip if never actually focused. if (visitIdRef.current === 0) return; const sessionTime = Math.round( (Date.now() - sessionStartRef.current) / 1000, diff --git a/app/components/Views/Homepage/hooks/useHomeViewedEvent.ts b/app/components/Views/Homepage/hooks/useHomeViewedEvent.ts index 25ecaa3ce85..26ffeb10d0c 100644 --- a/app/components/Views/Homepage/hooks/useHomeViewedEvent.ts +++ b/app/components/Views/Homepage/hooks/useHomeViewedEvent.ts @@ -5,6 +5,7 @@ import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics'; import { useHomepageScrollContext } from '../context/HomepageScrollContext'; export const HomeSectionNames = { + CASH: 'cash', TOKENS: 'tokens', PERPS: 'perps', DEFI: 'defi', diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 599918d150d..f8c9313f56e 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -261,6 +261,7 @@ const Routes = { TRENDING_TOKENS_FULL_VIEW: 'TrendingTokensFullView', RWA_TOKENS_FULL_VIEW: 'RWATokensFullView', DEFI_FULL_VIEW: 'DeFiFullView', + CASH_TOKENS_FULL_VIEW: 'CashTokensFullView', }, VAULT_RECOVERY: { RESTORE_WALLET: 'RestoreWallet', diff --git a/app/core/NavigationService/types.ts b/app/core/NavigationService/types.ts index ba3ea77092e..358fdeceb31 100644 --- a/app/core/NavigationService/types.ts +++ b/app/core/NavigationService/types.ts @@ -441,6 +441,7 @@ export interface RootStackParamList extends ParamListBase { WalletConnectSessionsView: undefined; NftFullView: undefined; TokensFullView: undefined; + CashTokensFullView: undefined; TrendingTokensFullView: undefined; RWATokensFullView: undefined; diff --git a/app/util/formatFiat.ts b/app/util/formatFiat.ts index 4ad15834320..47d32747c13 100644 --- a/app/util/formatFiat.ts +++ b/app/util/formatFiat.ts @@ -27,6 +27,7 @@ const formatFiat = (fiatAmount: BigNumber, currency?: string) => { result = getIntlNumberFormatter(I18n.locale, { style: 'currency', currency, + currencyDisplay: 'narrowSymbol', minimumFractionDigits: hasDecimals ? 2 : 0, // string is valid parameter for format function // for some reason it gives TS issue @@ -37,8 +38,6 @@ const formatFiat = (fiatAmount: BigNumber, currency?: string) => { result = `${value} ${currency}`; } - result = result.replace('US$', '$'); - return isSmall ? `<${result}` : result; }; diff --git a/locales/languages/en.json b/locales/languages/en.json index 6ff74f21180..09ba3ad066a 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -5922,6 +5922,7 @@ "earn": { "claimable_bonus_tooltip": "The annualized bonus you’ve earned for holding mUSD. Your bonus is claimable daily on Linea.", "earn_a_percentage_bonus": "Earn a {{percentage}}% bonus", + "percentage_bonus": "{{percentage}}% bonus", "claimable_bonus": "Claimable bonus", "claim_bonus": "Claim bonus", "claim_bonus_subtitle": "Bonus will be paid out on {{networkName}}.", @@ -8007,6 +8008,10 @@ }, "homepage": { "sections": { + "cash": "Cash", + + "cash_empty_description": "You don't have any mUSD yet. Convert stablecoins to mUSD from the Cash section on the homepage.", + "cash_empty_description_network_filter": "No mUSD on this network. Switch network to see your mUSD.", "tokens": "Tokens", "perpetuals": "Perpetuals", "predictions": "Predictions",