From e0655239d4e228c2a504ec2d279d757462183222 Mon Sep 17 00:00:00 2001 From: sethkfman <10342624+sethkfman@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:43:30 -0700 Subject: [PATCH 1/5] chore: added fingerprint status check to skip build & cache (#23697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR updates the Android E2E build step and applies an if check on the caching and build step if the fingerprint finds a hit. ## **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** Screenshot 2025-12-04 at 3 22 37 PM ### **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] > [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) is generating a summary for commit e5e31e216639907068b3f343d42da075cc686e9c. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .github/workflows/build-android-e2e.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-android-e2e.yml b/.github/workflows/build-android-e2e.yml index b27338838e0c..4effd0be4dec 100644 --- a/.github/workflows/build-android-e2e.yml +++ b/.github/workflows/build-android-e2e.yml @@ -122,6 +122,7 @@ jobs: - name: Cache Gradle dependencies uses: cirruslabs/cache@v4 + if: ${{ steps.apk-cache-restore.outputs.cache-hit != 'true' }} env: GRADLE_CACHE_VERSION: 1 with: @@ -134,6 +135,7 @@ jobs: key: gradle-${{ env.GRADLE_CACHE_VERSION }}-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - name: Build Android E2E APKs + if: ${{ steps.apk-cache-restore.outputs.cache-hit != 'true' }} run: | echo "🏗 Building Android E2E APKs..." export NODE_OPTIONS="--max-old-space-size=8192" From 5f8eb199576fb18729c31fa6aab6f16e2a532c90 Mon Sep 17 00:00:00 2001 From: Vince Howard Date: Thu, 4 Dec 2025 17:32:08 -0700 Subject: [PATCH 2/5] fix: unified keyboard actions background color (#23649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** fix: unified keyboard actions background color ## **Changelog** CHANGELOG entry:null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MDP-399 ## **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 | | -------- | ------- | | ![before](https://github.com/user-attachments/assets/73ec43f2-799d-4a42-8a37-c53727802529) | ![after](https://github.com/user-attachments/assets/b1cda6b3-6927-4bf3-b63a-f4866e7394a6) | ### **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** - [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] > Unifies quick-amount/keyboard action button styling to a muted background without borders across Stake and Earn views, updating snapshots accordingly. > > - **UI Styling** > - Set quick-amount buttons to `colors.background.muted` and remove borders in `app/components/UI/Stake/components/QuickAmounts.tsx`. > - Align Earn Stake/Unstake quick-amount buttons to muted background (`#3c4d9d0f`) with no borders in snapshots. > - **Tests** > - Update Jest snapshots for `EarnInputView` and `EarnWithdrawInputView` to reflect new button background and removed borders. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f037f8a22b7446df4ba40326e8213767a6452a5c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../__snapshots__/EarnInputView.test.tsx.snap | 32 +++++-------------- .../EarnWithdrawInputView.test.tsx.snap | 16 +++------- .../UI/Stake/components/QuickAmounts.tsx | 4 +-- 3 files changed, 13 insertions(+), 39 deletions(-) diff --git a/app/components/UI/Earn/Views/EarnInputView/__snapshots__/EarnInputView.test.tsx.snap b/app/components/UI/Earn/Views/EarnInputView/__snapshots__/EarnInputView.test.tsx.snap index e089ca754abf..bb0f8faefc01 100644 --- a/app/components/UI/Earn/Views/EarnInputView/__snapshots__/EarnInputView.test.tsx.snap +++ b/app/components/UI/Earn/Views/EarnInputView/__snapshots__/EarnInputView.test.tsx.snap @@ -586,10 +586,8 @@ exports[`EarnInputView render matches snapshot 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, @@ -623,10 +621,8 @@ exports[`EarnInputView render matches snapshot 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, @@ -660,10 +656,8 @@ exports[`EarnInputView render matches snapshot 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, @@ -697,10 +691,8 @@ exports[`EarnInputView render matches snapshot 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, @@ -2080,10 +2072,8 @@ exports[`EarnInputView when values are entered in the keypad updates ETH and fia { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, @@ -2117,10 +2107,8 @@ exports[`EarnInputView when values are entered in the keypad updates ETH and fia { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, @@ -2154,10 +2142,8 @@ exports[`EarnInputView when values are entered in the keypad updates ETH and fia { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, @@ -2191,10 +2177,8 @@ exports[`EarnInputView when values are entered in the keypad updates ETH and fia { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, diff --git a/app/components/UI/Earn/Views/EarnWithdrawInputView/__snapshots__/EarnWithdrawInputView.test.tsx.snap b/app/components/UI/Earn/Views/EarnWithdrawInputView/__snapshots__/EarnWithdrawInputView.test.tsx.snap index 82bcd49c3067..6e23522abbc9 100644 --- a/app/components/UI/Earn/Views/EarnWithdrawInputView/__snapshots__/EarnWithdrawInputView.test.tsx.snap +++ b/app/components/UI/Earn/Views/EarnWithdrawInputView/__snapshots__/EarnWithdrawInputView.test.tsx.snap @@ -577,10 +577,8 @@ exports[`EarnWithdrawInputView render matches snapshot 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, @@ -614,10 +612,8 @@ exports[`EarnWithdrawInputView render matches snapshot 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, @@ -651,10 +647,8 @@ exports[`EarnWithdrawInputView render matches snapshot 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, @@ -688,10 +682,8 @@ exports[`EarnWithdrawInputView render matches snapshot 1`] = ` { "alignItems": "center", "alignSelf": "stretch", - "backgroundColor": "#ffffff", - "borderColor": "#b7bbc8", + "backgroundColor": "#3c4d9d0f", "borderRadius": 20, - "borderWidth": 1, "flex": 1, "flexDirection": "row", "height": 40, diff --git a/app/components/UI/Stake/components/QuickAmounts.tsx b/app/components/UI/Stake/components/QuickAmounts.tsx index 58714fd0f4cb..b6b005190ad3 100644 --- a/app/components/UI/Stake/components/QuickAmounts.tsx +++ b/app/components/UI/Stake/components/QuickAmounts.tsx @@ -28,9 +28,7 @@ const createStyles = (colors: Colors) => }, amount: { flex: 1, - borderWidth: 1, - borderColor: colors.border.default, - backgroundColor: colors.background.default, + backgroundColor: colors.background.muted, flexDirection: 'row', justifyContent: 'center', paddingHorizontal: 16, From 28c90a2fd9928572f5ed85f165747f6f20d9b74c Mon Sep 17 00:00:00 2001 From: Vince Howard Date: Thu, 4 Dec 2025 17:57:44 -0700 Subject: [PATCH 3/5] fix: update bg color for earn upsell banner (#23701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** update background color for earn upsell banner ## **Changelog** CHANGELOG entry:null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MDP-396 ## **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** ### Light Mode (Ignore the font weight issue its not related to this PR) | before | after | | -------- | ------- | | ![before](https://github.com/user-attachments/assets/3b80695c-6afc-4af6-8034-06b926b04b55) | ![after](https://github.com/user-attachments/assets/4155ca05-9130-4a87-a270-617f291b8eb3) | ### Dark Mode (Ignore the font weight issue its not related to this PR) | before | after | | -------- | ------- | | ![before](https://github.com/user-attachments/assets/b2f8f5cc-3e3b-4da4-9f05-c5625fc1e3a7) | ![after](https://github.com/user-attachments/assets/b0e36e1b-9498-4edd-8c3d-ac7dabce3654) | ### **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** - [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] > Change UpsellBannerHeader container background from `colors.background.alternative` to `colors.background.section`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 83ebad457b42a128dedcef0070030205a22b4d93. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../UpsellBannerHeader/UpsellBannerHeader.styles.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/UI/Stake/components/UpsellBanner/UpsellBannerHeader/UpsellBannerHeader.styles.tsx b/app/components/UI/Stake/components/UpsellBanner/UpsellBannerHeader/UpsellBannerHeader.styles.tsx index 4b7befc72a24..bbced69d5d70 100644 --- a/app/components/UI/Stake/components/UpsellBanner/UpsellBannerHeader/UpsellBannerHeader.styles.tsx +++ b/app/components/UI/Stake/components/UpsellBanner/UpsellBannerHeader/UpsellBannerHeader.styles.tsx @@ -7,7 +7,7 @@ const styleSheet = (params: { theme: Theme }) => { return StyleSheet.create({ container: { - backgroundColor: colors.background.alternative, + backgroundColor: colors.background.section, borderRadius: 8, gap: 8, paddingVertical: 24, From fa3b09c7b40a86a61e955396390997047c0970c1 Mon Sep 17 00:00:00 2001 From: CW Date: Thu, 4 Dec 2025 17:25:20 -0800 Subject: [PATCH 4/5] test: restore wdio files for performance tests (#23690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** ### Description This PR restores the WebDriverIO (wdio) helper files that are required dependencies for the Appwright performance tests. ### Changes Restored Files: - wdio/helpers/Gestures.js - Provides cross-platform gesture utilities including tap, long press, swipe (up/down/left/right), and text input methods. Uses percentage-based coordinates for reliable multi-device support. - wdio/helpers/Selectors.js - Provides platform-specific element selection utilities supporting iOS class chains and Android content descriptors, XPath queries by text/resource-id, and CSS selectors. Why The performance tests in appwright/tests/performance/ depend on these wdio helper modules. These files were previously removed but are still required as imports for the performance test infrastructure to function correctly. ## **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. --- > [!NOTE] > Restore WDIO helper modules adding cross‑platform gesture actions and platform-aware selectors required by performance tests. > > - **Helpers**: > - Add `wdio/helpers/Gestures.js` with tap/long‑press, percentage-based swipes (up/down/left/right), coordinate taps, text input, and visibility-with-swipe utilities. > - Add `wdio/helpers/Selectors.js` with platform-aware selectors (iOS class chains, Android content-desc/resource-id) and XPath/CSS helpers. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7e11a4dbe9ff7e34d5b44e6d19cb70d4511ad550. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Curtis David --- wdio/helpers/Gestures.js | 408 ++++++++++++++++++++++++++++++++++++++ wdio/helpers/Selectors.js | 64 ++++++ 2 files changed, 472 insertions(+) create mode 100644 wdio/helpers/Gestures.js create mode 100644 wdio/helpers/Selectors.js diff --git a/wdio/helpers/Gestures.js b/wdio/helpers/Gestures.js new file mode 100644 index 000000000000..8a9eb87a39cb --- /dev/null +++ b/wdio/helpers/Gestures.js @@ -0,0 +1,408 @@ +import Selectors from './Selectors'; + +/** + * To make a Gesture methods more robust for multiple devices and also + * multiple screen sizes the advice is to work with percentages instead of + * actual coordinates. The percentages will calculate the position on the + * screen based on the SCREEN_SIZE which will be determined once if needed + * multiple times. + */ +let SCREEN_SIZE; + +/** + * The values in the below object are percentages of the screen + */ +const SWIPE_DIRECTION = { + down: { + start: { + x: 50, + y: 15, + }, + end: { + x: 50, + y: 85, + }, + }, + left: { + start: { + x: 95, + y: 50, + }, + end: { + x: 5, + y: 50, + }, + }, + right: { + start: { + x: 5, + y: 50, + }, + end: { + x: 95, + y: 50, + }, + }, + up: { + start: { + x: 50, + y: 85, + }, + end: { + x: 50, + y: 15, + }, + }, +}; + +const Actions = { + PRESS: 'press', + LONGPRESS: 'longPress', + TAP: 'tap', + MOVETO: 'moveTo', + WAIT: 'wait', + RELEASE: 'release', +}; + +class Gestures { + static async waitAndTap(element) { + const elem = await element; + await elem.waitForDisplayed({ timeout: 25000 }); + await elem.click(); + } + + static async tap(element, tapType = 'TAP') { + const elem = await element; + switch (tapType) { + case 'TAP': + await elem.click(); + break; + case 'LONGPRESS': + await driver.performActions([{ + type: 'pointer', + id: 'finger1', + parameters: { pointerType: 'touch' }, + actions: [ + { type: 'pointerDown', duration: 0 }, + { type: 'pause', duration: 1000 }, + { type: 'pointerUp', duration: 0 } + ] + }]); + break; + case 'RELEASE': + await driver.performActions([{ + type: 'pointer', + id: 'finger1', + parameters: { pointerType: 'touch' }, + actions: [ + { type: 'pointerUp', duration: 0 } + ] + }]); + break; + case 'WAIT': + await driver.pause(1000); + break; + case 'MOVETO': + const location = await elem.getLocation(); + await driver.performActions([{ + type: 'pointer', + id: 'finger1', + parameters: { pointerType: 'touch' }, + actions: [ + { type: 'pointerMove', duration: 0, x: location.x, y: location.y } + ] + }]); + break; + default: + throw new Error('Tap type not found'); + } + } + + static async tapTextByXpath(text, tapType = 'TAP') { + const elem = await Selectors.getXpathElementByText(text); + await elem.waitForDisplayed(); + switch (tapType) { + case 'TAP': + await elem.click(); + break; + case 'LONGPRESS': + await driver.performActions([{ + type: 'pointer', + id: 'finger1', + parameters: { pointerType: 'touch' }, + actions: [ + { type: 'pointerDown', duration: 0 }, + { type: 'pause', duration: 1000 }, + { type: 'pointerUp', duration: 0 } + ] + }]); + break; + case 'RELEASE': + await driver.performActions([{ + type: 'pointer', + id: 'finger1', + parameters: { pointerType: 'touch' }, + actions: [ + { type: 'pointerUp', duration: 0 } + ] + }]); + break; + default: + throw new Error('Tap type not found'); + } + } + + static async tapByTextContaining(text, tapType = 'TAP') { + const elem = await Selectors.getXpathElementByTextContains(text); + await elem.waitForDisplayed(); + switch (tapType) { + case 'TAP': + await elem.click(); + break; + case 'LONGPRESS': + await driver.performActions([{ + type: 'pointer', + id: 'finger1', + parameters: { pointerType: 'touch' }, + actions: [ + { type: 'pointerDown', duration: 0 }, + { type: 'pause', duration: 1000 }, + { type: 'pointerUp', duration: 0 } + ] + }]); + break; + case 'RELEASE': + await driver.performActions([{ + type: 'pointer', + id: 'finger1', + parameters: { pointerType: 'touch' }, + actions: [ + { type: 'pointerUp', duration: 0 } + ] + }]); + break; + default: + throw new Error('Tap type not found'); + } + } + + static async tapByCoordinatesPercentage( + xAxisPercent, + yAxisPercentage, + tapCount = 1, + ) { + const { width, height } = await driver.getWindowSize(); + const widthPoint = (width * xAxisPercent) / 100; + const heightPoint = (height * yAxisPercentage) / 100; + await driver.touchPerform([ + { + action: 'tap', + options: { + x: widthPoint, + y: heightPoint, + count: tapCount, + }, + }, + ]); + } + + static async longPress(element, waitTime) { + const elem = await element; + await elem.waitForDisplayed(); + await driver.performActions([{ + type: 'pointer', + id: 'finger1', + parameters: { pointerType: 'touch' }, + actions: [ + { type: 'pointerDown', duration: 0 }, + { type: 'pause', duration: waitTime }, + { type: 'pointerUp', duration: 0 } + ] + }]); + } + + static async typeText(element, text) { + const elem = await element; + await elem.waitForDisplayed(); + await elem.click(); + await elem.clearValue(); + await elem.setValue(text, +'\n'); + } + + static async setValueWithoutTap(element, text) { + //Some instances typeText above does not work because of tap + const elem = await element; + await elem.waitForDisplayed(); + await elem.clearValue(); + await elem.setValue(text, +'\n'); + } + + /** + * Check if an element is visible and if not wipe up a portion of the screen to + * check if it visible after x amount of scrolls + */ + static async checkIfDisplayedWithSwipeUp(element, maxScrolls, amount = 0) { + // If the element is not displayed and we haven't scrolled the max amount of scrolls + // then scroll and execute the method again + const elem = await element; + if (!(await elem.isDisplayed()) && amount <= maxScrolls) { + await this.swipeUp(0.85); + await this.checkIfDisplayedWithSwipeUp(element, maxScrolls, amount + 1); + } else if (amount > maxScrolls) { + // If the element is still not visible after the max amount of scroll let it fail + throw new Error( + `The element '${element}' could not be found or is not visible.`, + ); + } // The element was found, proceed with the next action + } + /** + * Swipe down based on a percentage + */ + + static async swipeDown(percentage = 1) { + await this.swipeOnPercentage( + this.calculateXY(SWIPE_DIRECTION.down.start, percentage), + this.calculateXY(SWIPE_DIRECTION.down.end, percentage), + ); + } + /** + * Swipe Up based on a percentage + */ + + static async swipeUp(percentage = 1) { + await this.swipeOnPercentage( + this.calculateXY(SWIPE_DIRECTION.up.start, percentage), + this.calculateXY(SWIPE_DIRECTION.up.end, percentage), + ); + } + /** + * Swipe left based on a percentage + */ + + static async swipeLeft(percentageX = 1, percentageY = 1) { + await this.swipeOnPercentage( + this.calculateXY(SWIPE_DIRECTION.left.start, percentageX), + this.calculateXY(SWIPE_DIRECTION.left.end, percentageY), + ); + } + /** + * Swipe right based on a percentage + */ + + static async swipeRight(percentage = 1) { + await this.swipeOnPercentage( + this.calculateXY(SWIPE_DIRECTION.right.start, percentage), + this.calculateXY(SWIPE_DIRECTION.right.end, percentage), + ); + } + /** + * Swipe from coordinates (from) to the new coordinates (to). The given coordinates are + * percentages of the screen. + */ + + static async swipeOnPercentage(from, to) { + // Get the screen size and store it so it can be re-used. + // This will save a lot of webdriver calls if this methods is used multiple times. + SCREEN_SIZE = SCREEN_SIZE || (await driver.getWindowSize()); // Get the start position on the screen for the swipe + + const pressOptions = this.getDeviceScreenCoordinates(SCREEN_SIZE, from); // Get the move to position on the screen for the swipe + + const moveToScreenCoordinates = this.getDeviceScreenCoordinates( + SCREEN_SIZE, + to, + ); + await this.swipe(pressOptions, moveToScreenCoordinates); + } + /** + * Swipe from coordinates (from) to the new coordinates (to). The given coordinates are in pixels. + */ + + static async improvedSwipe() { + // TODO + const startPercentage = 98; + const endPercentage = 0; + const anchorPercentage = 50; + + const { width, height } = await driver.getWindowSize(); + const anchor = (height * anchorPercentage) / 100; + const startPoint = (width * startPercentage) / 100; + const endPoint = (width * endPercentage) / 100; + await driver.touchPerform([ + { + action: 'press', + options: { + x: startPoint, + y: anchor, + }, + }, + { + action: 'wait', + options: { + ms: 100, + }, + }, + { + action: 'moveTo', + options: { + x: endPoint, + y: anchor, + }, + }, + { + action: 'release', + options: {}, + }, + ]); + } + + static async swipe(from, to) { + // TODO + await driver.performActions([ + { + // a. Create the event + type: 'pointer', + id: 'finger1', + parameters: { pointerType: 'touch' }, + actions: [ + // b. Move finger into start position + { type: 'pointerMove', duration: 0, x: from.x, y: from.y }, + // c. Finger comes down into contact with screen + { type: 'pointerDown', button: 0 }, + // d. Pause for a little bit + { type: 'pause', duration: 100 }, + // e. Finger moves to end position + // We move our finger from the center of the element to the + // starting position of the element. + // Play with the duration to make the swipe go slower / faster + { type: 'pointerMove', duration: 1000, x: to.x, y: to.y }, + // f. Finger gets up, off the screen + { type: 'pointerUp', button: 0 }, + ], + }, + ]); + // Add a pause, just to make sure the swipe is done + await driver.pause(1000); + } + /** + * Get the screen coordinates based on a device his screen size + */ + + static getDeviceScreenCoordinates(screenSize, coordinates) { + return { + x: Math.round(screenSize.width * (coordinates.x / 100)), + y: Math.round(screenSize.height * (coordinates.y / 100)), + }; + } + /** + * Calculate the x y coordinates based on a percentage + */ + + static calculateXY({ x, y }, percentage) { + return { + x: x * percentage, + y: y * percentage, + }; + } +} + +export default Gestures; diff --git a/wdio/helpers/Selectors.js b/wdio/helpers/Selectors.js new file mode 100644 index 000000000000..5dc793d19e3e --- /dev/null +++ b/wdio/helpers/Selectors.js @@ -0,0 +1,64 @@ +class Selectors { + + static async getElementByPlatform(id, isNested = false) { + if (!isNested) { + return $(`~${id}`); + } + + const platform = await driver.getPlatform(); + if (platform === 'Android') { + return $(`~${id}`); + } else if (platform === 'iOS') { + /** + * Use class chains for iOS + * Ref.: https://webdriver.io/docs/selectors#ios-uiautomation + * Too many levels of nesting cause test ids not to be rendered + * Ref.: https://github.com/appium/appium/issues/14825 + */ + return $(`-ios class chain:${id}`); + } + } + + static async getXpathByContentDesc(id) { + return driver.$$(`//*[@content-desc='${id}']`); + } + + static async getXpathElementByText(text) { + const platform = await driver.getPlatform(); + if (platform === 'iOS') { + return await $(`//*[@name='${text}']`); + } + + if (platform === 'Android') { + return await $(`//*[@text='${text}']`); + } + } + + static async getXpathElementByTextContains(text) { + const platform = await driver.getPlatform(); + if (platform === 'iOS') { + return await $(`//*[contains(@name, '${text}')]`); + } + + if (platform === 'Android') { + return await $(`//*[contains(@text, '${text}')]`); + } + } + + static async getXpathElementByResourceId(id) { + const platform = await driver.getPlatform(); + if (platform === 'iOS') { + return await $(`~${id}`); + } + + if (platform === 'Android') { + return await $(`//*[@resource-id='${id}']`); + } + } + + static async getElementByCss(css) { + return await $(css); + } +} + +export default Selectors; From f053689b29ba728fbb4f391b8e739dfa2ae55491 Mon Sep 17 00:00:00 2001 From: Pedro Pablo Aste Kompen Date: Thu, 4 Dec 2025 23:06:55 -0300 Subject: [PATCH 5/5] fix(ramp): parse slip44 intent as native asset cp-7.61.0 (#23689) ## **Description** This PR adds slip44 wildcard matching support to the Deposit `useCryptoCurrencies` hook, aligning it with the existing behavior in the Aggregator flow. **Reason for change:** When navigating to the Deposit flow from the Asset Details page with a native asset (e.g., ETH), the intent contains a wildcard asset ID like `eip155:1/slip44:.`. However, the Deposit SDK returns native tokens with specific slip44 coin types (e.g., `eip155:1/slip44:60`). The direct string comparison was failing to match these, causing the native token not to be pre-selected. **Solution:** Added fallback logic that parses the CAIP-19 asset ID and matches any token with the same `chainId` and `slip44` namespace when the direct match fails. This mirrors the existing behavior in the Aggregator `useCryptoCurrencies` hook. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/23441 ## **Manual testing steps** ```gherkin Feature: Native token selection in Deposit flow Scenario: user navigates to Deposit from Asset Details with native token Given the user is on the Asset Details page for ETH (or any native token) And the Deposit flow is enabled for the user's region When user taps the "Buy" button Then the Deposit flow opens with ETH pre-selected as the cryptocurrency ``` ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/5a7b1ca1-3939-4ab1-a693-bf8579814e98 ### **After** https://github.com/user-attachments/assets/293d0de4-a430-43d3-b0bd-c97315baecd4 ## **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] > Adds slip44 wildcard CAIP-19 intent matching to pre-select native tokens in the Deposit flow and introduces focused tests for this behavior. > > - **Hook** (`app/components/UI/Ramp/Deposit/hooks/useCryptoCurrencies.ts`): > - Add slip44 wildcard handling by parsing CAIP-19 (`parseCAIP19AssetId`) when direct `assetId` match fails, selecting a native token with the same `chainId` and `slip44` namespace. > - Preserve direct `assetId` match precedence over wildcard matching. > - Minor refactor to use a mutable `intentCrypto` before selection. > - **Tests** (`app/components/UI/Ramp/Deposit/hooks/useCryptoCurrencies.test.ts`): > - Add cases for slip44 wildcard selecting native token (same/different `chainId`). > - Add fallback to first token when no native match found. > - Confirm direct match is preferred over wildcard. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7d6606a79ffcfa8772fed86d9a46406aea81659c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../Deposit/hooks/useCryptoCurrencies.test.ts | 97 +++++++++++++++++++ .../Ramp/Deposit/hooks/useCryptoCurrencies.ts | 19 +++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/app/components/UI/Ramp/Deposit/hooks/useCryptoCurrencies.test.ts b/app/components/UI/Ramp/Deposit/hooks/useCryptoCurrencies.test.ts index 18d405db45b0..c63459b6aa10 100644 --- a/app/components/UI/Ramp/Deposit/hooks/useCryptoCurrencies.test.ts +++ b/app/components/UI/Ramp/Deposit/hooks/useCryptoCurrencies.test.ts @@ -429,5 +429,102 @@ describe('useCryptoCurrencies', () => { cryptosWithMissing[0], ); }); + + it('selects native token when intent has slip44 wildcard assetId', () => { + const intent = { assetId: 'eip155:1/slip44:.' }; + mockUseDepositSDK.mockReturnValue( + createMockSDKReturn({ + selectedRegion: MOCK_US_REGION, + selectedCryptoCurrency: null, + setSelectedCryptoCurrency: mockSetSelectedCryptoCurrency, + intent, + setIntent: mockSetIntent, + }), + ); + + renderHook(() => useCryptoCurrencies()); + + expect(mockSetSelectedCryptoCurrency).toHaveBeenCalledWith( + MOCK_ETH_TOKEN, + ); + }); + + it('selects native token when intent has slip44 wildcard with different chainId', () => { + const mockPolygonNativeToken = { + assetId: 'eip155:137/slip44:966', + chainId: 'eip155:137', + name: 'Polygon', + symbol: 'POL', + decimals: 18, + iconUrl: 'https://example.com/pol.png', + }; + const cryptosWithPolygon = [ + ...MOCK_CRYPTOCURRENCIES, + mockPolygonNativeToken, + ]; + mockUseDepositSdkMethod.mockReturnValue([ + { data: cryptosWithPolygon, error: null, isFetching: false }, + mockRetryFetchCryptoCurrencies, + ]); + mockUseSelector.mockReturnValue({ + ...mockNetworkConfigurations, + 'eip155:137': { name: 'Polygon', chainId: '0x89' }, + }); + + const intent = { assetId: 'eip155:137/slip44:.' }; + mockUseDepositSDK.mockReturnValue( + createMockSDKReturn({ + selectedRegion: MOCK_US_REGION, + selectedCryptoCurrency: null, + setSelectedCryptoCurrency: mockSetSelectedCryptoCurrency, + intent, + setIntent: mockSetIntent, + }), + ); + + renderHook(() => useCryptoCurrencies()); + + expect(mockSetSelectedCryptoCurrency).toHaveBeenCalledWith( + mockPolygonNativeToken, + ); + }); + + it('falls back to first token when slip44 wildcard does not match any native token', () => { + const intent = { assetId: 'eip155:999/slip44:.' }; + mockUseDepositSDK.mockReturnValue( + createMockSDKReturn({ + selectedRegion: MOCK_US_REGION, + selectedCryptoCurrency: null, + setSelectedCryptoCurrency: mockSetSelectedCryptoCurrency, + intent, + setIntent: mockSetIntent, + }), + ); + + renderHook(() => useCryptoCurrencies()); + + expect(mockSetSelectedCryptoCurrency).toHaveBeenCalledWith( + MOCK_CRYPTOCURRENCIES[0], + ); + }); + + it('prefers direct match over slip44 wildcard matching', () => { + const intent = { assetId: MOCK_USDC_TOKEN.assetId }; + mockUseDepositSDK.mockReturnValue( + createMockSDKReturn({ + selectedRegion: MOCK_US_REGION, + selectedCryptoCurrency: null, + setSelectedCryptoCurrency: mockSetSelectedCryptoCurrency, + intent, + setIntent: mockSetIntent, + }), + ); + + renderHook(() => useCryptoCurrencies()); + + expect(mockSetSelectedCryptoCurrency).toHaveBeenCalledWith( + MOCK_USDC_TOKEN, + ); + }); }); }); diff --git a/app/components/UI/Ramp/Deposit/hooks/useCryptoCurrencies.ts b/app/components/UI/Ramp/Deposit/hooks/useCryptoCurrencies.ts index c9dea844a763..da1dc7fd4429 100644 --- a/app/components/UI/Ramp/Deposit/hooks/useCryptoCurrencies.ts +++ b/app/components/UI/Ramp/Deposit/hooks/useCryptoCurrencies.ts @@ -11,6 +11,7 @@ import { isCaipChainId } from '@metamask/utils'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { toHex } from '@metamask/controller-utils'; import { toLowerCaseEquals } from '../../../../../util/general'; +import { parseCAIP19AssetId } from '../../Aggregator/utils/parseCaip19AssetId'; export interface UseCryptoCurrenciesResult { cryptoCurrencies: DepositCryptoCurrency[] | null; @@ -56,10 +57,26 @@ export function useCryptoCurrencies(): UseCryptoCurrenciesResult { useEffect(() => { if (cryptoCurrencies && cryptoCurrencies.length > 0) { if (intent?.assetId) { - const intentCrypto = cryptoCurrencies.find((token) => + let intentCrypto = cryptoCurrencies.find((token) => toLowerCaseEquals(token.assetId, intent.assetId), ); + // Handle slip44 wildcard matching any native asset + if (!intentCrypto) { + const intentParsedCaip19 = parseCAIP19AssetId(intent.assetId); + if (intentParsedCaip19?.assetNamespace === 'slip44') { + intentCrypto = cryptoCurrencies.find((token) => { + const tokenParsed = parseCAIP19AssetId(token.assetId); + return ( + tokenParsed && + tokenParsed.namespace === intentParsedCaip19.namespace && + tokenParsed.chainId === intentParsedCaip19.chainId && + tokenParsed.assetNamespace === 'slip44' + ); + }); + } + } + setIntent((prevIntent) => prevIntent ? { ...prevIntent, assetId: undefined } : undefined, );