From 4d1276b859b1d80fce09c675727284f41652c753 Mon Sep 17 00:00:00 2001 From: Priya Date: Thu, 31 Jul 2025 14:59:53 +0200 Subject: [PATCH 01/14] chore: add warning logs for all requests to live server (#17703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR does the following: - Logs are now shown whenever a request is sent to a live server or redirected to the catch-all mock, making it easier to debug unexpected network calls. - A temporary URL and domain allowlist has been added to permit specific requests to reach real servers. The long-term goal is to reduce this list to zero. - Merging this PR will only log a warning for new, non-mocked requests and the tests will not fail yet. This allows teams time to mock missing URLs without blocking CI. What's Next: - Extend the allowlist to include all current live URLs used in existing tests, preserving the current behavior. - Enforce stricter validation in the test setup: throw an error if new tests access live URLs that are not in the allowlist and haven’t been mocked. - Collaborate across teams to fully mock all URLs in the allowlist ## **Changelog** CHANGELOG entry: ## **Related issues** Fixes: [MMQA-805](https://consensyssoftware.atlassian.net/browse/MMQA-805) ## **Manual testing steps** 1. Go to this page... 2. 3. ## **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. [MMQA-805]: https://consensyssoftware.atlassian.net/browse/MMQA-805?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- e2e/api-mocking/mock-e2e-allowlist.js | 28 +++++++++++ e2e/api-mocking/mock-server.js | 70 ++++++++++++++++++++++----- e2e/init.js | 11 +++++ 3 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 e2e/api-mocking/mock-e2e-allowlist.js diff --git a/e2e/api-mocking/mock-e2e-allowlist.js b/e2e/api-mocking/mock-e2e-allowlist.js new file mode 100644 index 000000000000..b67cb2a04995 --- /dev/null +++ b/e2e/api-mocking/mock-e2e-allowlist.js @@ -0,0 +1,28 @@ +// Please do not add any more items to this list. +// This list is temporary and the goal is to reduce it to 0, meaning all requests are mocked in our e2e tests. + +export const ALLOWLISTED_HOSTS = [ + 'localhost', + '127.0.0.1', + '10.0.2.2', // Android emulator host + 'api.tenderly.co', + 'rpc.tenderly.co', +]; + +export const ALLOWLISTED_URLS = [ + // Temporarily allow existing live requests during migration + 'https://client-config.api.cx.metamask.io/v1/flags?client=mobile&distribution=main&environment=dev', + 'https://staking.api.cx.metamask.io/v1/lending/1/markets', + 'https://staking.api.cx.metamask.io/v1/pooled-staking/stakes/1?accounts=0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3', + 'https://staking.api.cx.metamask.io/v1/pooled-staking/eligibility?addresses=0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3', + 'https://staking.api.cx.metamask.io/v1/pooled-staking/vault/1', + 'https://staking.api.cx.metamask.io/v1/lending/markets', + 'https://staking.api.cx.metamask.io/v1/pooled-staking/vault/1/apys?days=365&order=desc', + 'https://staking.api.cx.metamask.io/v1/pooled-staking/vault/1/apys/averages', + 'https://staking.api.cx.metamask.io/v1/lending/positions/0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3', + 'https://mainnet.infura.io/v3/8f4bc0ed77aa4a2c886a4d929754f414', + 'https://min-api.cryptocompare.com/data/pricemulti?fsyms=btc%2Csol&tsyms=usd', + 'https://pulse.walletconnect.org/batch?projectId=e698cc28a9e75eb175ae3c991ac7eb2a&st=events_sdk&sv=js-2.19.2&sp=desktop', + 'https://clients3.google.com/generate_204', + 'https://security-alerts.api.cx.metamask.io/validate/0x539', +]; diff --git a/e2e/api-mocking/mock-server.js b/e2e/api-mocking/mock-server.js index 035f4f948902..ced90555e709 100644 --- a/e2e/api-mocking/mock-server.js +++ b/e2e/api-mocking/mock-server.js @@ -3,6 +3,12 @@ import { getLocal } from 'mockttp'; import portfinder from 'portfinder'; import _ from 'lodash'; import { device } from 'detox'; +import { ALLOWLISTED_HOSTS, ALLOWLISTED_URLS } from './mock-e2e-allowlist.js'; +import { createLogger } from '../framework/logger'; + +const logger = createLogger({ + name: 'MockServer', +}); /** * Utility function to handle direct fetch requests @@ -34,6 +40,36 @@ const handleDirectFetch = async (url, method, headers, requestBody) => { } }; +/** + * Utility function to check if a URL is allowed + * @param {string} url - The URL to check + * @returns {boolean} True if the URL is allowed, false otherwise + */ +const isUrlAllowed = (url) => { + try { + // First check if the exact URL is in the allowed URLs list + if (ALLOWLISTED_URLS.includes(url)) { + return true; + } + + // Then check if the hostname is in the allowed hosts list + const parsedUrl = new URL(url); + const hostname = parsedUrl.hostname; + + return ALLOWLISTED_HOSTS.some((allowedHost) => { + // Support exact match or wildcard subdomains (e.g., "*.example.com") + if (allowedHost.startsWith('*.')) { + const domain = allowedHost.slice(2); + return hostname === domain || hostname.endsWith(`.${domain}`); + } + return hostname === allowedHost; + }); + } catch (error) { + logger.warn('Invalid URL:', url); + return false; + } +}; + /** * Starts the mock server and sets up mock events. * @@ -135,12 +171,19 @@ export const startMockServer = async (events, port) => { }; } - // If no matching mock found, pass through to actual endpoint + // If no matching mock found, check if URL is allowed before passing through const updatedUrl = device.getPlatform() === 'android' ? urlEndpoint.replace('localhost', '127.0.0.1') : urlEndpoint; + // Check if the URL is in the allowed list + if (!isUrlAllowed(updatedUrl)) { + const errorMessage = `Request going to live server: ${updatedUrl}`; + logger.warn(errorMessage); + global.liveServerRequest = new Error(errorMessage); + } + return handleDirectFetch( updatedUrl, method, @@ -149,17 +192,22 @@ export const startMockServer = async (events, port) => { ); }); - // In case any other requests are made, pass them through to the actual endpoint - await mockServer - .forUnmatchedRequest() - .thenCallback(async (request) => - handleDirectFetch( - request.url, - request.method, - request.headers, - await request.body.getText(), - ), + // In case any other requests are made, check allowed list before passing through + await mockServer.forUnmatchedRequest().thenCallback(async (request) => { + // Check if the URL is in the allowed list + if (!isUrlAllowed(request.url)) { + const errorMessage = `Request going to live server: ${request.url}`; + logger.warn(errorMessage); + global.liveServerRequest = new Error(errorMessage); + } + + return handleDirectFetch( + request.url, + request.method, + request.headers, + await request.body.getText(), ); + }); return mockServer; }; diff --git a/e2e/init.js b/e2e/init.js index b191bf8d2222..a68ef30cccff 100644 --- a/e2e/init.js +++ b/e2e/init.js @@ -1,3 +1,5 @@ +/* eslint-env jest */ +import { logger } from './framework'; import Utilities from './utils/Utilities'; /** @@ -10,3 +12,12 @@ beforeAll(async () => { permissions: { notifications: 'YES' }, }); }); + +global.liveServerRequest = null; +afterEach(() => { + if (global.liveServerRequest) { + const err = global.liveServerRequest; + global.liveServerRequest = null; // reset for next test + logger.warn(err); // change this to throw once the allow list is updated + } +}); From 0e52709054c1a540b7cdb38fce50f8001083a28b Mon Sep 17 00:00:00 2001 From: Ganesh Suresh Patra Date: Thu, 31 Jul 2025 18:30:32 +0530 Subject: [PATCH 02/14] Fix/qa bugs (#17750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Bug 1: https://github.com/MetaMask/metamask-mobile/issues/17690 - Bug 2: https://github.com/MetaMask/metamask-mobile/issues/17596 - Bug 3: https://www.notion.so/metamask-consensys/Save-your-SRP-blurry-text-should-be-dark-for-dark-mode-23ef86d67d6880d38c77f1c2a20e651a?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 4: https://www.notion.so/metamask-consensys/Remove-extra-space-after-Learn-how-on-the-wallet-is-ready-screen-23ef86d67d6880178aefe8060287cabd?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 5: https://www.notion.so/metamask-consensys/Remove-Learn-more-on-the-wallet-recovery-screen-in-settings-and-the-R-in-the-title-should-be-low-23ef86d67d6880cca1dbd8d7158e42d7?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 6: https://www.notion.so/metamask-consensys/Remove-white-part-in-graphics-23ff86d67d6880459efec45fee8047e0?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 7: https://www.notion.so/metamask-consensys/Remove-back-button-from-Save-your-SRP-screen-23ff86d67d6880f29d78e0b25bf0897a?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 8: https://www.notion.so/metamask-consensys/Capitalise-G-and-A-in-Google-and-Apple-in-settings-23ff86d67d688043b81fe446fb9c8ba2?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 9: https://www.notion.so/metamask-consensys/Copy-issues-in-Forgot-your-password-screens-23ff86d67d68809b9eaef9e204509ccd?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link ## **Changelog** CHANGELOG entry: ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to the app 2. please follow the given steps in each bug. 3. ## **Screenshots/Recordings** ### **Before** - Bug 1: https://github.com/MetaMask/metamask-mobile/issues/17690 - Bug 2: https://github.com/MetaMask/metamask-mobile/issues/17596 - Bug 3: https://www.notion.so/metamask-consensys/Save-your-SRP-blurry-text-should-be-dark-for-dark-mode-23ef86d67d6880d38c77f1c2a20e651a?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 4: https://www.notion.so/metamask-consensys/Remove-extra-space-after-Learn-how-on-the-wallet-is-ready-screen-23ef86d67d6880178aefe8060287cabd?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 5: https://www.notion.so/metamask-consensys/Remove-Learn-more-on-the-wallet-recovery-screen-in-settings-and-the-R-in-the-title-should-be-low-23ef86d67d6880cca1dbd8d7158e42d7?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 6: https://www.notion.so/metamask-consensys/Remove-white-part-in-graphics-23ff86d67d6880459efec45fee8047e0?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 7: https://www.notion.so/metamask-consensys/Remove-back-button-from-Save-your-SRP-screen-23ff86d67d6880f29d78e0b25bf0897a?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 8: https://www.notion.so/metamask-consensys/Capitalise-G-and-A-in-Google-and-Apple-in-settings-23ff86d67d688043b81fe446fb9c8ba2?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link - Bug 9: https://www.notion.so/metamask-consensys/Copy-issues-in-Forgot-your-password-screens-23ff86d67d68809b9eaef9e204509ccd?v=211f86d67d68803c88d7000cfcf4f021&source=copy_link ### **After** https://github.com/user-attachments/assets/f75f57e9-279e-4b64-a6eb-b9f21b713fa8 Screenshot 2025-07-30 at 4 06
46 PM Screenshot 2025-07-30 at 6 53 44 PM Screenshot 2025-07-30 at 6 57 49 PM Screenshot 2025-07-30 at 7 18 31 PM Screenshot 2025-07-30 at 7 31 15 PM Screenshot 2025-07-30 at 7 46
43 PM Screenshot 2025-07-30 at 7 48 50 PM Screenshot 2025-07-30 at 7 58 19 PM Screenshot 2025-07-30 at 8 02 24 PM Screenshot 2025-07-30 at 8 48 39 PM Screenshot 2025-07-30 at 8 53 33 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** - [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. --- app/components/Nav/App/App.tsx | 6 +- .../__snapshots__/index.test.tsx.snap | 1586 ++++++++++++++++- app/components/UI/OptinMetrics/index.js | 4 +- app/components/UI/OptinMetrics/index.test.tsx | 59 +- .../__snapshots__/index.test.tsx.snap | 1004 ++++++++++- .../Views/AccountBackupStep1/index.js | 54 +- .../Views/AccountBackupStep1/index.test.tsx | 111 +- .../__snapshots__/index.test.tsx.snap | 531 +++++- .../Views/AccountStatus/index.styles.ts | 7 +- .../Views/AccountStatus/index.test.tsx | 44 +- app/components/Views/Login/index.tsx | 7 +- .../__snapshots__/index.test.tsx.snap | 789 +++++++- .../Views/ManualBackupStep1/index.js | 9 +- .../Views/ManualBackupStep1/index.test.tsx | 51 +- .../Views/ManualBackupStep1/styles.ts | 3 +- .../Views/ManualBackupStep2/index.test.tsx | 8 +- .../Views/ManualBackupStep2/styles.ts | 1 + .../Views/OnboardingSuccess/index.tsx | 8 +- .../ProtectYourWallet.test.tsx | 6 +- .../ProtectYourWallet/ProtectYourWallet.tsx | 20 +- .../SecuritySettings.test.tsx.snap | 27 +- .../__snapshots__/index.test.tsx.snap | 2 +- app/components/Views/WalletRecovery/index.tsx | 3 +- app/images/dark-blur.png | Bin 0 -> 92674 bytes app/images/secure_wallet_dark.png | Bin 0 -> 49233 bytes locales/languages/en.json | 10 +- 26 files changed, 4213 insertions(+), 137 deletions(-) create mode 100644 app/images/dark-blur.png create mode 100644 app/images/secure_wallet_dark.png diff --git a/app/components/Nav/App/App.tsx b/app/components/Nav/App/App.tsx index aeaa0436d3c5..e40143752279 100644 --- a/app/components/Nav/App/App.tsx +++ b/app/components/Nav/App/App.tsx @@ -215,7 +215,11 @@ const OnboardingNav = () => ( - + + + + + + + + + + + + + + + + + + Help us improve MetaMask + + + We’d like to gather basic usage data to improve MetaMask. Know that we never sell the data you provide here. + + + + + + Private: + + + Clicks and views on the app are stored, but other details (like your public address) are not. + + + + + + + + General: + + + We temporarily use your IP address to detect a general location (like your country or region), but it's never stored. + + + + + + + + Optional: + + + You decide if you want to share or delete your usage data via settings any time. + + + + + + + + + We'll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features). + + + + + + We’ll let you know if we decide to use this data for other purposes. You can review our + + Privacy Policy + + for more information. Remember, you can go to settings and opt out at any time. + + + + + + + + + No thanks + + + + + + I agree + + + + + + + + + + + + + + + + OptinMetrics + + + + + + + + + + + + + + + +`; + +exports[`OptinMetrics Snapshots android render matches snapshot with status bar height to zero 1`] = ` + + + + + + + + + + + + + + + + + + Help us improve MetaMask + + + We’d like to gather basic usage data to improve MetaMask. Know that we never sell the data you provide here. + + + + + + Private: + + + Clicks and views on the app are stored, but other details (like your public address) are not. + + + + + + + + General: + + + We temporarily use your IP address to detect a general location (like your country or region), but it's never stored. + + + + + + + + Optional: + + + You decide if you want to share or delete your usage data via settings any time. + + + + + + + + + We'll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features). + + + + + + We’ll let you know if we decide to use this data for other purposes. You can review our + + Privacy Policy + + for more information. Remember, you can go to settings and opt out at any time. + + + + + + + + + No thanks + + + + + + I agree + + + + + + + + + + + + + + + + OptinMetrics + + + + + + + + + + + + + + + +`; + +exports[`OptinMetrics Snapshots iOS renders correctly 1`] = ` root: { ...baseStyles.flexGrow, backgroundColor: colors.background.default, - paddingTop: 24, + paddingTop: + Platform.OS === 'android' ? StatusBar.currentHeight || 24 : 24, }, checkbox: { display: 'flex', diff --git a/app/components/UI/OptinMetrics/index.test.tsx b/app/components/UI/OptinMetrics/index.test.tsx index 81fa49647119..dfd3a3f3cec3 100644 --- a/app/components/UI/OptinMetrics/index.test.tsx +++ b/app/components/UI/OptinMetrics/index.test.tsx @@ -5,6 +5,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react-native'; import { strings } from '../../../../locales/i18n'; import { MetricsEventBuilder } from '../../../core/Analytics/MetricsEventBuilder'; import { MetaMetricsOptInSelectorsIDs } from '../../../../e2e/selectors/Onboarding/MetaMetricsOptIn.selectors'; +import { Platform } from 'react-native'; const { InteractionManager } = jest.requireActual('react-native'); @@ -41,18 +42,62 @@ jest.mock('../../../reducers/legalNotices', () => ({ isPastPrivacyPolicyDate: jest.fn().mockReturnValue(true), })); +// Use dynamic mocking to avoid native module conflicts +jest.doMock('react-native', () => { + const originalRN = jest.requireActual('react-native'); + return { + ...originalRN, + StatusBar: { + currentHeight: 42, + }, + }; +}); + describe('OptinMetrics', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('render matches snapshot', () => { - const { toJSON } = renderScreen( - OptinMetrics, - { name: 'OptinMetrics' }, - { state: {} }, - ); - expect(toJSON()).toMatchSnapshot(); + describe('Snapshots iOS', () => { + Platform.OS = 'ios'; + it('renders correctly', () => { + const { toJSON } = renderScreen( + OptinMetrics, + { name: 'OptinMetrics' }, + { state: {} }, + ); + expect(toJSON()).toMatchSnapshot(); + }); + }); + + describe('Snapshots android', () => { + beforeEach(() => { + Platform.OS = 'android'; + }); + + it('render matches snapshot', () => { + const { toJSON } = renderScreen( + OptinMetrics, + { name: 'OptinMetrics' }, + { state: {} }, + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('render matches snapshot with status bar height to zero', () => { + const { StatusBar } = jest.requireMock('react-native'); + const originalCurrentHeight = StatusBar.currentHeight; + StatusBar.currentHeight = 0; + + const { toJSON } = renderScreen( + OptinMetrics, + { name: 'OptinMetrics' }, + { state: {} }, + ); + expect(toJSON()).toMatchSnapshot(); + + StatusBar.currentHeight = originalCurrentHeight; + }); }); describe('sets traits and sends metric event on confirm', () => { diff --git a/app/components/Views/AccountBackupStep1/__snapshots__/index.test.tsx.snap b/app/components/Views/AccountBackupStep1/__snapshots__/index.test.tsx.snap index 96561c398f94..99cc60903103 100644 --- a/app/components/Views/AccountBackupStep1/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/AccountBackupStep1/__snapshots__/index.test.tsx.snap @@ -1,11 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AccountBackupStep1 render matches snapshot 1`] = ` +exports[`AccountBackupStep1 Snapshots android render matches snapshot 1`] = ` @@ -19,6 +20,7 @@ exports[`AccountBackupStep1 render matches snapshot 1`] = ` { "backgroundColor": "#ffffff", "flex": 1, + "paddingTop": 24, } } testID="protect-your-account-screen" @@ -39,6 +41,7 @@ exports[`AccountBackupStep1 render matches snapshot 1`] = ` "color": "#686e7d", "fontFamily": "Geist Regular", "fontSize": 16, + "fontWeight": "400", "letterSpacing": 0, "lineHeight": 24, } @@ -64,6 +67,7 @@ exports[`AccountBackupStep1 render matches snapshot 1`] = ` "color": "#121314", "fontFamily": "Geist Bold", "fontSize": 32, + "fontWeight": "700", "letterSpacing": 0, "lineHeight": 40, "marginBottom": 16, @@ -101,6 +105,7 @@ exports[`AccountBackupStep1 render matches snapshot 1`] = ` "color": "#686e7d", "fontFamily": "Geist Regular", "fontSize": 16, + "fontWeight": "400", "letterSpacing": 0, "lineHeight": 24, } @@ -116,6 +121,7 @@ exports[`AccountBackupStep1 render matches snapshot 1`] = ` "color": "#4459ff", "fontFamily": "Geist Regular", "fontSize": 16, + "fontWeight": "400", "letterSpacing": 0, "lineHeight": 24, } @@ -135,6 +141,7 @@ exports[`AccountBackupStep1 render matches snapshot 1`] = ` "color": "#686e7d", "fontFamily": "Geist Regular", "fontSize": 16, + "fontWeight": "400", "letterSpacing": 0, "lineHeight": 24, } @@ -184,6 +191,7 @@ exports[`AccountBackupStep1 render matches snapshot 1`] = ` "color": "#ffffff", "fontFamily": "Geist Medium", "fontSize": 16, + "fontWeight": "500", "letterSpacing": 0, "lineHeight": 24, } @@ -225,6 +233,1000 @@ exports[`AccountBackupStep1 render matches snapshot 1`] = ` "color": "#121314", "fontFamily": "Geist Medium", "fontSize": 16, + "fontWeight": "500", + "letterSpacing": 0, + "lineHeight": 24, + } + } + > + Remind me later + + + + + + + + +`; + +exports[`AccountBackupStep1 Snapshots android render matches snapshot with status bar height to zero 1`] = ` + + + + + + Step 2 of 3 + + + + Secure your wallet + + + + + Don’t risk losing your funds. Protect your wallet by saving your + + + Secret Recovery Phrase + + + in a place you trust. + + + + It’s the only way to recover your wallet if you get locked out of the app or get a new device. + + + + + + + + Get started + + + + + + + Remind me later + + + + + + + + +`; + +exports[`AccountBackupStep1 Snapshots iOS render matches snapshot 1`] = ` + + + + + + Step 2 of 3 + + + + Secure your wallet + + + + + Don’t risk losing your funds. Protect your wallet by saving your + + + Secret Recovery Phrase + + + in a place you trust. + + + + It’s the only way to recover your wallet if you get locked out of the app or get a new device. + + + + + + + + Get started + + + + + + + Remind me later + + + + + + + + +`; + +exports[`AccountBackupStep1 Theme appearance renders dark SRP design image by default (dark theme) 1`] = ` + + + + + + Step 2 of 3 + + + + Secure your wallet + + + + + Don’t risk losing your funds. Protect your wallet by saving your + + + Secret Recovery Phrase + + + in a place you trust. + + + + It’s the only way to recover your wallet if you get locked out of the app or get a new device. + + + + + + + + Get started + + + + + + + Remind me later + + + + + + + + +`; + +exports[`AccountBackupStep1 Theme appearance renders light SRP design image for light theme 1`] = ` + + + + + + Step 2 of 3 + + + + Secure your wallet + + + + + Don’t risk losing your funds. Protect your wallet by saving your + + + Secret Recovery Phrase + + + in a place you trust. + + + + It’s the only way to recover your wallet if you get locked out of the app or get a new device. + + + + + + + + Get started + + + + + + StyleSheet.create({ mainWrapper: { backgroundColor: colors.background.default, flex: 1, + paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight || 24 : 0, }, scrollviewWrapper: { flexGrow: 1, @@ -111,9 +109,8 @@ const createStyles = (colors) => * the backup seed phrase flow */ const AccountBackupStep1 = (props) => { - const { route } = props; const [hasFunds, setHasFunds] = useState(false); - const { colors } = useTheme(); + const { colors, themeAppearance } = useTheme(); const styles = createStyles(colors); const { isEnabled: isMetricsEnabled } = useMetrics(); @@ -125,34 +122,6 @@ const AccountBackupStep1 = (props) => { const navigation = useNavigation(); - const headerLeft = useCallback( - () => ( - navigation.goBack()}> - - - ), - [navigation, colors, styles.headerLeft], - ); - - useEffect(() => { - navigation.setOptions({ - ...getOnboardingNavbarOptions( - route, - { - headerLeft, - }, - colors, - false, - ), - gesturesEnabled: false, - }); - }, [navigation, route, colors, headerLeft]); - useEffect( () => { // Check if user has funds @@ -259,7 +228,14 @@ const AccountBackupStep1 = (props) => { > {strings('account_backup_step_1.title')} - + {strings('account_backup_step_1.info_text_1_1')}{' '} diff --git a/app/components/Views/AccountBackupStep1/index.test.tsx b/app/components/Views/AccountBackupStep1/index.test.tsx index 88778c9c0a73..1b659baff6a2 100644 --- a/app/components/Views/AccountBackupStep1/index.test.tsx +++ b/app/components/Views/AccountBackupStep1/index.test.tsx @@ -12,7 +12,7 @@ import Engine from '../../../core/Engine'; import StorageWrapper from '../../../store/storage-wrapper'; import { ONBOARDING_SUCCESS_FLOW } from '../../../constants/onboarding'; import Routes from '../../../constants/navigation/Routes'; -import { InteractionManager } from 'react-native'; +import { InteractionManager, Platform } from 'react-native'; // Use fake timers to resolve reanimated issues. jest.useFakeTimers(); @@ -43,6 +43,21 @@ jest.mock('../../hooks/useMetrics', () => { }; }); +// Mock useTheme hook - default to dark theme +const mockUseTheme = jest.fn().mockReturnValue({ + colors: {}, + themeAppearance: 'dark', // Default to dark theme +}); + +jest.mock('../../../util/theme', () => ({ + useTheme: () => mockUseTheme(), + AppThemeKey: { + os: 'os', + light: 'light', + dark: 'dark', + }, +})); + const mockNavigate = jest.fn(); const mockGoBack = jest.fn(); const mockSetOptions = jest.fn(); @@ -77,6 +92,17 @@ jest .spyOn(InteractionManager, 'runAfterInteractions') .mockImplementation(mockRunAfterInteractions); +// Use dynamic mocking to avoid native module conflicts +jest.doMock('react-native', () => { + const originalRN = jest.requireActual('react-native'); + return { + ...originalRN, + StatusBar: { + currentHeight: 42, + }, + }; +}); + const mockResetActionOnboardingSuccessWizard = CommonActions.reset({ index: 1, routes: [ @@ -114,9 +140,32 @@ describe('AccountBackupStep1', () => { }; }; - it('render matches snapshot', () => { - const { wrapper } = setupTest(); - expect(wrapper).toMatchSnapshot(); + describe('Snapshots iOS', () => { + it('render matches snapshot', () => { + Platform.OS = 'ios'; + const { wrapper } = setupTest(); + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('Snapshots android', () => { + beforeEach(() => { + Platform.OS = 'android'; + }); + + it('render matches snapshot', () => { + const { wrapper } = setupTest(); + expect(wrapper).toMatchSnapshot(); + }); + + it('render matches snapshot with status bar height to zero', () => { + const { StatusBar } = jest.requireMock('react-native'); + const originalCurrentHeight = StatusBar.currentHeight; + StatusBar.currentHeight = 0; + const { wrapper } = setupTest(); + expect(wrapper).toMatchSnapshot(); + StatusBar.currentHeight = originalCurrentHeight; + }); }); it('sets hasFunds to true when Engine.hasFunds returns true', () => { @@ -257,29 +306,6 @@ describe('AccountBackupStep1', () => { }); }); - it('renders header left button, calls goBack when pressed', () => { - setupTest(); - - // Verify that setOptions was called with the correct configuration - expect(mockSetOptions).toHaveBeenCalled(); - const setOptionsCall = mockSetOptions.mock.calls[0][0]; - - // Get the headerLeft function from the options - const headerLeftComponent = setOptionsCall.headerLeft(); - - // Verify the headerLeft component renders correctly - expect(headerLeftComponent).toBeDefined(); - - // The headerLeft component should be a TouchableOpacity - expect(headerLeftComponent.type).toBe('TouchableOpacity'); - - // Simulate pressing the back button by calling onPress directly - headerLeftComponent.props.onPress(); - - // Verify that goBack was called - expect(mockGoBack).toHaveBeenCalled(); - }); - it('show what is seedphrase modal when srp link is pressed', () => { (Engine.hasFunds as jest.Mock).mockReturnValue(true); const { wrapper } = setupTest(); @@ -424,4 +450,35 @@ describe('AccountBackupStep1', () => { expect(mockNavigate).toHaveBeenCalledWith('ManualBackupStep1', {}); }); }); + + describe('Theme appearance', () => { + afterEach(() => { + mockUseTheme.mockReturnValue({ + colors: {}, + themeAppearance: 'dark', + }); + }); + + it('renders dark SRP design image by default (dark theme)', () => { + mockUseTheme.mockReturnValue({ + colors: {}, + themeAppearance: 'dark', + }); + + const { wrapper } = setupTest(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('renders light SRP design image for light theme', () => { + mockUseTheme.mockReturnValue({ + colors: {}, + themeAppearance: 'light', + }); + + const { wrapper } = setupTest(); + + expect(wrapper).toMatchSnapshot(); + }); + }); }); diff --git a/app/components/Views/AccountStatus/__snapshots__/index.test.tsx.snap b/app/components/Views/AccountStatus/__snapshots__/index.test.tsx.snap index 7b3f80b50bb5..8cf44d1ec318 100644 --- a/app/components/Views/AccountStatus/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/AccountStatus/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AccountStatus Snapshots renders correctly with accountName in route params 1`] = ` +exports[`AccountStatus Snapshots android renders correctly with accountName in route params 1`] = ` `; -exports[`AccountStatus Snapshots renders correctly with type="found" 1`] = ` +exports[`AccountStatus Snapshots android renders correctly with type="found and statusbar current height to zero" 1`] = ` `; -exports[`AccountStatus Snapshots renders correctly with type="not_exist" 1`] = ` +exports[`AccountStatus Snapshots android renders correctly with type="not_exist" 1`] = ` + + + + + + + Wallet not found + + + + + We couldn’t find a wallet for "[missing {{accountName}} value]". Do you want to create a new one with this login? + + + + + + + + + Create a new wallet + + + + + Use a different login method + + + + + +`; + +exports[`AccountStatus Snapshots iOS renders correctly with accountName in route params 1`] = ` + + + + + + + Wallet already exists + + + + + A wallet using "test@example.com" already exists. Do you want to try logging in instead? + + + + + + + + + Log in + + + + + Use a different login method + + + + + +`; + +exports[`AccountStatus Snapshots iOS renders correctly with type="found" 1`] = ` + + + + + + + Wallet already exists + + + + + A wallet using "[missing {{accountName}} value]" already exists. Do you want to try logging in instead? + + + + + + + + + Log in + + + + + Use a different login method + + + + + +`; + +exports[`AccountStatus Snapshots iOS renders correctly with type="not_exist" 1`] = ` ({ }, })); +// Use dynamic mocking to avoid native module conflicts +jest.doMock('react-native', () => { + const originalRN = jest.requireActual('react-native'); + return { + ...originalRN, + StatusBar: { + currentHeight: 42, + }, + }; +}); + describe('AccountStatus', () => { beforeEach(() => { jest.clearAllMocks(); mockRoute.params = {}; }); - describe('Snapshots', () => { + describe('Snapshots iOS', () => { + beforeEach(() => { + Platform.OS = 'ios'; + }); + it('renders correctly with type="not_exist"', () => { const { toJSON } = renderWithProvider(); expect(toJSON()).toMatchSnapshot(); @@ -72,6 +88,32 @@ describe('AccountStatus', () => { }); }); + describe('Snapshots android', () => { + beforeEach(() => { + Platform.OS = 'android'; + }); + + it('renders correctly with type="not_exist"', () => { + const { toJSON } = renderWithProvider(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly with type="found and statusbar current height to zero"', () => { + const { StatusBar } = jest.requireMock('react-native'); + const originalCurrentHeight = StatusBar.currentHeight; + StatusBar.currentHeight = 0; + const { toJSON } = renderWithProvider(); + expect(toJSON()).toMatchSnapshot(); + StatusBar.currentHeight = originalCurrentHeight; + }); + + it('renders correctly with accountName in route params', () => { + mockRoute.params = { accountName: 'test@example.com' }; + const { toJSON } = renderWithProvider(); + expect(toJSON()).toMatchSnapshot(); + }); + }); + describe('Behavior Tests', () => { describe('Primary button interactions', () => { it('navigates to Rehydrate screen when type="found" and primary button is pressed', () => { diff --git a/app/components/Views/Login/index.tsx b/app/components/Views/Login/index.tsx index 6dcbe8febb8d..eef4e9387ff7 100644 --- a/app/components/Views/Login/index.tsx +++ b/app/components/Views/Login/index.tsx @@ -653,6 +653,11 @@ const Login: React.FC = ({ saveOnboardingEvent }) => { return null; }; + const handlePasswordChange = (newPassword: string) => { + setPassword(newPassword); + setError(null); + }; + return ( = ({ saveOnboardingEvent }) => { autoCapitalize="none" secureTextEntry ref={fieldRef} - onChangeText={setPassword} + onChangeText={handlePasswordChange} value={password} onSubmitEditing={onLogin} endAccessory={ diff --git a/app/components/Views/ManualBackupStep1/__snapshots__/index.test.tsx.snap b/app/components/Views/ManualBackupStep1/__snapshots__/index.test.tsx.snap index 438ea2b4ad6c..a71b7df22efa 100644 --- a/app/components/Views/ManualBackupStep1/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/ManualBackupStep1/__snapshots__/index.test.tsx.snap @@ -1,5 +1,789 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`ManualBackupStep1 Theme appearance renders dark SRP design image by default (dark theme) 1`] = ` + + + + Step 2 of 3 + + + + + + + Save your Secret Recovery Phrase + + + + This is your + + + Secret Recovery Phrase. + + + Write it down in the correct order and keep it safe. If someone has your Secret Recovery Phrase, they can access your wallet. + + + Don’t share it with anyone, ever. + + + + + + + + + + + + + Tap to reveal + + + Make sure no one is watching your screen. + + + + + + + + + + Continue + + + + + + + + + +`; + +exports[`ManualBackupStep1 Theme appearance renders light SRP design image for light theme 1`] = ` + + + + Step 2 of 3 + + + + + + + Save your Secret Recovery Phrase + + + + This is your + + + Secret Recovery Phrase. + + + Write it down in the correct order and keep it safe. If someone has your Secret Recovery Phrase, they can access your wallet. + + + Don’t share it with anyone, ever. + + + + + + + + + + + + + Tap to reveal + + + Make sure no one is watching your screen. + + + + + + + + + + Continue + + + + + + + + + +`; + exports[`ManualBackupStep1 render matches snapshot 1`] = ` - Secret Recovery Phrase + Secret Recovery Phrase. Write it down in the correct order and keep it safe. If someone has your Secret Recovery Phrase, they can access your wallet. @@ -276,7 +1060,7 @@ exports[`ManualBackupStep1 render matches snapshot 1`] = ` } > @@ -218,7 +223,7 @@ const ManualBackupStep1 = ({ {strings('manual_backup_step_1.reveal')} diff --git a/app/components/Views/ManualBackupStep1/index.test.tsx b/app/components/Views/ManualBackupStep1/index.test.tsx index bb61178c1c30..04e3f1f22909 100644 --- a/app/components/Views/ManualBackupStep1/index.test.tsx +++ b/app/components/Views/ManualBackupStep1/index.test.tsx @@ -8,7 +8,7 @@ import { fireEvent, waitFor } from '@testing-library/react-native'; import { ManualBackUpStepsSelectorsIDs } from '../../../../e2e/selectors/Onboarding/ManualBackUpSteps.selectors'; import { AppThemeKey } from '../../../util/theme/models'; import { strings } from '../../../../locales/i18n'; -import { InteractionManager } from 'react-native'; +import { InteractionManager, Platform } from 'react-native'; const mockStore = configureMockStore(); const initialState = { @@ -58,6 +58,21 @@ jest.mock('react-native', () => { }; }); +// Mock useTheme hook - default to dark theme +const mockUseTheme = jest.fn().mockReturnValue({ + colors: {}, + themeAppearance: 'dark', // Default to dark theme +}); + +jest.mock('../../../util/theme', () => ({ + useTheme: () => mockUseTheme(), + AppThemeKey: { + os: 'os', + light: 'light', + dark: 'dark', + }, +})); + describe('ManualBackupStep1', () => { const mockRunAfterInteractions = jest.fn().mockImplementation((cb) => { cb(); @@ -391,4 +406,38 @@ describe('ManualBackupStep1', () => { // Verify that goBack was called expect(mockGoBack).toHaveBeenCalled(); }); + + describe('Theme appearance', () => { + afterEach(() => { + mockUseTheme.mockReturnValue({ + colors: {}, + themeAppearance: 'dark', + }); + Platform.OS = 'ios'; + }); + + it('renders dark SRP design image by default (dark theme)', () => { + mockUseTheme.mockReturnValue({ + colors: {}, + themeAppearance: 'dark', + }); + + const { wrapper } = setupTest(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('renders light SRP design image for light theme', () => { + Platform.OS = 'android'; + mockUseTheme.mockReturnValue({ + colors: {}, + themeAppearance: 'light', + }); + + const { wrapper } = setupTest(); + + expect(wrapper).toMatchSnapshot(); + Platform.OS = 'ios'; + }); + }); }); diff --git a/app/components/Views/ManualBackupStep1/styles.ts b/app/components/Views/ManualBackupStep1/styles.ts index 775c69303a1d..1777f6b8746f 100644 --- a/app/components/Views/ManualBackupStep1/styles.ts +++ b/app/components/Views/ManualBackupStep1/styles.ts @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -import { StyleSheet } from 'react-native'; +import { Platform, StyleSheet } from 'react-native'; import { fontStyles } from '../../../styles/common'; // TODO: Replace "any" with type @@ -138,5 +138,6 @@ export const createStyles = (colors: any) => }, buttonContainer: { paddingHorizontal: 0, + marginBottom: Platform.OS === 'android' ? 16 : 0, }, }); diff --git a/app/components/Views/ManualBackupStep2/index.test.tsx b/app/components/Views/ManualBackupStep2/index.test.tsx index c9c19aa2e25b..befd2ac4e05f 100644 --- a/app/components/Views/ManualBackupStep2/index.test.tsx +++ b/app/components/Views/ManualBackupStep2/index.test.tsx @@ -8,7 +8,7 @@ import { fireEvent, waitFor } from '@testing-library/react-native'; import { ManualBackUpStepsSelectorsIDs } from '../../../../e2e/selectors/Onboarding/ManualBackUpSteps.selectors'; import { strings } from '../../../../locales/i18n'; import Routes from '../../../constants/navigation/Routes'; -import { InteractionManager } from 'react-native'; +import { InteractionManager, Platform } from 'react-native'; import { ONBOARDING_SUCCESS_FLOW } from '../../../constants/onboarding'; import { ReactTestInstance } from 'react-test-renderer'; @@ -110,6 +110,10 @@ describe('ManualBackupStep2', () => { }); describe('with mockWords', () => { + beforeEach(() => { + Platform.OS = 'ios'; + }); + const mockRoute = jest.fn().mockReturnValue({ params: { words: mockWords, @@ -233,6 +237,7 @@ describe('ManualBackupStep2', () => { }; it('render and handle word selection in grid', () => { + Platform.OS = 'android'; const { wrapper, mockNavigation } = setupTest(); const gridItems = wrapper.getByTestId( `${ManualBackUpStepsSelectorsIDs.GRID_ITEM}-0`, @@ -242,6 +247,7 @@ describe('ManualBackupStep2', () => { fireEvent.press(gridItems); expect(gridItems).toHaveStyle({ backgroundColor: expect.any(String) }); mockNavigation.mockRestore(); + Platform.OS = 'ios'; }); it('render SuccessErrorSheet with type error when seed phrase is invalid', () => { diff --git a/app/components/Views/ManualBackupStep2/styles.ts b/app/components/Views/ManualBackupStep2/styles.ts index 9546bfefed22..4897cbe02e5a 100644 --- a/app/components/Views/ManualBackupStep2/styles.ts +++ b/app/components/Views/ManualBackupStep2/styles.ts @@ -143,6 +143,7 @@ const createStyles = (colors: any) => }, buttonContainer: { paddingHorizontal: 0, + marginBottom: Platform.OS === 'android' ? 16 : 0, }, }); diff --git a/app/components/Views/OnboardingSuccess/index.tsx b/app/components/Views/OnboardingSuccess/index.tsx index 1fe6e193450e..813d09982380 100644 --- a/app/components/Views/OnboardingSuccess/index.tsx +++ b/app/components/Views/OnboardingSuccess/index.tsx @@ -31,7 +31,7 @@ import importAdditionalAccounts from '../../../util/importAdditionalAccounts'; import createStyles from './index.styles'; import CelebratingFox from '../../../animations/Celebrating_Fox.json'; import SearchingFox from '../../../animations/Searching_Fox.json'; -import LottieView from 'lottie-react-native'; +import LottieView, { AnimationObject } from 'lottie-react-native'; import { ONBOARDING_SUCCESS_FLOW } from '../../../constants/onboarding'; export const ResetNavigationToHome = CommonActions.reset({ @@ -91,7 +91,7 @@ export const OnboardingSuccessComponent: React.FC = ({ style={styles.walletReadyImage} autoPlay loop - source={SearchingFox} + source={SearchingFox as AnimationObject} resizeMode="contain" /> @@ -128,7 +128,7 @@ export const OnboardingSuccessComponent: React.FC = ({ style={styles.walletReadyImage} autoPlay loop - source={SearchingFox} + source={SearchingFox as AnimationObject} resizeMode="contain" /> @@ -156,7 +156,7 @@ export const OnboardingSuccessComponent: React.FC = ({ style={styles.walletReadyImage} autoPlay loop - source={CelebratingFox} + source={CelebratingFox as AnimationObject} resizeMode="contain" /> diff --git a/app/components/Views/Settings/SecuritySettings/Sections/ProtectYourWallet/ProtectYourWallet.test.tsx b/app/components/Views/Settings/SecuritySettings/Sections/ProtectYourWallet/ProtectYourWallet.test.tsx index 1c52d95558d8..3058be9794c7 100644 --- a/app/components/Views/Settings/SecuritySettings/Sections/ProtectYourWallet/ProtectYourWallet.test.tsx +++ b/app/components/Views/Settings/SecuritySettings/Sections/ProtectYourWallet/ProtectYourWallet.test.tsx @@ -53,7 +53,7 @@ describe('ProtectYourWallet', () => { }); it('renders correctly when SRP is not backed up', () => { - const { getByText } = renderWithProvider( + const { getByText, queryByText } = renderWithProvider( { ); expect(getByText(strings('app_settings.protect_title'))).toBeDefined(); - expect(getByText(strings('app_settings.protect_desc'))).toBeDefined(); + expect(queryByText(strings('app_settings.protect_desc'))).toBeDefined(); expect(getByText(strings('app_settings.learn_more'))).toBeDefined(); expect(getByText(strings('app_settings.back_up_now'))).toBeDefined(); }); @@ -79,7 +79,7 @@ describe('ProtectYourWallet', () => { ); expect(getByText(strings('app_settings.protect_title'))).toBeDefined(); - expect(getByText(strings('app_settings.protect_desc'))).toBeDefined(); + expect(queryByText(strings('app_settings.protect_desc'))).toBeDefined(); expect( getByText(strings('app_settings.seedphrase_backed_up')), ).toBeDefined(); diff --git a/app/components/Views/Settings/SecuritySettings/Sections/ProtectYourWallet/ProtectYourWallet.tsx b/app/components/Views/Settings/SecuritySettings/Sections/ProtectYourWallet/ProtectYourWallet.tsx index c02ceb52938d..628665bad37c 100644 --- a/app/components/Views/Settings/SecuritySettings/Sections/ProtectYourWallet/ProtectYourWallet.tsx +++ b/app/components/Views/Settings/SecuritySettings/Sections/ProtectYourWallet/ProtectYourWallet.tsx @@ -106,14 +106,20 @@ const ProtectYourWallet = ({ style={styles.desc} > {strings('app_settings.protect_desc')} + {!oauthFlow && !srpBackedup ? ( + Linking.openURL(LEARN_MORE_URL)} + > + {' '} + {strings('app_settings.learn_more')} + + ) : ( + '.' + )} - {!oauthFlow && !srpBackedup && ( -