From 983b54ed8763258303040c7650efb3f315fa8cf1 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 27 May 2026 16:48:41 +0000 Subject: [PATCH 01/15] chore: set OTA_VERSION to v7.78.1 for OTA hotfix (release/7.78.1-ota) --- 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 9f9698e005e..9b3420f5a10 100644 --- a/app/constants/ota.ts +++ b/app/constants/ota.ts @@ -12,7 +12,7 @@ import otaConfig from '../../ota.config.js'; * Reset when releasing a new native build as appropriate for that line. * Kept here (not only in ota.config.js) so changes there do not alter the Expo fingerprint and break CI. */ -export const OTA_VERSION: string = 'vX.XX.X'; +export const OTA_VERSION: string = 'v7.78.1'; export const RUNTIME_VERSION = otaConfig.RUNTIME_VERSION; export const PROJECT_ID = otaConfig.PROJECT_ID; export const UPDATE_URL = otaConfig.UPDATE_URL; From 59d702bc1932e1af91b691f00294dc8176d783cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Wed, 27 May 2026 18:45:26 +0100 Subject: [PATCH 02/15] =?UTF-8?q?cherry-pick:=20fix(perps):=20investigate?= =?UTF-8?q?=20Failed=20to=20execute=20'dispatchEvent'=20on=20'EventTa?= =?UTF-8?q?=E2=80=A6=20(#30701)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …rget': parameter 1 is not of type 'Event' (#30612) ## **Description** Fix `TypeError: Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'` crash caused by the `CloseEvent` polyfill in `shim.js` using `event-target-shim`'s `Event` class instead of React Native's own `Event` class. When `@nktkas/rews` (Hyperliquid SDK WebSocket transport) dispatches a `CloseEvent` on the native WebSocket, RN's `dispatchEvent` validates `event instanceof RNEvent` — which failed because `event-target-shim` provides a different `Event` class. Replaced `event-target-shim` globals with React Native's own `Event`, `EventTarget`, `CloseEvent`, and `MessageEvent` classes so all `instanceof` checks pass consistently. ## **Changelog** CHANGELOG entry: Fixed a crash caused by CloseEvent dispatch on WebSocket failing instanceof validation ## **Related issues** Fixes: [TAT-3223](https://consensyssoftware.atlassian.net/browse/TAT-3223) ## **Manual testing steps** ```gherkin Feature: CloseEvent dispatch on native WebSocket Scenario: CloseEvent is dispatched on native WebSocket without error Given the app is running with Hyperliquid SDK active When a WebSocket connection is closed while in CONNECTING state Then no TypeError is thrown And the CloseEvent is dispatched successfully ``` ## **Screenshots/Recordings** State-only fix: no visual evidence needed. Both ACs proven via CDP eval (CloseEvent dispatch success) and lint:tsc (no TS errors). ## **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. ## **Validation Recipe**
recipe.json ```json { "pr": "TAT-3223", "title": "CloseEvent dispatchEvent on native WebSocket must not throw TypeError", "jira": "TAT-3223", "acceptance_criteria": [ "CloseEvent dispatched on native WebSocket must not throw TypeError", "No new TypeScript errors introduced by the fix" ], "validate": { "static": ["yarn lint:tsc"], "workflow": { "pre_conditions": ["wallet.unlocked"], "entry": "ac1-eval-closeevent-dispatch", "nodes": { "ac1-eval-closeevent-dispatch": { "action": "eval_sync", "expression": "(function() { try { var ws = new WebSocket('wss://echo.websocket.org'); var ce = new CloseEvent('close', {code: 1006, reason: '', wasClean: false}); ws.dispatchEvent(ce); ws.close(); return JSON.stringify({success: true, error: null}); } catch(e) { return JSON.stringify({success: false, error: e.message}); } })()", "assert": { "operator": "eq", "field": "success", "value": true }, "next": "ac1-eval-closeevent-props" }, "ac1-eval-closeevent-props": { "action": "eval_sync", "expression": "(function() { var ce = new CloseEvent('close', {code: 1006, reason: 'test', wasClean: true}); return JSON.stringify({type: ce.type, code: ce.code, reason: ce.reason, wasClean: ce.wasClean}); })()", "assert": { "all": [ { "operator": "eq", "field": "code", "value": 1006 }, { "operator": "eq", "field": "reason", "value": "test" }, { "operator": "eq", "field": "wasClean", "value": true } ] }, "next": "ac1-eval-messageevent-dispatch" }, "ac1-eval-messageevent-dispatch": { "action": "eval_sync", "expression": "(function() { try { var ws = new WebSocket('wss://echo.websocket.org'); var me = new MessageEvent('message', {data: 'hello'}); ws.dispatchEvent(me); ws.close(); return JSON.stringify({success: true, error: null}); } catch(e) { return JSON.stringify({success: false, error: e.message}); } })()", "assert": { "operator": "eq", "field": "success", "value": true }, "next": "setup-done" }, "setup-done": { "action": "end", "status": "pass" } } } } } ```
## **Recipe Workflow**
workflow.mmd ```mermaid graph TD ac1-eval-closeevent-dispatch["ac1-eval-closeevent-dispatch
eval_sync: CloseEvent dispatch on native WS"] ac1-eval-closeevent-props["ac1-eval-closeevent-props
eval_sync: Verify CloseEvent properties"] ac1-eval-messageevent-dispatch["ac1-eval-messageevent-dispatch
eval_sync: MessageEvent dispatch on native WS"] setup-done["setup-done
end: pass"] ac1-eval-closeevent-dispatch --> ac1-eval-closeevent-props ac1-eval-closeevent-props --> ac1-eval-messageevent-dispatch ac1-eval-messageevent-dispatch --> setup-done ```
[TAT-3223]: https://consensyssoftware.atlassian.net/browse/TAT-3223?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- > [!NOTE] > **Medium Risk** > Touches app bootstrap polyfills used by Hyperliquid WebSockets; low blast radius but wrong globals could break perps connectivity at runtime. > > **Overview** > Fixes a **Hyperliquid / perps WebSocket crash** where `dispatchEvent` rejected `CloseEvent` because polyfilled events did not pass React Native’s `instanceof Event` check. > > **`shim.js`** stops using **`event-target-shim`** and hand-rolled `CloseEvent` / `MessageEvent` constructors. When globals are missing, it assigns React Native’s own **`Event`**, **`EventTarget`**, **`CloseEvent`**, and **`MessageEvent`** from RN private web API modules so events dispatched by `@nktkas/rews` match what RN’s WebSocket `EventTarget` expects. > > **`event-target-shim`** is removed from **`package.json`** / lockfile. **`shim.test.js`** adds unit coverage for RN `CloseEvent` and `MessageEvent` properties and inheritance. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5d5cb890f0580cb94d2af53ac577caa515aa86a5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. [TAT-3223]: https://consensyssoftware.atlassian.net/browse/TAT-3223?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [TAT-3223]: https://consensyssoftware.atlassian.net/browse/TAT-3223?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ Co-authored-by: abretonc7s <107169956+abretonc7s@users.noreply.github.com> --- package.json | 1 - shim.js | 41 +++++++++++++--------------- shim.test.js | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 8 ------ 4 files changed, 94 insertions(+), 31 deletions(-) create mode 100644 shim.test.js diff --git a/package.json b/package.json index 47e823c77f4..5db4bab8d3b 100644 --- a/package.json +++ b/package.json @@ -424,7 +424,6 @@ "ethereumjs-util": "^7.0.10", "ethers": "^5.0.14", "ethjs-ens": "2.0.1", - "event-target-shim": "^6.0.2", "eventemitter2": "^6.4.9", "events": "3.0.0", "expo": "54.0.33", diff --git a/shim.js b/shim.js index f946306f4d6..1bee816c3e8 100644 --- a/shim.js +++ b/shim.js @@ -158,14 +158,23 @@ global.crypto = { process.browser = false; -// EventTarget polyfills for Hyperliquid SDK WebSocket support +// EventTarget / Event polyfills for Hyperliquid SDK WebSocket support. +// React Native's WebSocket extends RN's internal EventTarget, whose +// dispatchEvent validates `event instanceof RNEvent`. The event-target-shim +// package provides a *different* Event class that fails this check, causing +// "parameter 1 is not of type 'Event'" TypeErrors when @nktkas/rews dispatches +// CloseEvent on the native WebSocket. Use RN's own classes so all instanceof +// checks pass consistently. if ( typeof global.EventTarget === 'undefined' || typeof global.Event === 'undefined' ) { - const { Event, EventTarget } = require('event-target-shim'); - global.EventTarget = EventTarget; - global.Event = Event; + // eslint-disable-next-line @react-native/no-deep-imports -- RN does not export Event/EventTarget at the top level + global.Event = + require('react-native/src/private/webapis/dom/events/Event').default; + // eslint-disable-next-line @react-native/no-deep-imports -- RN does not export EventTarget at the top level + global.EventTarget = + require('react-native/src/private/webapis/dom/events/EventTarget').default; } if (typeof global.CustomEvent === 'undefined') { @@ -178,29 +187,17 @@ if (typeof global.CustomEvent === 'undefined') { } // CloseEvent polyfill for @nktkas/rews v2 (used by Hyperliquid SDK WebSocket transport) -// React Native/Hermes does not provide CloseEvent as a global constructor if (typeof global.CloseEvent === 'undefined') { - global.CloseEvent = function (type, params) { - params = params || {}; - const event = new global.Event(type, params); - event.code = params.code ?? 0; - event.reason = params.reason ?? ''; - event.wasClean = params.wasClean ?? false; - return event; - }; + // eslint-disable-next-line @react-native/no-deep-imports -- RN does not export CloseEvent at the top level + global.CloseEvent = + require('react-native/src/private/webapis/websockets/events/CloseEvent').default; } // MessageEvent polyfill for @nktkas/rews v2 (used by Hyperliquid SDK WebSocket transport) -// React Native/Hermes does not provide MessageEvent as a global constructor if (typeof global.MessageEvent === 'undefined') { - global.MessageEvent = function (type, params) { - params = params || {}; - const event = new global.Event(type, params); - event.data = params.data ?? null; - event.origin = params.origin ?? ''; - event.lastEventId = params.lastEventId ?? ''; - return event; - }; + // eslint-disable-next-line @react-native/no-deep-imports -- RN does not export MessageEvent at the top level + global.MessageEvent = + require('react-native/src/private/webapis/html/events/MessageEvent').default; } class AbortError extends Error { diff --git a/shim.test.js b/shim.test.js new file mode 100644 index 00000000000..75402f6e840 --- /dev/null +++ b/shim.test.js @@ -0,0 +1,75 @@ +/** + * Tests for the Event/EventTarget/CloseEvent/MessageEvent polyfills in shim.js. + * + * The core fix (TAT-3223) ensures polyfilled globals use React Native's own + * Event classes for instanceof compatibility with RN's EventTarget.dispatchEvent. + * Full dispatch compatibility is validated by the agentic recipe against the + * live runtime; these unit tests verify constructor behavior and property access. + */ + +/* eslint-disable @react-native/no-deep-imports, import-x/no-commonjs */ +const RNCloseEvent = + require('react-native/src/private/webapis/websockets/events/CloseEvent').default; +const RNMessageEvent = + require('react-native/src/private/webapis/html/events/MessageEvent').default; +const RNEvent = + require('react-native/src/private/webapis/dom/events/Event').default; +/* eslint-enable @react-native/no-deep-imports, import-x/no-commonjs */ + +describe('Event polyfill shims (TAT-3223)', () => { + describe('CloseEvent', () => { + it('preserves code, reason, and wasClean via getters', () => { + const ce = new RNCloseEvent('close', { + code: 1006, + reason: 'abnormal', + wasClean: false, + }); + + expect(ce.type).toBe('close'); + expect(ce.code).toBe(1006); + expect(ce.reason).toBe('abnormal'); + expect(ce.wasClean).toBe(false); + }); + + it('defaults code to 0, reason to empty, wasClean to false', () => { + const ce = new RNCloseEvent('close'); + + expect(ce.code).toBe(0); + expect(ce.reason).toBe(''); + expect(ce.wasClean).toBe(false); + }); + + it('extends RN Event', () => { + const ce = new RNCloseEvent('close', { code: 1000 }); + + expect(ce instanceof RNEvent).toBe(true); + expect(ce.type).toBe('close'); + }); + }); + + describe('MessageEvent', () => { + it('preserves data and origin via getters', () => { + const me = new RNMessageEvent('message', { + data: 'payload', + origin: 'wss://example.com', + }); + + expect(me.type).toBe('message'); + expect(me.data).toBe('payload'); + expect(me.origin).toBe('wss://example.com'); + }); + + it('defaults data to undefined, origin to empty string', () => { + const me = new RNMessageEvent('message'); + + expect(me.data).toBeUndefined(); + expect(me.origin).toBe(''); + }); + + it('extends RN Event', () => { + const me = new RNMessageEvent('message', { data: 'test' }); + + expect(me instanceof RNEvent).toBe(true); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 9dc6718399a..573cae5fc72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28746,13 +28746,6 @@ __metadata: languageName: node linkType: hard -"event-target-shim@npm:^6.0.2": - version: 6.0.2 - resolution: "event-target-shim@npm:6.0.2" - checksum: 10/aa69fc4193cad3f1e4dc0c2d3f2689ea2d477f5ff2fbee8b65f866035b15658e1985932b06ba2190c3d2cc9cc6802c26facd6c60487590c1a05f44545ec24f42 - languageName: node - linkType: hard - "eventemitter2@npm:^6.4.9": version: 6.4.9 resolution: "eventemitter2@npm:6.4.9" @@ -35683,7 +35676,6 @@ __metadata: ethereumjs-util: "npm:^7.0.10" ethers: "npm:^5.0.14" ethjs-ens: "npm:2.0.1" - event-target-shim: "npm:^6.0.2" eventemitter2: "npm:^6.4.9" events: "npm:3.0.0" execa: "npm:^8.0.1" From a3715bb17dfd9b484c684d1b63c0f87bb23eaeb2 Mon Sep 17 00:00:00 2001 From: metamaskbot Date: Wed, 27 May 2026 17:47:27 +0000 Subject: [PATCH 03/15] [skip ci] Bump version number to 5196 --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 060e01e64f2..c2063f9656b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.78.0" - versionCode 5142 + versionCode 5196 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" missingDimensionStrategy 'detox', 'full' diff --git a/bitrise.yml b/bitrise.yml index a2f5008d067..ea309d22e03 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3558,13 +3558,13 @@ app: VERSION_NAME: 7.78.0 - opts: is_expand: false - VERSION_NUMBER: 5142 + VERSION_NUMBER: 5196 - opts: is_expand: false FLASK_VERSION_NAME: 7.78.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 5142 + FLASK_VERSION_NUMBER: 5196 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index d4697636c4e..94ea3eb5acf 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -990,7 +990,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5142; + CURRENT_PROJECT_VERSION = 5196; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1059,7 +1059,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5142; + CURRENT_PROJECT_VERSION = 5196; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1125,7 +1125,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5142; + CURRENT_PROJECT_VERSION = 5196; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1192,7 +1192,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5142; + CURRENT_PROJECT_VERSION = 5196; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 5e3d69598830fe0622a0f7d587cd142a130d1882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Wed, 27 May 2026 18:53:39 +0100 Subject: [PATCH 04/15] Revert "[skip ci] Bump version number to 5196" This reverts commit a3715bb17dfd9b484c684d1b63c0f87bb23eaeb2. --- android/app/build.gradle | 2 +- bitrise.yml | 4 ++-- ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index c2063f9656b..060e01e64f2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -188,7 +188,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionName "7.78.0" - versionCode 5196 + versionCode 5142 testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" missingDimensionStrategy 'detox', 'full' diff --git a/bitrise.yml b/bitrise.yml index ea309d22e03..a2f5008d067 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -3558,13 +3558,13 @@ app: VERSION_NAME: 7.78.0 - opts: is_expand: false - VERSION_NUMBER: 5196 + VERSION_NUMBER: 5142 - opts: is_expand: false FLASK_VERSION_NAME: 7.78.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 5196 + FLASK_VERSION_NUMBER: 5142 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index 94ea3eb5acf..d4697636c4e 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -990,7 +990,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5196; + CURRENT_PROJECT_VERSION = 5142; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1059,7 +1059,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5196; + CURRENT_PROJECT_VERSION = 5142; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1125,7 +1125,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5196; + CURRENT_PROJECT_VERSION = 5142; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1192,7 +1192,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 5196; + CURRENT_PROJECT_VERSION = 5142; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; From 265546cc7064a9a5fb031c559792cf622430093a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Wed, 27 May 2026 18:53:59 +0100 Subject: [PATCH 05/15] ota fingerprint hack --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5db4bab8d3b..4516d3d2425 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "build:android:checksum:prod": "./scripts/checksum.sh", "build:android:checksum:flask": "export METAMASK_BUILD_TYPE='flask' && ./scripts/checksum.sh flask", "build:android:checksum:verify": "shasum -a 512 -c sha512sums.txt", - "fingerprint:generate": "node scripts/generate-fingerprint.js", + "fingerprint:generate": "git checkout tags/v7.78.0 -- package.json && node scripts/generate-fingerprint.js", "build:repack:android": "PLATFORM=android node scripts/repack.js", "build:repack:ios": "PLATFORM=ios node scripts/repack.js", "build:liveline-webview": "./scripts/build-liveline-webview.sh", From a0eebce4f67de524e83ee6d31597e6c187c1e1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Wed, 27 May 2026 19:58:27 +0100 Subject: [PATCH 06/15] modify fingerprint hack --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4516d3d2425..5499bea3d58 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "build:android:checksum:prod": "./scripts/checksum.sh", "build:android:checksum:flask": "export METAMASK_BUILD_TYPE='flask' && ./scripts/checksum.sh flask", "build:android:checksum:verify": "shasum -a 512 -c sha512sums.txt", - "fingerprint:generate": "git checkout tags/v7.78.0 -- package.json && node scripts/generate-fingerprint.js", + "fingerprint:generate": "git checkout 57b7bd6076280b12ca06a978d16eff24c6e7cb19 -- package.json && node scripts/generate-fingerprint.js", "build:repack:android": "PLATFORM=android node scripts/repack.js", "build:repack:ios": "PLATFORM=ios node scripts/repack.js", "build:liveline-webview": "./scripts/build-liveline-webview.sh", From 896867a8398a838b9d00939379fb9782f15360fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Wed, 27 May 2026 20:22:05 +0100 Subject: [PATCH 07/15] modify fingerprint hack again --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5499bea3d58..397e539cd8d 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "build:android:checksum:prod": "./scripts/checksum.sh", "build:android:checksum:flask": "export METAMASK_BUILD_TYPE='flask' && ./scripts/checksum.sh flask", "build:android:checksum:verify": "shasum -a 512 -c sha512sums.txt", - "fingerprint:generate": "git checkout 57b7bd6076280b12ca06a978d16eff24c6e7cb19 -- package.json && node scripts/generate-fingerprint.js", + "fingerprint:generate": "git fetch --tags && git checkout v7.78.0 -- package.json && node scripts/generate-fingerprint.js", "build:repack:android": "PLATFORM=android node scripts/repack.js", "build:repack:ios": "PLATFORM=ios node scripts/repack.js", "build:liveline-webview": "./scripts/build-liveline-webview.sh", From da5beef2572bb0a610e3d5539a6bcba832c21306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Fri, 29 May 2026 01:30:03 +0100 Subject: [PATCH 08/15] release: update changelog for 7.78.1 (hotfix - no test plan) (#30700) ## **Description** Add the `[7.78.1]` section to `CHANGELOG.md` for the `v7.78.1` OTA hotfix. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: ## **Changelog** CHANGELOG entry: ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: metamaskbot --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8523f0f111..e8048e91fa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.78.1] + +### Fixed + +- Fixed a crash caused by CloseEvent dispatch on WebSocket failing instanceof validation (#30612) + ## [7.78.0] ### Added @@ -11557,7 +11563,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.78.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.78.1...HEAD +[7.78.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.78.0...v7.78.1 [7.78.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.2...v7.78.0 [7.77.2]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.1...v7.77.2 [7.77.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.0...v7.77.1 From f327cc0c16eee020f65c2ee7c36ad0aca5d5592a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Fri, 29 May 2026 01:54:08 +0100 Subject: [PATCH 09/15] remove fingerprint hack --- .yarnrc.yml | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.yarnrc.yml b/.yarnrc.yml index dba849354f6..e3934841d1c 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -19,6 +19,7 @@ npmAuditIgnoreAdvisories: - 1113442 # bn.js affected by an infinite loop. No fix available yet (latest is 5.2.1, affected <=5.2.3). Suppressing for now to unblock CI. https://github.com/advisories/GHSA-378v-28hj-76wf - 1115765 # XML injection via unsafe CDATA serialization allows attacker-controlled markup insertion https://github.com/advisories/GHSA-wh4c-j3r5-mjhp - 1116970 # uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided. We're using v4 and v1 which are not affected. Ignored while we work through the breaking changes between fixed and used versions. Track progress: https://consensyssoftware.atlassian.net/browse/MCWP-557 + - 1119502 yarnPath: .yarn/releases/yarn-4.14.1.cjs diff --git a/package.json b/package.json index 397e539cd8d..5db4bab8d3b 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "build:android:checksum:prod": "./scripts/checksum.sh", "build:android:checksum:flask": "export METAMASK_BUILD_TYPE='flask' && ./scripts/checksum.sh flask", "build:android:checksum:verify": "shasum -a 512 -c sha512sums.txt", - "fingerprint:generate": "git fetch --tags && git checkout v7.78.0 -- package.json && node scripts/generate-fingerprint.js", + "fingerprint:generate": "node scripts/generate-fingerprint.js", "build:repack:android": "PLATFORM=android node scripts/repack.js", "build:repack:ios": "PLATFORM=ios node scripts/repack.js", "build:liveline-webview": "./scripts/build-liveline-webview.sh", From 3e5edf71a8926498ca3ff22d2582e66f9faae6d3 Mon Sep 17 00:00:00 2001 From: tommasini <46944231+tommasini@users.noreply.github.com> Date: Fri, 29 May 2026 21:04:08 +0100 Subject: [PATCH 10/15] chore: bump axios 16.1 (#30815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix audit issue: https://github.com/advisories/GHSA-35jp-ww65-95wh ## **Changelog** CHANGELOG entry: ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- > [!NOTE] > **Low Risk** > Dependency-only security patch with no app code changes; standard patch-level HTTP client upgrade. > > **Overview** > Bumps **axios** from `^1.15.x` to **`^1.16.0`** in `package.json` (direct dependency and Yarn **resolutions**) and refreshes **`yarn.lock`** so the tree resolves to **axios 1.16.1**, addressing advisory [GHSA-35jp-ww65-95wh](https://github.com/advisories/GHSA-35jp-ww65-95wh). > > The lockfile also picks up **follow-redirects** `1.16.0` and axios’s updated transitive deps (e.g. **https-proxy-agent**). No application source changes. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e4f36bb06418ba7de17fd2a8e959bb7c9ac9a630. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- package.json | 4 ++-- yarn.lock | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 86588bd1962..081792342ed 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,7 @@ "@unrs/resolver-binding-wasm32-wasi": "npm:npm-empty-package@1.0.0", "d3-color": "3.1.0", "napi-postinstall": "npm:npm-empty-package@1.0.0", - "axios": "^1.15.1", + "axios": "^1.16.0", "lodash": "4.18.1", "redux-persist-filesystem-storage/react-native-blob-util": "^0.19.9", "@ethersproject/providers/ws": "^7.5.10", @@ -407,7 +407,7 @@ "@walletconnect/utils": "^2.23.0", "@xmldom/xmldom": "^0.8.13", "asyncstorage-down": "4.2.0", - "axios": "^1.15.0", + "axios": "^1.16.0", "bignumber.js": "^9.0.1", "bitcoin-address-validation": "2.2.3", "bnjs4": "npm:bn.js@^4.12.3", diff --git a/yarn.lock b/yarn.lock index 8148c323f21..7d1c3a53775 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22781,14 +22781,15 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.15.1": - version: 1.15.2 - resolution: "axios@npm:1.15.2" +"axios@npm:^1.16.0": + version: 1.16.1 + resolution: "axios@npm:1.16.1" dependencies: - follow-redirects: "npm:^1.15.11" + follow-redirects: "npm:^1.16.0" form-data: "npm:^4.0.5" + https-proxy-agent: "npm:^5.0.1" proxy-from-env: "npm:^2.1.0" - checksum: 10/eebbd8cb777316d4252cd994a06ec9fb956ef519214a62dab6c5443ae8b753b5116e9a770502316789e6cdef1101e6aae53b6936d6a3791b2d66d75f4d7d2462 + checksum: 10/9b6218cf96321cfbbf8f160658d695367114bcf4fb62492bdc1ccd647f184b5c71ae400e5ecaaf41079bc561de2ecbaf1fec63f398b3ec53389beff7694df64c languageName: node linkType: hard @@ -30149,13 +30150,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.11": - version: 1.15.11 - resolution: "follow-redirects@npm:1.15.11" +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.16.0": + version: 1.16.0 + resolution: "follow-redirects@npm:1.16.0" peerDependenciesMeta: debug: optional: true - checksum: 10/07372fd74b98c78cf4d417d68d41fdaa0be4dcacafffb9e67b1e3cf090bc4771515e65020651528faab238f10f9b9c0d9707d6c1574a6c0387c5de1042cde9ba + checksum: 10/3fbe3d80b3b544c22705d837aa5d4a0d07a740d913534a2620b0a004c610af4148e3b58723536dd099aaa1c9d3a155964bde9665d6e5cb331460809a1fc572fd languageName: node linkType: hard @@ -35643,7 +35644,7 @@ __metadata: appium-xcuitest-driver: "npm:9.5.0" assert: "npm:^1.5.0" asyncstorage-down: "npm:4.2.0" - axios: "npm:^1.15.0" + axios: "npm:^1.16.0" babel-jest: "npm:^29.7.0" babel-loader: "npm:^9.1.3" babel-plugin-inline-import: "npm:^3.0.0" From 903189830e57c9c3a5cc5362e72e39ae67f480d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Loureiro?= <175489935+joaoloureirop@users.noreply.github.com> Date: Fri, 29 May 2026 22:28:55 +0100 Subject: [PATCH 11/15] chore(ci): add bitrise.yml to fix release automation (#30825) ## **Description** fix release automation by restoring bitrise.yml. Needed for version bump workflows. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- bitrise.yml | 3608 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3608 insertions(+) create mode 100644 bitrise.yml diff --git a/bitrise.yml b/bitrise.yml new file mode 100644 index 00000000000..17f707e0a19 --- /dev/null +++ b/bitrise.yml @@ -0,0 +1,3608 @@ +--- +format_version: '8' +default_step_lib_source: 'https://github.com/bitrise-io/bitrise-steplib.git' +project_type: react-native + +#Pipelines are listed below +pipelines: + # Generates prod builds for all targets + create_prod_builds_pipeline: + stages: + - create_prod_builds: {} + create_android_env_builds_pipeline: + stages: + - create_android_env_builds: {} + #Creates MetaMask-QA apps and stores apk/ipa in Bitrise + create_qa_builds_pipeline: + stages: + - create_build_qa: {} + #Builds MetaMask, MetaMask-QA apps and stores apk/ipa in Bitrise + build_all_targets_pipeline: + stages: + - bump_version_stage: {} + - create_build_release: {} + - bump_version_stage: {} + - create_build_beta: {} + - bump_version_stage: {} + - create_build_qa: {} + - bump_version_stage: {} + #Releases MetaMask apps and stores apk/ipa into Play(Internal Testing)/App(TestFlight) Store + release_builds_to_store_pipeline: + stages: + - bump_version_stage: {} + - create_build_release: {} + - deploy_build_release: {} + - create_build_qa: {} #Generate QA builds for E2E app upgrade tests + #Releases MetaMask beta apps and stores apk/ipa into Play(Internal Testing)/App(TestFlight) Store + beta_builds_to_store_pipeline: + stages: + - bump_version_stage: {} + - create_build_beta: {} + - deploy_build_release: {} + #Releases MetaMask RC apps for release buil profiling + release_rc_builds_to_store_pipeline: + stages: + - bump_version_stage: {} + - create_build_rc: {} + - deploy_build_release: {} + #Releases MetaMask apps and stores ipa into App(TestFlight) Store + release_ios_to_store_pipeline: + stages: + - bump_version_stage: {} + - create_ios_release: {} + - deploy_ios_release: {} + #Releases MetaMask apps and stores apk Play(Internal Testing) Store + release_android_to_store_pipeline: + stages: + - bump_version_stage: {} + - create_android_release: {} + - deploy_android_release: {} + # TODO: Remove this workflow since it's not used anymore + run_e2e_ios_pipeline: + stages: + - build_e2e_ios_stage: {} + - run_e2e_ios_stage: {} + - notify: {} + # TODO: Remove this workflow since it's not used anymore + run_e2e_flask_pipeline: + stages: + - pr_cache_check_stage: {} + - build_e2e_flask_ios_android: {} + - run_e2e_flask_ios_android: {} + - notify: {} + #Run single suits or test files + run_single_test_suite_ios_android: + stages: + - build_smoke_e2e_ios_android_stage: {} + - run_single_e2e_ios_android_stage: {} + - notify: {} + #Run E2E test suite for Android only + run_e2e_android_pipeline: + stages: + - build_e2e_android_stage: {} #builds android detox E2E + - run_e2e_android_stage: {} #runs android detox test E2E + - notify: {} + #PR_e2e_verfication (build ios & android), run iOS (smoke), emulator Android + release_e2e_pipeline: + stages: + - build_e2e_ios_android_stage: {} + - run_release_e2e_ios_android_stage: {} + - notify: {} + #PR_e2e_verfication (build ios & android), run iOS (smoke), emulator Android + pr_smoke_e2e_pipeline: + stages: + - set_main_target_stage: {} + - pr_cache_check_stage: {} + - build_smoke_e2e_ios_android_stage: {} + - run_smoke_e2e_ios_android_stage: {} + - notify: {} + + flask_smoke_e2e_pipeline: + stages: + - set_flask_target_stage: {} + - pr_cache_check_stage: {} + - build_e2e_flask_ios_android: {} + - run_e2e_flask_ios_android_stage: {} + - notify: {} + + #Performance smoke test pipeline - runs only performance tests + smoke_e2e_performance_pipeline: + stages: + - set_main_target_stage: {} + - pr_cache_check_stage: {} + - build_smoke_e2e_ios_android_stage: {} + # - run_smoke_e2e_performance_ios_android_stage: {} + - notify: {} + + #PR_e2e_verfication (build ios & android), run iOS (regression), emulator Android + pr_rc_rwy_pipeline: + workflows: + set_main_target_workflow: {} + pr_check_build_cache: + depends_on: + - set_main_target_workflow + abort_on_fail: true + build_ios_rc_and_upload_sourcemaps: + depends_on: + - pr_check_build_cache + build_android_rc_and_upload_sourcemaps: + depends_on: + - pr_check_build_cache + expo_dev_pipeline: + stages: + - create_build_dev_expo: {} + # - app_launch_times_test_stage: {} + #Main expo pipeline + expo_main_pipeline: + stages: + - create_build_main_expo: {} + #Flask expo pipeline + expo_flask_pipeline: + stages: + - create_build_flask_expo: {} + #QA expo pipeline + expo_qa_pipeline: + stages: + - create_build_qa_expo: {} + # multichain_permissions_e2e_pipeline: + # stages: + # - build_multichain_permissions_e2e_ios_android_stage: {} + # - run_multichain_permissions_e2e_ios_android_stage: {} + # Pipeline for Flask + create_flask_release_builds_pipeline: + stages: + - create_build_flask_release: {} + - notify: {} + release_flask_builds_to_store_pipeline: + stages: + - create_build_flask_release: {} + - deploy_flask_build_release: {} + - release_notify: {} + nightly_exp_builds_pipeline: + workflows: + bump_version_code: {} + build_android_main_exp: + depends_on: + - bump_version_code + build_ios_main_exp: + depends_on: + - bump_version_code + nightly_rc_builds_pipeline: + workflows: + bump_version_code: {} + build_android_main_rc: + depends_on: + - bump_version_code + build_ios_main_rc: + depends_on: + - bump_version_code + exp_builds_to_testflight_pipeline: + workflows: + bump_version_code: {} + build_android_main_exp: + depends_on: + - bump_version_code + build_ios_main_exp: + depends_on: + - bump_version_code + upload_ios_main_to_testflight: + depends_on: + - build_ios_main_exp + rc_builds_to_testflight_pipeline: + workflows: + bump_version_code: {} + build_android_main_rc: + depends_on: + - bump_version_code + build_ios_main_rc: + depends_on: + - bump_version_code + upload_ios_main_to_testflight: + depends_on: + - build_ios_main_rc +#Stages reference workflows. Those workflows cannot but utility "_this-is-a-utility" +stages: + bump_version_stage: + workflows: + - bump_version_code: {} + create_build_all_targets: + workflows: + - build_android_release: {} + - build_ios_release: {} + - build_android_flask_release: {} + - build_ios_flask_release: {} + - build_android_qa: {} + - build_ios_qa: {} + - build_android_devbuild: {} + - build_ios_devbuild: {} + - build_ios_simbuild: {} + create_build_release: + workflows: + - build_android_main_prod: {} + - build_ios_main_prod: {} + create_build_beta: + workflows: + - build_android_main_beta: {} + - build_ios_main_beta: {} + deploy_build_release: + workflows: + - deploy_android_to_store: {} + - deploy_ios_to_store: {} + create_ios_release: + workflows: + - build_ios_release: {} + deploy_ios_release: + workflows: + - deploy_ios_to_store: {} + create_android_release: + workflows: + - build_android_main_prod: {} + create_android_release_new: + workflows: + - build_android_main_prod: {} + - build_android_main_beta: {} + - build_android_main_rc: {} + create_ios_release_new: + workflows: + - build_ios_main_prod: {} + - build_ios_main_beta: {} + - build_ios_main_rc: {} + create_build_rc: + workflows: + - build_android_main_rc: {} + - build_ios_main_rc: {} + deploy_android_release: + workflows: + - deploy_android_to_store: {} + create_build_dev_expo: + workflows: + - build_android_devbuild: {} + - build_ios_devbuild: {} + - build_ios_simbuild: {} + create_build_main_expo: + workflows: + - build_android_devbuild: {} + - build_ios_devbuild: {} + - build_ios_simbuild: {} + create_build_flask_expo: + workflows: + - build_android_flask_devbuild: {} + - build_ios_flask_devbuild: {} + - build_ios_flask_simbuild: {} + create_build_qa_expo: + workflows: + - build_android_qa_devbuild: {} + - build_ios_qa_devbuild: {} + - build_ios_qa_simbuild: {} + create_build_qa: + workflows: + - build_android_qa: {} + - build_ios_qa: {} + create_prod_builds: + workflows: + - build_ios_main_prod: {} + - build_android_main_prod: {} + - build_ios_qa_prod: {} + - build_android_qa_prod: {} + - build_ios_flask_prod: {} + - build_android_flask_prod: {} + create_android_env_builds: + workflows: + - build_android_main_prod: {} + - build_android_main_rc: {} + - build_android_main_beta: {} + - build_android_main_exp: {} + - build_android_main_test: {} + - build_android_main_e2e: {} + - build_android_main_dev: {} + - build_android_flask_prod: {} + - build_android_flask_test: {} + - build_android_flask_e2e: {} + - build_android_flask_dev: {} + create_build_qa_android: + workflows: + - build_android_qa: {} + create_build_qa_ios: + workflows: + - build_ios_qa: {} + # TODO: Remove this workflow since it's not used anymore + build_e2e_ios_stage: + workflows: + - ios_e2e_build: {} + # TODO: Remove this workflow since it's not used anymore + run_e2e_ios_stage: + workflows: + - ios_e2e_test: {} + pr_cache_check_stage: + abort_on_fail: true + workflows: + - pr_check_build_cache: {} + # Sets the METAMASK_BUILD_TYPE variable to the main target + set_main_target_stage: + workflows: + - set_main_target_workflow: {} + # Sets the METAMASK_BUILD_TYPE variable to the flask target + set_flask_target_stage: + workflows: + - set_flask_target_workflow: {} + build_smoke_e2e_ios_android_stage: + abort_on_fail: true + workflows: + - build_ios_main_e2e: + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + # Disabling in CI to allow GHA runs + # - build_android_main_e2e: + # run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "false"}}' + build_multichain_permissions_e2e_ios_android_stage: + abort_on_fail: true + workflows: + - build_ios_multichain_permissions_e2e: {} + - build_android_multichain_permissions_e2e: {} + # run_multichain_permissions_e2e_ios_android_stage: + # workflows: + # - run_tag_multichain_permissions_ios: {} + # - run_tag_multichain_permissions_android: {} + run_e2e_flask_ios_android_stage: + workflows: + - run_ios_api_specs: {} + - run_trade_swimlane_ios_smoke: {} + - run_trade_swimlane_android_smoke: {} + - run_network_abstraction_swimlane_ios_smoke: {} + - run_network_abstraction_swimlane_android_smoke: {} + - run_network_expansion_swimlane_ios_smoke: {} + - run_network_expansion_swimlane_android_smoke: {} + - run_wallet_platform_swimlane_ios_smoke: {} + - run_wallet_platform_swimlane_android_smoke: {} + - run_tag_smoke_confirmations_android: {} + - run_tag_smoke_confirmations_ios: {} + - run_tag_flask_build_tests_ios: {} + - run_tag_flask_build_tests_android: {} + - run_tag_smoke_accounts_ios: {} + - run_tag_smoke_accounts_android: {} + run_single_e2e_ios_android_stage: + workflows: + - run_single_ios_e2e_test: {} + - run_single_android_e2e_test: {} + run_smoke_e2e_ios_android_stage: + workflows: + - run_ios_api_specs: {} + - run_trade_swimlane_ios_smoke: {} + # - run_trade_swimlane_android_smoke: {} + - run_network_abstraction_swimlane_ios_smoke: {} + # - run_network_abstraction_swimlane_android_smoke: {} + - run_network_expansion_swimlane_ios_smoke: {} + # - run_network_expansion_swimlane_android_smoke: {} + - run_wallet_platform_swimlane_ios_smoke: {} + # - run_wallet_platform_swimlane_android_smoke: {} + # - run_tag_smoke_confirmations_android: {} + - run_tag_smoke_identity_ios: {} + # - run_tag_smoke_identity_android: {} + - run_tag_smoke_confirmations_ios: {} + - run_tag_smoke_multichain_api_ios: {} + - run_tag_smoke_accounts_ios: {} + # - run_tag_smoke_accounts_android: {} + # - run_tag_smoke_performance_ios: {} + # - run_tag_smoke_performance_android: {} + - run_tag_smoke_money_ios: {} + # - run_tag_smoke_money_android: {} + # The entire workflow is disabled as Android runs on GHA and iOS was already skipped due to a regression + # run_smoke_e2e_performance_ios_android_stage: + # workflows: + # - run_tag_smoke_performance_ios: {} + #- run_tag_smoke_performance_android: {} + # TODO: This stage does the same thing as build_smoke_e2e_ios_android_stage + build_regression_e2e_ios_android_stage: + abort_on_fail: true + workflows: + - build_ios_main_e2e: + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + - build_android_main_e2e: + run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "false"}}' + run_regression_e2e_ios_android_stage: + workflows: + - ios_run_regression_confirmations_tests: {} + - ios_run_regression_wallet_platform_tests: {} + - ios_run_regression_trade_tests: {} + - ios_run_regression_network_abstraction_tests: {} + - ios_run_regression_network_expansion_tests: {} + - ios_run_regression_accounts_tests: {} + - ios_run_regression_ux_tests: {} + - ios_run_regression_assets_tests: {} + - android_run_regression_confirmations_tests: {} + - android_run_regression_wallet_platform_tests: {} + - android_run_regression_trade_tests: {} + - android_run_regression_network_abstraction_tests: {} + - android_run_regression_network_expansion_tests: {} + - android_run_regression_performance_tests: {} + - android_run_regression_assets_tests: {} + - android_run_regression_accounts_tests: {} + - android_run_regression_ux_tests: {} + run_release_e2e_ios_android_stage: + workflows: + - ios_run_regression_confirmations_tests: {} + - ios_run_regression_wallet_platform_tests: {} + - ios_run_regression_trade_tests: {} + - ios_run_regression_network_abstraction_tests: {} + - ios_run_regression_network_expansion_tests: {} + - ios_run_regression_accounts_tests: {} + - ios_run_regression_ux_tests: {} + - ios_run_regression_assets_tests: {} + - android_run_regression_confirmations_tests: {} + - android_run_regression_wallet_platform_tests: {} + - android_run_regression_trade_tests: {} + - android_run_regression_network_abstraction_tests: {} + - android_run_regression_network_expansion_tests: {} + - android_run_regression_performance_tests: {} + - android_run_regression_assets_tests: {} + - android_run_regression_accounts_tests: {} + - android_run_regression_ux_tests: {} + - run_ios_api_specs: {} + - run_trade_swimlane_ios_smoke: {} + - run_trade_swimlane_android_smoke: {} + - run_network_expansion_swimlane_ios_smoke: {} + - run_network_expansion_swimlane_android_smoke: {} + - run_network_abstraction_swimlane_ios_smoke: {} + - run_network_abstraction_swimlane_android_smoke: {} + - run_wallet_platform_swimlane_ios_smoke: {} + - run_wallet_platform_swimlane_android_smoke: {} + - run_tag_smoke_confirmations_android: {} + - run_tag_smoke_confirmations_ios: {} + - run_tag_smoke_multichain_api_ios: {} + - run_tag_smoke_accounts_ios: {} + - run_tag_smoke_accounts_android: {} + - run_tag_smoke_money_ios: {} + - run_tag_smoke_money_android: {} + build_regression_e2e_ios_gns_disabled_stage: + abort_on_fail: true + workflows: + - build_ios_main_e2e_gns_disabled: + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + run_regression_e2e_ios_gns_disabled_stage: + workflows: + - ios_run_regression_network_abstraction_tests_gns_disabled: {} + + # TODO: Remove this stage since it's not used anymore + run_e2e_ios_android_stage: + workflows: + - ios_e2e_test: {} + - android_e2e_test: {} + build_e2e_ios_android_stage: + workflows: + - build_android_qa: {} + - ios_e2e_build: {} + - android_e2e_build: {} + build_e2e_android_stage: + workflows: + - android_e2e_build: {} + run_e2e_android_stage: + workflows: + - android_e2e_test: {} + notify: + workflows: + - notify_success: {} + release_notify: + workflows: + - release_announcing_stores: {} + build_e2e_flask_ios_android: + workflows: + - build_android_flask_e2e: {} + - build_ios_flask_e2e: {} + # TODO: Remove this workflow since it's not used anymore + run_e2e_flask_ios_android: + workflows: + - run_flask_e2e_android: {} + - run_flask_e2e_ios: {} + create_build_flask_release: + workflows: + - build_android_flask_release: {} + - build_ios_flask_release: {} + deploy_flask_build_release: + workflows: + - deploy_android_to_store: + envs: + - MM_ANDROID_PACKAGE_NAME: 'io.metamask.flask' + - deploy_ios_to_store: + +workflows: + # Code Setups + setup: + steps: + - activate-ssh-key@4: + run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' + - git-clone@6: {} + set_commit_hash: + steps: + - script@1: + title: Set commit hash env variable + inputs: + - content: |- + #!/usr/bin/env bash + BRANCH_COMMIT_HASH="$(git rev-parse HEAD)" + + # Log the value of BRANCH_COMMIT_HASH + echo "BRANCH_COMMIT_HASH is set to: $BRANCH_COMMIT_HASH" + + envman add --key BRANCH_COMMIT_HASH --value "$BRANCH_COMMIT_HASH" + - share-pipeline-variable@1: + title: Persist commit hash across all stages + inputs: + - variables: |- + BRANCH_COMMIT_HASH + code_setup: + before_run: + - setup + - prep_environment + steps: + # - restore-cocoapods-cache@2: {} + - yarn@0: + inputs: + - command: install --immutable + title: Yarn Install + - script@1: + inputs: + - content: |- + #!/usr/bin/env bash + envman add --key METAMASK_YARN_CACHE_DIR --value "$(yarn cache dir)" + title: Get Yarn cache directory + - yarn@0: + inputs: + - command: setup:github-ci + title: Yarn Setup + prep_environment: + steps: + - restore-cache@2: + title: Restore Node + inputs: + - key: node-{{ getenv "NODE_VERSION" }}-{{ .OS }}-{{ .Arch }} + - script@1: + title: node, yarn, corepack installation + inputs: + - content: |- + #!/usr/bin/env bash + echo "Gems being installed with bundler gem" + bundle install --gemfile=ios/Gemfile + echo "Node $NODE_VERSION being installed" + + set -e + + # Add and enable NVM + wget -O install-nvm.sh "https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh" + echo "${NVM_SHA256SUM} install-nvm.sh" > install-nvm.sh.SHA256SUM + sha256sum -c install-nvm.sh.SHA256SUM + chmod +x install-nvm.sh && ./install-nvm.sh && rm ./install-nvm.sh + source "${HOME}/.nvm/nvm.sh" + echo 'source "${HOME}/.nvm/nvm.sh"' | tee -a ${HOME}/.{bashrc,profile} + + # Retry logic for Node installation + MAX_ATTEMPTS=3 + ATTEMPT=1 + until [ $ATTEMPT -gt $MAX_ATTEMPTS ] + do + echo "Attempt $ATTEMPT to install Node.js" + nvm install ${NODE_VERSION} + INSTALL_STATUS=$? # Capture the exit status of the nvm install command + if [ $INSTALL_STATUS -eq 0 ]; then + echo "Node.js installation successful!" + break + else + echo "Node.js installation failed with exit code $INSTALL_STATUS" + ATTEMPT=$((ATTEMPT+1)) + echo "Node.js installation failed, retrying in 5 seconds..." + sleep 5 + fi + done + + if [ $ATTEMPT -gt $MAX_ATTEMPTS ]; then + echo "Node.js installation failed after $MAX_ATTEMPTS attempts." + exit 1 + fi + envman add --key PATH --value $PATH + + node --version + + echo "Corepack being installed with npm" + npm i -g "corepack@$COREPACK_VERSION" + echo "Corepack enabling $YARN_VERSION" + corepack enable + - save-cache@1: + title: Save Node + inputs: + - key: node-{{ getenv "NODE_VERSION" }}-{{ .OS }}-{{ .Arch }} + - paths: |- + ../.nvm/ + ../../../root/.nvm/ + extract_version_info: + steps: + - script@1: + title: Extract Version Info from Android build.gradle + inputs: + - content: | + #!/bin/bash + set -e + + # Path to Android build.gradle file + BUILD_GRADLE_PATH="$PROJECT_LOCATION_ANDROID/app/build.gradle" + + # Extract versionName (remove quotes) + APP_SEM_VER_NAME_TMP=$(grep -o 'versionName "[^"]*"' "$BUILD_GRADLE_PATH" | sed 's/versionName "\(.*\)"/\1/') + + # Extract versionCode + APP_BUILD_NUMBER_TMP=$(grep -o 'versionCode [0-9]*' "$BUILD_GRADLE_PATH" | sed 's/versionCode \([0-9]*\)/\1/') + + # Validate that we found both values + if [ -z "$APP_SEM_VER_NAME_TMP" ] || [ -z "$APP_BUILD_NUMBER_TMP" ]; then + echo "Error: Could not extract version information from $BUILD_GRADLE_PATH" + echo "APP_SEM_VER_NAME: $APP_SEM_VER_NAME_TMP" + echo "APP_BUILD_NUMBER: $APP_SEM_VER_NAME_TMP" + exit 1 + fi + + echo "APP_SEM_VER_NAME: $APP_SEM_VER_NAME_TMP" + echo "APP_BUILD_NUMBER: $APP_BUILD_NUMBER_TMP" + + # Export as environment variables + envman add --key APP_SEM_VER_NAME --value "$APP_SEM_VER_NAME_TMP" + envman add --key APP_BUILD_NUMBER --value "$APP_BUILD_NUMBER_TMP" + install_applesimutils: + steps: + - script@1: + title: applesimutils installation + inputs: + - content: |- + #!/usr/bin/env bash + echo "Now installing applesimutils..." + brew tap wix/brew + brew install applesimutils + + # Notifications utility workflows + # Provides values for commit or branch message and path depending on commit env setup initialised or not + _get_workflow_info: + steps: + - activate-ssh-key@4: + is_always_run: true # always run to also feed failure notifications + run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' + - git-clone@6: + inputs: + - update_submodules: 'no' + is_always_run: true # always run to also feed failure notifications + - script@1: + is_always_run: true # always run to also feed failure notifications + inputs: + - content: | + #!/bin/bash + # generate reference to commit from env or using git + COMMIT_SHORT_HASH="${BITRISE_GIT_COMMIT:0:7}" + BRANCH_HEIGHT='' + WORKFLOW_TRIGGER='Push' + + if [[ -z "$BITRISE_GIT_COMMIT" ]]; then + COMMIT_SHORT_HASH="$(git rev-parse --short HEAD)" + BRANCH_HEIGHT='HEAD' + WORKFLOW_TRIGGER='Manual' + fi + + envman add --key COMMIT_SHORT_HASH --value "$COMMIT_SHORT_HASH" + envman add --key BRANCH_HEIGHT --value "$BRANCH_HEIGHT" + envman add --key WORKFLOW_TRIGGER --value "$WORKFLOW_TRIGGER" + title: Get commit or branch name and path variables + + # Slack notification utils: we have two workflows to allow choosing when to notify: on success, on failure or both. + # A workflow for instance create_qa_builds will notify on failure for each build_android_qa or build_ios_qa + # but will only notify success if both success and create_qa_builds succeeds. + + # Send a Slack message on successful release + release_announcing_stores: + before_run: + - code_setup + steps: + - yarn@0: + inputs: + - command: build:announce + title: Announcing pre-release + is_always_run: false + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: standard + + # Send a Slack message when workflow succeeds + notify_success: + before_run: + - _get_workflow_info + steps: + # Update Bitrise comment in PR with success status + - comment-on-github-pull-request@0: + is_always_run: true + run_if: '{{getenv "TRIGGERED_BY_PR_LABEL" | eq "true"}}' + inputs: + - personal_access_token: '$GITHUB_ACCESS_TOKEN' + - body: |- + ## [https://bitrise.io/](${BITRISEIO_PIPELINE_BUILD_URL}) **Bitrise** + + ✅✅✅ `${BITRISEIO_PIPELINE_TITLE}` passed on Bitrise! ✅✅✅ + + Commit hash: ${GITHUB_PR_HASH} + Build link: ${BITRISEIO_PIPELINE_BUILD_URL} + + >[!NOTE] + >- You can kick off another `${BITRISEIO_PIPELINE_TITLE}` on Bitrise by removing and re-applying the `run-ios-e2e-smoke` label on the pull request + + + + - repository_url: '$GIT_REPOSITORY_URL' + - issue_number: '$GITHUB_PR_NUMBER' + - api_base_url: 'https://api.github.com' + - update_comment_tag: '$GITHUB_PR_HASH' + - script@1: + is_always_run: true + title: Label PR with success + inputs: + - content: |- + #!/usr/bin/env bash + # Define label data + LABELS_JSON='{"labels":["bitrise-result-ready"]}' + + # API URL to add labels to a PR + API_URL="https://api.github.com/repos/$BITRISEIO_GIT_REPOSITORY_OWNER/$BITRISEIO_GIT_REPOSITORY_SLUG/issues/$GITHUB_PR_NUMBER/labels" + + # Perform the curl request and capture the HTTP status code + HTTP_RESPONSE=$(curl -s -o response.txt -w "%{http_code}" -X POST -H "Authorization: token $GITHUB_ACCESS_TOKEN" -H "Accept: application/vnd.github.v3+json" -d "$LABELS_JSON" "$API_URL") + + # Output the HTTP status code + echo "HTTP Response Code: $HTTP_RESPONSE" + + # Optionally check the response + echo "HTTP Response Code: $HTTP_RESPONSE" + + if [ "$HTTP_RESPONSE" -ne 200 ]; then + echo "Failed to apply label. Status code: $HTTP_RESPONSE" + cat response.txt # Show error message from GitHub if any + else + echo "Label applied successfully." + fi + + # Clean up the response file + rm response.txt + + + # Send a Slack message when workflow fails + notify_failure: + before_run: + - _get_workflow_info + steps: + - script@1: + is_always_run: true + title: Check if PR comment should be updated + inputs: + - content: |- + #!/usr/bin/env bash + if [[ "$TRIGGERED_BY_PR_LABEL" == "true" && $BITRISE_BUILD_STATUS == 1 ]]; then + envman add --key SHOULD_UPDATE_PR_COMMENT --value "true" + else + envman add --key SHOULD_UPDATE_PR_COMMENT --value "false" + fi + # Update Bitrise comment in PR with failure status + - comment-on-github-pull-request@0: + is_always_run: true + run_if: '{{getenv "SHOULD_UPDATE_PR_COMMENT" | eq "true"}}' + inputs: + - personal_access_token: '$GITHUB_ACCESS_TOKEN' + - body: |- + ## [https://bitrise.io/](${BITRISEIO_PIPELINE_BUILD_URL}) **Bitrise** + + ❌❌❌ `${BITRISEIO_PIPELINE_TITLE}` failed on Bitrise! ❌❌❌ + + Commit hash: ${GITHUB_PR_HASH} + Build link: ${BITRISEIO_PIPELINE_BUILD_URL} + + >[!NOTE] + >- You can rerun any failed steps by opening the Bitrise build, tapping `Rebuild` on the upper right then `Rebuild unsuccessful Workflows` + >- You can kick off another `${BITRISEIO_PIPELINE_TITLE}` on Bitrise by removing and re-applying the `run-ios-e2e-smoke` label on the pull request + + > [!TIP] + >- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise + + + + - repository_url: '$GIT_REPOSITORY_URL' + - issue_number: '$GITHUB_PR_NUMBER' + - api_base_url: 'https://api.github.com' + - update_comment_tag: '$GITHUB_PR_HASH' + bump_version_code: + before_run: + - _get_workflow_info + steps: + - script@1: + is_always_run: true + title: Trigger Update Build Version Action + inputs: + - content: |- + #!/usr/bin/env bash + set -e + + # Trigger the workflow + RESPONSE=$(curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_TRIGGER_ACTION_TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/MetaMask/metamask-mobile/actions/workflows/125632963/dispatches" \ + -d "{\"ref\":\"main\",\"inputs\":{\"base-branch\":\"$BITRISE_GIT_BRANCH\"}}" || exit 1) + + echo "Waiting 25 seconds for workflow to start..." + sleep 25 + + # Check completion status every 20 seconds + for i in {1..5}; do + echo "Checking workflow status (Attempt $i of 5)..." + + RESPONSE=$(curl -L \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_TRIGGER_ACTION_TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/MetaMask/metamask-mobile/actions/workflows/125632963/runs?branch=main&status=in_progress") + + # Store the total_count value + TOTAL_COUNT=$(echo "$RESPONSE" | jq -r '.total_count') + + if [ "$TOTAL_COUNT" = "0" ]; then + echo "Workflow finished result: https://github.com/MetaMask/metamask-mobile/actions/workflows/update-latest-build-version.yml" + exit 0 + else + # Get the status and conclusion of the most recent run + STATUS=$(echo "$RESPONSE" | jq -r '.workflow_runs[0].status') + echo "Current status: $STATUS" + echo "Workflow is still in progress (status: $STATUS)..." + sleep 20 + fi + done + + echo "Timeout: Workflow did not complete within 100 seconds" + echo "Check this action status for reason: https://github.com/MetaMask/metamask-mobile/actions/workflows/update-latest-build-version.yml" + exit 1 + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: standard + # CI Steps + ci_test: + before_run: + - code_setup + steps: + - yarn@0: + inputs: + - args: '' + - command: test:unit --silent + title: Unit Test + is_always_run: false + - script@1: + inputs: + - content: |- + #!/usr/bin/env bash + echo 'weew - everything passed!' + title: All Tests Passed + is_always_run: false + # E2E Steps + ### This workflow uses a flag (TEST_SUITE) that defines the specific set of tests to be run. + ## in this instance Regression. In future iterations we can rename to ios_test_suite_selection & android_test_suite_selection + ios_build_regression_tests: + after_run: + - ios_e2e_build + ios_run_regression_confirmations_tests: + envs: + - TEST_SUITE_TAG: 'RegressionConfirmations' + after_run: + - ios_e2e_test + ios_run_regression_wallet_platform_tests: + envs: + - TEST_SUITE_TAG: 'RegressionWalletPlatform' + after_run: + - ios_e2e_test + ios_run_regression_trade_tests: + envs: + - TEST_SUITE_TAG: 'RegressionTrade' + after_run: + - ios_e2e_test + ios_run_regression_network_abstraction_tests: + envs: + - TEST_SUITE_TAG: 'RegressionNetworkAbstractions' + after_run: + - ios_e2e_test + ios_run_regression_network_abstraction_tests_gns_disabled: + envs: + - TEST_SUITE_TAG: 'RegressionNetworkAbstractions' + after_run: + - ios_e2e_test + ios_run_regression_network_expansion_tests: + envs: + - TEST_SUITE_TAG: 'RegressionNetworkExpansion' + after_run: + - ios_e2e_test + ios_run_regression_performance_tests: + envs: + - TEST_SUITE_TAG: 'RegressionPerformance' + after_run: + - ios_e2e_test + ios_run_regression_accounts_tests: + envs: + - TEST_SUITE_TAG: 'RegressionAccounts' + after_run: + - ios_e2e_test + ios_run_regression_assets_tests: + envs: + - TEST_SUITE_TAG: 'RegressionAssets' + after_run: + - ios_e2e_test + ios_run_regression_ux_tests: + envs: + - TEST_SUITE_TAG: 'RegressionWalletUX' + after_run: + - ios_e2e_test + android_build_regression_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + after_run: + - android_e2e_build + android_run_regression_confirmations_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionConfirmations' + after_run: + - android_e2e_test + android_run_regression_wallet_platform_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionWalletPlatform' + after_run: + - android_e2e_test + android_run_regression_trade_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionTrade' + after_run: + - android_e2e_test + android_run_regression_network_abstraction_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionNetworkAbstractions' + after_run: + - android_e2e_test + android_run_regression_network_expansion_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionNetworkExpansion' + after_run: + - android_e2e_test + android_run_regression_performance_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionPerformance' + after_run: + - android_e2e_test + android_run_regression_accounts_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionAccounts' + after_run: + - android_e2e_test + android_run_regression_ux_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionWalletUX' + after_run: + - android_e2e_test + android_run_regression_assets_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionAssets' + after_run: + - android_e2e_test + download_production_qa_apk: + steps: + - script@1: + title: Download Production QA APK + inputs: + - content: | + #!/usr/bin/env bash + ./tests/scripts/download-android-qa-app.sh + # APK_PATH is already set by the download script using envman + build_flask_e2e_android: + after_run: + - android_flask_e2e_build + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow since it's not used anymore + run_flask_e2e_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - METAMASK_BUILD_TYPE: 'flask' + after_run: + - android_e2e_test + build_flask_e2e_ios: + envs: + - COMMAND_YARN: 'build:ios:flask:e2e' + after_run: + - ios_e2e_build + # TODO: Remove this workflow since it's not used anymore + run_flask_e2e_ios: + envs: + - METAMASK_BUILD_TYPE: 'flask' + after_run: + - ios_e2e_test + build_ios_multichain_permissions_e2e: + after_run: + - ios_e2e_build + # - android_e2e_build + build_android_multichain_permissions_e2e: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + after_run: + - android_e2e_build + + ### Separating workflows so they run concurrently during smoke runs + run_tag_smoke_multichain_api_ios: + envs: + - TEST_SUITE_TAG: '.*SmokeMultiChainAPI.*' + after_run: + - ios_e2e_test + + run_network_expansion_swimlane_ios_smoke: + envs: + - TEST_SUITE_TAG: '.*NetworkExpansion.*' + after_run: + - ios_e2e_test + run_network_expansion_swimlane_android_smoke: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*NetworkExpansion.*' + after_run: + - android_e2e_test + + run_wallet_platform_swimlane_ios_smoke: + envs: + - TEST_SUITE_TAG: '.*SmokeWalletPlatform.*' + after_run: + - ios_e2e_test + run_wallet_platform_swimlane_android_smoke: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*SmokeWalletPlatform.*' + after_run: + - android_e2e_test + + run_network_abstraction_swimlane_ios_smoke: + envs: + - TEST_SUITE_TAG: '.*SmokeNetworkAbstraction.*' + after_run: + - ios_e2e_test + run_network_abstraction_swimlane_android_smoke: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*SmokeNetworkAbstraction.*' + after_run: + - android_e2e_test + run_trade_swimlane_ios_smoke: + envs: + - TEST_SUITE_TAG: '.*(SmokeSwap|SmokeStake).*' + after_run: + - ios_e2e_test + run_trade_swimlane_android_smoke: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*(SmokeSwap|SmokeStake).*' + after_run: + - android_e2e_test + run_ios_api_specs: + after_run: + - ios_api_specs + run_tag_smoke_confirmations_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'SmokeConfirmations' + after_run: + - android_e2e_test + run_tag_smoke_confirmations_ios: + envs: + - TEST_SUITE_TAG: 'SmokeConfirmations' + after_run: + - ios_e2e_test + run_tag_smoke_performance_ios: + envs: + - TEST_SUITE_TAG: '.*SmokePerformance.*' + after_run: + - ios_e2e_test + run_tag_smoke_performance_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*SmokePerformance.*' + after_run: + - android_e2e_test + run_tag_multichain_permissions_ios: + envs: + - TEST_SUITE_TAG: '.*SmokeMultiChainPermissions.*' + after_run: + - ios_e2e_test + run_tag_multichain_permissions_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*SmokeMultiChainPermissions.*' + after_run: + - android_e2e_test + run_tag_flask_build_tests_ios: + envs: + - TEST_SUITE_TAG: '.*FlaskBuildTests.*' + after_run: + - ios_e2e_test + run_tag_flask_build_tests_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*FlaskBuildTests.*' + after_run: + - android_e2e_test + run_tag_smoke_identity_ios: + envs: + - TEST_SUITE_TAG: 'SmokeIdentity' + after_run: + - ios_e2e_test + run_tag_smoke_identity_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'SmokeIdentity' + after_run: + - android_e2e_test + run_tag_smoke_accounts_ios: + envs: + - TEST_SUITE_TAG: 'SmokeAccounts' + after_run: + - ios_e2e_test + run_tag_smoke_accounts_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'SmokeAccounts' + after_run: + - android_e2e_test + run_tag_smoke_money_ios: + envs: + - TEST_SUITE_TAG: '.*SmokeMoney.*' + after_run: + - ios_e2e_test + run_tag_smoke_money_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*SmokeMoney.*' + after_run: + - android_e2e_test + android_e2e_build: + envs: + - KEYSTORE_URL: $BITRISEIO_ANDROID_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/release.keystore' + - BUILD_COMMAND: 'yarn build:android:main:e2e' + after_run: + - _android_e2e_build_template + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + # TODO: Consolidate android_e2e_build and android_flask_e2e_build once _android_e2e_build_template and _android_build_template is consolidated + android_flask_e2e_build: + envs: + - KEYSTORE_URL: $BITRISEIO_ANDROID_FLASK_KEYSTORE_URL_URL + - KEYSTORE_PATH: 'android/keystores/flaskRelease.keystore' + - BUILD_COMMAND: 'yarn build:android:flask:e2e' + after_run: + - _android_e2e_build_template + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + run_single_android_e2e_test: + run_if: '{{ or (ne "$E2E_TEST_FILE" "") (ne "$TEST_SUITE_TAG" "") }}' + after_run: + - android_e2e_test + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + + run_single_ios_e2e_test: + run_if: '{{ or (ne "$E2E_TEST_FILE" "") (ne "$TEST_SUITE_TAG" "") }}' + after_run: + - ios_e2e_test + + android_e2e_test: + before_run: + - setup + - prep_environment + after_run: + - notify_failure + steps: + - restore-gradle-cache@2: {} + - restore-cache@2: + title: Restore Android PR Build Cache (if build was skipped) + run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "true"}}' + inputs: + - key: '{{ getenv "ANDROID_PR_BUILD_CACHE_KEY" }}' + - script@1: + title: Copy Android build from cache (if build was skipped) + run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "true"}}' + inputs: + - content: |- + #!/usr/bin/env bash + echo "Copying Android build from cache..." + + if [ -d "/tmp/android-cache/build/outputs" ]; then + echo "Restoring Android build outputs from cache..." + mkdir -p android/app/build/outputs + cp -r /tmp/android-cache/build/outputs/* android/app/build/outputs/ + echo "✅ Android build artifacts restored from cache" + + echo "Restored files:" + find android/app/build/outputs -type f -name "*.apk" -o -name "*.aab" | head -5 + else + echo "❌ Cache directory /tmp/android-cache/build/outputs not found" + echo "Cache may not have been restored properly" + fi + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull Android build + - script@1: + title: Copy Android build for Detox + run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "false"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Create directories for Detox + mkdir -p "$BITRISE_SOURCE_DIR/android/app/build/outputs" + + # Copy saved files for Detox usage + # INTERMEDIATE_ANDROID_BUILD_DIR is the cached directory from android_e2e_build's "Save Android build" step + cp -r "$INTERMEDIATE_ANDROID_BUILD_DIR" "$BITRISE_SOURCE_DIR/android/app/build" + - restore-cache@2: + title: Restore cache node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - script@1: + title: Install foundry + inputs: + - content: |- + #!/bin/bash + yarn install:foundryup + - avd-manager@1: + inputs: + - api_level: '34' + - abi: 'x86_64' + - create_command_flags: --sdcard 8192M + - start_command_flags: -read-only + - profile: pixel_5 + - wait-for-android-emulator@1: {} + - script@1: + title: Run detox test + timeout: 1800 + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + export METAMASK_ENVIRONMENT='dev' + node -v + if [ -n "${E2E_TEST_FILE:-}" ]; then + echo "[INFO] Running only specified E2E_TEST_FILE(s): $E2E_TEST_FILE" + IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:android:run:qa-release $E2E_TEST_FILE + elif [ -n "${TEST_SUITE_TAG:-}" ]; then + echo "[INFO] Running tests matching TEST_SUITE_TAG: $TEST_SUITE_TAG" + ./tests/scripts/run-e2e-tags.sh + fi + - custom-test-results-export@1: + title: Export test results + is_always_run: true + is_skippable: true + inputs: + - base_path: $BITRISE_SOURCE_DIR/tests/reports/ + - test_name: E2E Tests + - search_pattern: $BITRISE_SOURCE_DIR/tests/reports/junit.xml + - deploy-to-bitrise-io@2.2.3: + title: Deploy test report files + is_always_run: true + is_skippable: true + - script@1: + title: Copy screenshot files + is_always_run: true + run_if: .IsBuildFailed + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + cp -r "$BITRISE_SOURCE_DIR/artifacts" "$BITRISE_DEPLOY_DIR" + - deploy-to-bitrise-io@2.3: + title: Deploy test screenshots + is_always_run: true + run_if: .IsBuildFailed + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR + - is_compress: true + - zip_name: E2E_Android_Failure_Artifacts + - script@1: + title: Copy performance results + is_always_run: true + run_if: '{{getenv "TEST_SUITE_TAG" | eq ".*SmokePerformance.*"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + # Create performance results directory + mkdir -p "$BITRISE_DEPLOY_DIR/performance-results" + + # Copy performance JSON files if they exist + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied account-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied network-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied switching-accounts-to-dismiss-load-testing-performance-results.json" + fi + - deploy-to-bitrise-io@2.3: + title: Deploy performance results + is_always_run: true + run_if: '{{getenv "TEST_SUITE_TAG" | eq ".*SmokePerformance.*"}}' + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR/performance-results + - is_compress: true + - zip_name: E2E_Performance_Results + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + + # Performance-specific Android E2E test workflow + android_e2e_test_performance: + before_run: + - setup + - prep_environment + after_run: + - notify_failure + steps: + - restore-gradle-cache@2: {} + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull Android build + - script@1: + title: Copy Android build for Detox + run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "false"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Create directories for Detox + mkdir -p "$BITRISE_SOURCE_DIR/android/app/build/outputs" + + # Copy saved files for Detox usage + # INTERMEDIATE_ANDROID_BUILD_DIR is the cached directory from android_e2e_build's "Save Android build" step + cp -r "$INTERMEDIATE_ANDROID_BUILD_DIR" "$BITRISE_SOURCE_DIR/android/app/build" + - restore-cache@2: + title: Restore cache node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - script@1: + title: Install foundry + inputs: + - content: |- + #!/bin/bash + yarn install:foundryup + - avd-manager@1: + inputs: + - api_level: '34' + - abi: 'x86_64' + - create_command_flags: --sdcard 8192M + - start_command_flags: -read-only + - profile: pixel_5 + - wait-for-android-emulator@1: {} + - script@1: + title: Run detox test + timeout: 1200 + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + + export METAMASK_ENVIRONMENT='e2e' + + if [ -n "${E2E_TEST_FILE:-}" ]; then + echo "[INFO] Running only specified E2E_TEST_FILE(s): $E2E_TEST_FILE" + IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:android:$METAMASK_BUILD_TYPE:prod $E2E_TEST_FILE + elif [ -n "${TEST_SUITE_TAG:-}" ]; then + echo "[INFO] Running tests matching TEST_SUITE_TAG: $TEST_SUITE_TAG" + ./tests/scripts/run-e2e-tags.sh + fi + - custom-test-results-export@1: + title: Export test results + is_always_run: true + is_skippable: true + inputs: + - base_path: $BITRISE_SOURCE_DIR/tests/reports/ + - test_name: E2E Tests + - search_pattern: $BITRISE_SOURCE_DIR/tests/reports/junit.xml + - deploy-to-bitrise-io@2.2.3: + title: Deploy test report files + is_always_run: true + is_skippable: true + - script@1: + title: Copy screenshot files + is_always_run: true + run_if: .IsBuildFailed + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + cp -r "$BITRISE_SOURCE_DIR/artifacts" "$BITRISE_DEPLOY_DIR" + - deploy-to-bitrise-io@2.3: + title: Deploy test screenshots + is_always_run: true + run_if: .IsBuildFailed + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR + - is_compress: true + - zip_name: E2E_Android_Failure_Artifacts + - script@1: + title: Copy performance results + is_always_run: true + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + # Create performance results directory + mkdir -p "$BITRISE_DEPLOY_DIR/performance-results" + + # Copy performance JSON files if they exist + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied account-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied network-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied switching-accounts-to-dismiss-load-testing-performance-results.json" + fi + - deploy-to-bitrise-io@2.3: + title: Deploy performance results + is_always_run: true + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR/performance-results + - is_compress: true + - zip_name: E2E_Performance_Results + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + + ios_api_specs: + before_run: + - setup + - code_setup + - install_applesimutils + - prep_environment + after_run: + - notify_failure + steps: + - restore-cache@2: + title: Restore iOS PR Build Cache (if build was skipped) + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "true"}}' + inputs: + - key: '{{ getenv "IOS_PR_BUILD_CACHE_KEY" }}' + - script@1: + title: Copy iOS build from cache (if build was skipped) + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "true"}}' + inputs: + - content: |- + #!/usr/bin/env bash + echo "Copying iOS build from cache..." + + # Check if cached build products exist + if [ -d "ios/build/Build/Products/Release-iphonesimulator" ]; then + echo "✅ iOS build artifacts found in cache" + echo "Build products directory contents:" + ls -la ios/build/Build/Products/Release-iphonesimulator/ | head -5 + else + echo "❌ iOS build products not found in cache" + mkdir -p ios/build/Build/Products/Release-iphonesimulator + fi + + # Check if cached Detox artifacts exist + if [ -d "../Library/Detox/ios" ]; then + echo "✅ Detox iOS artifacts found in cache" + echo "Detox directory contents:" + ls -la ../Library/Detox/ios/ | head -5 + else + echo "❌ Detox iOS artifacts not found in cache" + mkdir -p ../Library/Detox/ios + fi + + echo "iOS build artifacts restored from cache" + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull iOS build + - script@1: + title: Copy iOS build for Detox + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Create directories for Detox + mkdir -p "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + mkdir -p "$BITRISE_SOURCE_DIR/../Library/Detox/ios" + + # Copy saved files for Detox usage + # INTERMEDIATE_IOS_BUILD_DIR & INTERMEDIATE_IOS_DETOX_DIR are the cached directories by ios_e2e_build's "Save iOS build" step + cp -r "$INTERMEDIATE_IOS_BUILD_DIR" "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + cp -r "$INTERMEDIATE_IOS_DETOX_DIR" "$BITRISE_SOURCE_DIR/../Library/Detox" + # - restore-cocoapods-cache@2: {} + - restore-cache@2: + title: Restore cache node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - script@1: + title: Install foundry + inputs: + - content: |- + #!/bin/bash + yarn install:foundryup + - certificate-and-profile-installer@1: {} + - set-xcode-build-number@1: + inputs: + - build_short_version_string: $VERSION_NAME + - plist_path: $PROJECT_LOCATION_IOS/MetaMask/Info.plist + - script: + inputs: + - content: |- + # Add cache directory to environment variable + envman add --key BREW_APPLESIMUTILS --value "$(brew --cellar)/applesimutils" + envman add --key BREW_OPT_APPLESIMUTILS --value "/usr/local/opt/applesimutils" + brew tap wix/brew + title: Set Env Path for caching deps + - script@1: + title: Run detox test + timeout: 1800 + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + yarn test:api-specs --retries 1 + - script@1: + is_always_run: true + is_skippable: false + title: Add tests reports to Bitrise + inputs: + - content: |- + #!/usr/bin/env bash + cp -r $BITRISE_SOURCE_DIR/html-report/index.html $BITRISE_HTML_REPORT_DIR/ + - deploy-to-bitrise-io@2.2.3: + is_always_run: true + is_skippable: false + inputs: + - deploy_path: $BITRISE_HTML_REPORT_DIR + title: Deploy test report files + + pr_check_build_cache: + steps: + - activate-ssh-key@4: + run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' + - git-clone@6: {} + - restore-cache@2: + title: Restore last successful build commit marker + is_skippable: true + inputs: + - key: 'last-e2e-build-commit-pr-{{ getenv "GITHUB_PR_NUMBER" }}' + - script@1: + title: Generate cache keys and check both iOS and Android builds + inputs: + - content: |- + #!/usr/bin/env bash + ./scripts/generate-pr-cache-keys.sh + - restore-cache@2: + title: Check iOS cache + is_skippable: true + run_if: '{{getenv "IOS_PR_BUILD_CACHE_KEY" | ne ""}}' + inputs: + - key: '{{ getenv "IOS_PR_BUILD_CACHE_KEY" }}' + - script@1: + title: Process iOS cache result + run_if: '{{getenv "IOS_PR_BUILD_CACHE_KEY" | ne ""}}' + inputs: + - content: |- + #!/usr/bin/env bash + if [[ "$BITRISE_CACHE_HIT" == "exact" ]]; then + echo "✅ iOS cache found - build will be skipped" + envman add --key SKIP_IOS_BUILD --value "true" + else + echo "❌ iOS cache not found - build will proceed" + envman add --key SKIP_IOS_BUILD --value "false" + fi + envman add --key BITRISE_CACHE_HIT --value "" # Reset for next check + - restore-cache@2: + title: Check Android cache + is_skippable: true + run_if: '{{getenv "ANDROID_PR_BUILD_CACHE_KEY" | ne ""}}' + inputs: + - key: '{{ getenv "ANDROID_PR_BUILD_CACHE_KEY" }}' + - script@1: + title: Process Android cache result and finalize decisions + inputs: + - content: |- + #!/usr/bin/env bash + + # Initialize local variables with current state + LOCAL_SKIP_IOS="$SKIP_IOS_BUILD" + LOCAL_SKIP_ANDROID="$SKIP_ANDROID_BUILD" + + # Ensure iOS value is set if cache check was skipped + if [[ -z "$IOS_PR_BUILD_CACHE_KEY" ]]; then + echo "No iOS cache key available - build will proceed" + LOCAL_SKIP_IOS="false" + envman add --key SKIP_IOS_BUILD --value "false" + fi + + # Only process Android cache if we have cache keys + if [[ -n "$ANDROID_PR_BUILD_CACHE_KEY" ]]; then + if [[ "$BITRISE_CACHE_HIT" == "exact" ]]; then + echo "✅ Android cache found - build will be skipped" + LOCAL_SKIP_ANDROID="true" + envman add --key SKIP_ANDROID_BUILD --value "true" + else + echo "❌ Android cache not found - build will proceed" + LOCAL_SKIP_ANDROID="false" + envman add --key SKIP_ANDROID_BUILD --value "false" + fi + else + echo "No Android cache key available - build will proceed" + LOCAL_SKIP_ANDROID="false" + envman add --key SKIP_ANDROID_BUILD --value "false" + fi + + echo "" + echo "=== Final Build Decisions ===" + echo "iOS build: $([ "$LOCAL_SKIP_IOS" == "true" ] && echo "SKIP" || echo "BUILD")" + echo "Android build: $([ "$LOCAL_SKIP_ANDROID" == "true" ] && echo "SKIP" || echo "BUILD")" + - share-pipeline-variable@1: + title: Share variables across pipeline stages + inputs: + - variables: |- + SKIP_IOS_BUILD + SKIP_ANDROID_BUILD + IOS_PR_BUILD_CACHE_KEY + ANDROID_PR_BUILD_CACHE_KEY + + ios_e2e_build: + before_run: + - install_applesimutils + - code_setup + - set_commit_hash + after_run: + - notify_failure + steps: + - script@1: + title: Generating ccache key using native folder checksum + inputs: + - content: |- + #!/usr/bin/env bash + ./scripts/cache/set-cache-envs.sh ios + - certificate-and-profile-installer@1: {} + - script: + inputs: + - content: |- + # Add cache directory to environment variable + envman add --key BREW_APPLESIMUTILS --value "$(brew --cellar)/applesimutils" + envman add --key BREW_OPT_APPLESIMUTILS --value "/usr/local/opt/applesimutils" + brew tap wix/brew + title: Set Env Path for caching deps + - script@1: + title: Install CCache & symlink + inputs: + - content: |- + #!/usr/bin/env bash + brew install ccache with HOMEBREW_NO_DEPENDENTS_CHECK=1 + ln -s $(which ccache) /usr/local/bin/gcc + ln -s $(which ccache) /usr/local/bin/g++ + ln -s $(which ccache) /usr/local/bin/cc + ln -s $(which ccache) /usr/local/bin/c++ + ln -s $(which ccache) /usr/local/bin/clang + ln -s $(which ccache) /usr/local/bin/clang++ + - restore-cache@2: + title: Restore CCache + inputs: + - key: '{{ getenv "CCACHE_KEY" }}' + - script@1: + title: Set skip ccache upload + run_if: '{{ enveq "BITRISE_CACHE_HIT" "exact" }}' + inputs: + - content: |- + #!/usr/bin/env bash + envman add --key SKIP_CCACHE_UPLOAD --value "true" + - script@1: + title: "[Phase 1.5] Verify builds.yml config" + is_skippable: true + inputs: + - content: |- + #!/usr/bin/env bash + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Phase 1.5: Parallel Validation ║" + echo "║ Comparing Bitrise env vars with builds.yml config ║" + echo "╚════════════════════════════════════════════════════════════╝" + + # Set defaults if not already set + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-'e2e'} + + # Run verification (auto-detects build from env vars) + # Using --verbose to see all checks in build logs + node scripts/verify-build-config.js --verbose || true + + # Note: Currently running without --strict + # Once verified, change to: node scripts/verify-build-config.js --strict + - script@1: + title: Run detox build + timeout: 1800 + is_always_run: true + inputs: + - content: |- + #!/usr/bin/env bash + ./scripts/cache/setup-ccache.sh + node -v + if [ -n "$COMMAND_YARN" ]; then + GIT_BRANCH=$BITRISE_GIT_BRANCH yarn "$COMMAND_YARN" + else + echo "No COMMAND_YARN provided. Running yarn build:ios:main:e2e..." + yarn build:ios:main:e2e + fi + - save-cocoapods-cache@1: {} + - save-cache@1: + title: Save CCache + run_if: '{{not (enveq "SKIP_CCACHE_UPLOAD" "true")}}' + inputs: + - key: '{{ getenv "CCACHE_KEY" }}' + - paths: |- + ccache + - save-cache@1: + title: Save iOS PR Build Cache + inputs: + - key: '{{ getenv "IOS_PR_BUILD_CACHE_KEY" }}' + - paths: |- + ios/build/Build/Products/Release-iphonesimulator + ../Library/Detox/ios + - script@1: + title: Save last successful build commit + run_if: '{{getenv "GITHUB_PR_NUMBER" | ne ""}}' + inputs: + - content: |- + #!/usr/bin/env bash + # Create a marker file with the current commit + mkdir -p /tmp/last-build-commit + echo "$(git rev-parse HEAD 2>/dev/null || echo ${BITRISE_GIT_COMMIT})" > /tmp/last-build-commit/commit + echo "Build completed successfully at $(date)" >> /tmp/last-build-commit/commit + - save-cache@1: + title: Save last successful build commit marker + run_if: '{{getenv "GITHUB_PR_NUMBER" | ne ""}}' + inputs: + - key: 'last-e2e-build-commit-pr-{{ getenv "GITHUB_PR_NUMBER" }}' + - paths: /tmp/last-build-commit + - deploy-to-bitrise-io@2.2.3: + inputs: + - pipeline_intermediate_files: |- + ios/build/Build/Products/Release-iphonesimulator:INTERMEDIATE_IOS_BUILD_DIR + ../Library/Detox/ios:INTERMEDIATE_IOS_DETOX_DIR + title: Save iOS build + - save-cache@1: + title: Save node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - paths: node_modules + ios_e2e_test: + before_run: + - setup + - code_setup + - install_applesimutils + - prep_environment + after_run: + - notify_failure + steps: + - restore-cache@2: + title: Restore iOS PR Build Cache (if build was skipped) + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "true"}}' + inputs: + - key: '{{ getenv "IOS_PR_BUILD_CACHE_KEY" }}' + - script@1: + title: Copy iOS build from cache (if build was skipped) + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "true"}}' + inputs: + - content: |- + #!/usr/bin/env bash + echo "Copying iOS build from cache..." + + # Check if cached build products exist + if [ -d "ios/build/Build/Products/Release-iphonesimulator" ]; then + echo "✅ iOS build artifacts found in cache" + echo "Build products directory contents:" + ls -la ios/build/Build/Products/Release-iphonesimulator/ | head -5 + else + echo "❌ iOS build products not found in cache" + mkdir -p ios/build/Build/Products/Release-iphonesimulator + fi + + # Check if cached Detox artifacts exist + if [ -d "../Library/Detox/ios" ]; then + echo "✅ Detox iOS artifacts found in cache" + echo "Detox directory contents:" + ls -la ../Library/Detox/ios/ | head -5 + else + echo "❌ Detox iOS artifacts not found in cache" + mkdir -p ../Library/Detox/ios + fi + + echo "iOS build artifacts restored from cache" + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull iOS build + - script@1: + title: Copy iOS build for Detox + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Create directories for Detox + mkdir -p "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + mkdir -p "$BITRISE_SOURCE_DIR/../Library/Detox/ios" + + # Copy saved files for Detox usage + # INTERMEDIATE_IOS_BUILD_DIR & INTERMEDIATE_IOS_DETOX_DIR are the cached directories by ios_e2e_build's "Save iOS build" step + cp -r "$INTERMEDIATE_IOS_BUILD_DIR" "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + cp -r "$INTERMEDIATE_IOS_DETOX_DIR" "$BITRISE_SOURCE_DIR/../Library/Detox" + # - restore-cocoapods-cache@2: {} + - restore-cache@2: + title: Restore cache node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - script@1: + title: Install foundry + inputs: + - content: |- + #!/bin/bash + yarn install:foundryup + - certificate-and-profile-installer@1: {} + - set-xcode-build-number@1: + inputs: + - build_short_version_string: $VERSION_NAME + - plist_path: $PROJECT_LOCATION_IOS/MetaMask/Info.plist + - script: + inputs: + - content: |- + # Add cache directory to environment variable + envman add --key BREW_APPLESIMUTILS --value "$(brew --cellar)/applesimutils" + envman add --key BREW_OPT_APPLESIMUTILS --value "/usr/local/opt/applesimutils" + brew tap wix/brew + title: Set Env Path for caching deps + - script@1: + title: Boot up simulator + inputs: + - content: |- + #!/usr/bin/env bash + xcrun simctl boot "iPhone 15 Pro" || true + xcrun simctl list | grep Booted + - script@1: + title: Run detox test + timeout: 1800 + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + + # node -v + export METAMASK_ENVIRONMENT='dev' + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + # if [ "$METAMASK_BUILD_TYPE" = "flask" ]; then + # IS_TEST='true' METAMASK_BUILD_TYPE='flask' yarn test:e2e:ios:run:qa-release e2e/specs/flask/ + # else + # ./tests/scripts/run-e2e-tags.sh + # fi + if [ -n "${E2E_TEST_FILE:-}" ]; then + echo "[INFO] Running only specified E2E_TEST_FILE(s): $E2E_TEST_FILE" + IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:ios:run:qa-release $E2E_TEST_FILE + elif [ -n "${TEST_SUITE_TAG:-}" ]; then + echo "[INFO] Running tests matching TEST_SUITE_TAG: $TEST_SUITE_TAG" + ./tests/scripts/run-e2e-tags.sh + fi + - custom-test-results-export@1: + is_always_run: true + is_skippable: false + title: Export test results + inputs: + - base_path: $BITRISE_SOURCE_DIR/tests/reports/ + - test_name: E2E Tests + - search_pattern: $BITRISE_SOURCE_DIR/tests/reports/junit.xml + - deploy-to-bitrise-io@2.2.3: + is_always_run: true + is_skippable: true + title: Deploy test report files + - script@1: + is_always_run: true + run_if: .IsBuildFailed + title: Copy screenshot files + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + cp -r "$BITRISE_SOURCE_DIR/artifacts" "$BITRISE_DEPLOY_DIR" + - deploy-to-bitrise-io@2.3: + is_always_run: true + run_if: .IsBuildFailed + title: Deploy test screenshots + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR + - is_compress: true + - zip_name: 'E2E_IOS_Failure_Artifacts' + - script@1: + title: Copy performance results + is_always_run: true + run_if: '{{getenv "TEST_SUITE_TAG" | eq ".*SmokePerformance.*"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + # Create performance results directory + mkdir -p "$BITRISE_DEPLOY_DIR/performance-results" + + # Copy performance JSON files if they exist + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied account-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied network-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied switching-accounts-to-dismiss-load-testing-performance-results.json" + fi + - deploy-to-bitrise-io@2.3: + title: Deploy performance results + is_always_run: true + run_if: '{{getenv "TEST_SUITE_TAG" | eq ".*SmokePerformance.*"}}' + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR/performance-results + - is_compress: true + - zip_name: E2E_Performance_Results + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + + # Performance-specific iOS E2E test workflow + ios_e2e_test_performance: + before_run: + - setup + - install_applesimutils + - prep_environment + after_run: + - notify_failure + steps: + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull iOS build + - script@1: + title: Copy iOS build for Detox + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Create directories for Detox + mkdir -p "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + mkdir -p "$BITRISE_SOURCE_DIR/../Library/Detox/ios" + + # Copy saved files for Detox usage + # INTERMEDIATE_IOS_BUILD_DIR & INTERMEDIATE_IOS_DETOX_DIR are the cached directories by ios_e2e_build's "Save iOS build" step + cp -r "$INTERMEDIATE_IOS_BUILD_DIR" "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + cp -r "$INTERMEDIATE_IOS_DETOX_DIR" "$BITRISE_SOURCE_DIR/../Library/Detox" + # - restore-cocoapods-cache@2: {} + - restore-cache@2: + title: Restore cache node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - script@1: + title: Install foundry + inputs: + - content: |- + #!/bin/bash + yarn install:foundryup + - certificate-and-profile-installer@1: {} + - set-xcode-build-number@1: + inputs: + - build_short_version_string: $VERSION_NAME + - plist_path: $PROJECT_LOCATION_IOS/MetaMask/MetaMask-QA-Info.plist + - script: + inputs: + - content: |- + # Add cache directory to environment variable + envman add --key BREW_APPLESIMUTILS --value "$(brew --cellar)/applesimutils" + envman add --key BREW_OPT_APPLESIMUTILS --value "/usr/local/opt/applesimutils" + brew tap wix/brew + title: Set Env Path for caching deps + - script@1: + title: Boot up simulator + inputs: + - content: |- + #!/usr/bin/env bash + xcrun simctl boot "iPhone 15 Pro" || true + xcrun simctl list | grep Booted + - script@1: + title: Run detox test + timeout: 1200 + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + + export METAMASK_ENVIRONMENT='e2e' + + if [ -n "${E2E_TEST_FILE:-}" ]; then + echo "[INFO] Running only specified E2E_TEST_FILE(s): $E2E_TEST_FILE" + IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:ios:$METAMASK_BUILD_TYPE:prod $E2E_TEST_FILE + elif [ -n "${TEST_SUITE_TAG:-}" ]; then + echo "[INFO] Running tests matching TEST_SUITE_TAG: $TEST_SUITE_TAG" + ./tests/scripts/run-e2e-tags.sh + fi + - custom-test-results-export@1: + is_always_run: true + is_skippable: false + title: Export test results + inputs: + - base_path: $BITRISE_SOURCE_DIR/tests/reports/ + - test_name: E2E Tests + - search_pattern: $BITRISE_SOURCE_DIR/tests/reports/junit.xml + - deploy-to-bitrise-io@2.2.3: + is_always_run: true + is_skippable: true + title: Deploy test report files + - script@1: + is_always_run: true + run_if: .IsBuildFailed + title: Copy screenshot files + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + cp -r "$BITRISE_SOURCE_DIR/artifacts" "$BITRISE_DEPLOY_DIR" + - deploy-to-bitrise-io@2.3: + is_always_run: true + run_if: .IsBuildFailed + title: Deploy test screenshots + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR + - is_compress: true + - zip_name: 'E2E_IOS_Failure_Artifacts' + - script@1: + title: Copy performance results + is_always_run: true + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + # Create performance results directory + mkdir -p "$BITRISE_DEPLOY_DIR/performance-results" + + # Copy performance JSON files if they exist + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied account-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied network-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied switching-accounts-to-dismiss-load-testing-performance-results.json" + fi + - deploy-to-bitrise-io@2.3: + title: Deploy performance results + is_always_run: true + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR/performance-results + - is_compress: true + - zip_name: E2E_Performance_Results + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + + start_e2e_tests: + steps: + - build-router-start@0: + inputs: + - workflows: |- + ios_e2e_test + - wait_for_builds: 'true' + - access_token: $BITRISE_START_BUILD_ACCESS_TOKEN + - build-router-wait@0: + inputs: + - abort_on_fail: 'yes' + - access_token: $BITRISE_START_BUILD_ACCESS_TOKEN + # Runway Workflow for Release Candidate Builds + runway_build_release_candidate: + before_run: + - bump_version_code + after_run: + - build_ios_release_and_upload_sourcemaps + - build_android_release_and_upload_sourcemaps + # Android Builds + _android_build_template: + before_run: + - code_setup + - extract_version_info + after_run: + - notify_failure + steps: + - file-downloader@1: + inputs: + - source: $KEYSTORE_FILE_PATH + - destination: $KEYSTORE_PATH + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + # TapAndPay SDK Setup + - script@1: + title: Clone TapAndPay SDK + inputs: + - content: |- + #!/usr/bin/env bash + set -euo pipefail + mkdir -p ~/.ssh + echo "$TAP_AND_PAY_SDK_SSH_KEY" | base64 -d | tr -d '\r' > ~/.ssh/tap_and_pay_key + echo "" >> ~/.ssh/tap_and_pay_key + chmod 600 ~/.ssh/tap_and_pay_key + ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null + eval "$(ssh-agent -s)" + ssh-add ~/.ssh/tap_and_pay_key + echo "Cloning TapAndPay SDK into android/libs/..." + git clone --depth 1 git@github.com:MetaMask/tap-and-pay-sdk.git /tmp/tap-and-pay-sdk + mkdir -p android/libs + cp -r /tmp/tap-and-pay-sdk/* android/libs/ + rm -rf /tmp/tap-and-pay-sdk + rm -f ~/.ssh/tap_and_pay_key + echo "TapAndPay SDK installed to android/libs/" + - restore-gradle-cache@2: {} + - install-missing-android-tools@3: + inputs: + - ndk_version: $NDK_VERSION + - gradlew_path: $PROJECT_LOCATION/gradlew + # Note - This step will fail if stack is not Linux + - script@1: + title: Install ICU libraries for Hermes + inputs: + - content: |- + #!/usr/bin/env bash + sudo apt update + sudo apt install libicu-dev -y + - script@1: + title: "[Phase 1.5] Verify builds.yml config" + is_skippable: true + inputs: + - content: |- + #!/usr/bin/env bash + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Phase 1.5: Parallel Validation ║" + echo "║ Comparing Bitrise env vars with builds.yml config ║" + echo "╚════════════════════════════════════════════════════════════╝" + + # Set defaults if not already set + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-'production'} + + # Run verification (auto-detects build from env vars) + # Using --verbose to see all checks in build logs + node scripts/verify-build-config.js --verbose || true + + # Note: Currently running without --strict + # Once verified, change to: node scripts/verify-build-config.js --strict + - script@1: + title: Build Android Binary + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + node -v + if [ -n "$COMMAND_YARN" ]; then + GIT_BRANCH=$BITRISE_GIT_BRANCH yarn "$COMMAND_YARN" + else + echo "No COMMAND_YARN provided" + exit 1 + fi + - deploy-to-bitrise-io@2.2.3: + title: Share Detox files between pipelines + run_if: '{{getenv "SHARE_WITH_DETOX" | eq "true"}}' + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: android/app/build/outputs:INTERMEDIATE_ANDROID_BUILD_DIR + - save-gradle-cache@1: {} + - script@1: + title: Rename release files + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Set base paths for release builds + if [ "$IS_DEV_BUILD" = "true" ]; then + APK_DIR="$PROJECT_LOCATION/app/build/outputs/apk/$APP_NAME/debug" + else + APK_DIR="$PROJECT_LOCATION/app/build/outputs/apk/$APP_NAME/release" + BUNDLE_DIR="$PROJECT_LOCATION/app/build/outputs/bundle/$OUTPUT_PATH" + fi + + # Generate new names based on build type and version + if [ -n "$COMMAND_YARN" ]; then + NAME_FROM_YARN_COMMAND="$(cut -d':' -f3- <<< "$COMMAND_YARN" | sed 's/:/-/g')" + NEW_BASE_NAME="metamask-${NAME_FROM_YARN_COMMAND}-${APP_BUILD_NUMBER}" + else + NEW_BASE_NAME="metamask-${METAMASK_ENVIRONMENT}-${METAMASK_BUILD_TYPE}-${APP_SEM_VER_NAME}-${APP_BUILD_NUMBER}" + fi + + # Rename APK + if [ "$IS_DEV_BUILD" = "true" ]; then + OLD_APK="$APK_DIR/app-$APP_NAME-debug.apk" + OLD_AAB="$BUNDLE_DIR/app-$APP_NAME-debug.aab" + else + OLD_APK="$APK_DIR/app-$APP_NAME-release.apk" + OLD_AAB="$BUNDLE_DIR/app-$APP_NAME-release.aab" + fi + + NEW_APK="$APK_DIR/$NEW_BASE_NAME.apk" + cp "$OLD_APK" "$NEW_APK" + + # Rename AAB + if [ -n "$BUNDLE_DIR" ]; then + NEW_AAB="$BUNDLE_DIR/$NEW_BASE_NAME.aab" + cp "$OLD_AAB" "$NEW_AAB" + fi + + # Export new names as environment variables + envman add --key RENAMED_APK_FILE --value "$NEW_BASE_NAME.apk" + envman add --key RENAMED_AAB_FILE --value "$NEW_BASE_NAME.aab" + envman add --key APK_DEPLOY_PATH --value "$APK_DIR/$NEW_BASE_NAME.apk" + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - deploy_path: $APK_DEPLOY_PATH + title: Bitrise Deploy APK + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + inputs: + - pipeline_intermediate_files: $PROJECT_LOCATION/app/build/outputs/apk/$APP_NAME/release/sha512sums.txt:BITRISE_PLAY_STORE_SHA512SUMS_PATH + - deploy_path: $PROJECT_LOCATION/app/build/outputs/apk/$APP_NAME/release/sha512sums.txt + title: Bitrise Deploy Checksum + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + inputs: + - pipeline_intermediate_files: $PROJECT_LOCATION/app/build/outputs/mapping/$OUTPUT_PATH/mapping.txt:BITRISE_PLAY_STORE_MAPPING_PATH + - deploy_path: $PROJECT_LOCATION/app/build/outputs/mapping/$OUTPUT_PATH/mapping.txt + title: Bitrise ProGuard Map Files + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + inputs: + - pipeline_intermediate_files: $PROJECT_LOCATION/app/build/outputs/bundle/$OUTPUT_PATH/$RENAMED_AAB_FILE:BITRISE_PLAY_STORE_ABB_PATH + - deploy_path: $PROJECT_LOCATION/app/build/outputs/bundle/$OUTPUT_PATH/$RENAMED_AAB_FILE + title: Bitrise Deploy AAB + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + inputs: + - deploy_path: $PROJECT_LOCATION/app/build/generated/sourcemaps/react/$OUTPUT_PATH + - is_compress: true + - zip_name: Android_Sourcemaps_$OUTPUT_PATH + title: Deploy Android Sourcemaps + - script@1: + title: Prepare Android build outputs for caching + run_if: '{{and (getenv "ANDROID_PR_BUILD_CACHE_KEY" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - content: |- + #!/usr/bin/env bash + echo "=== Preparing Android cache ===" + echo "Current working directory: $(pwd)" + + # Create a clean cache directory structure + mkdir -p /tmp/android-cache/build/outputs + + if [ -d "android/app/build/outputs" ]; then + echo "Copying Android build outputs to cache staging area..." + cp -r android/app/build/outputs/* /tmp/android-cache/build/outputs/ + echo "Cache staging completed" + + echo "Cache contents:" + find /tmp/android-cache -type f | head -10 + echo "Total cache size: $(du -sh /tmp/android-cache 2>/dev/null || echo 'Unknown')" + else + echo "Warning: android/app/build/outputs not found!" + fi + - save-cache@1: + title: Save Android PR Build Cache + run_if: '{{and (getenv "ANDROID_PR_BUILD_CACHE_KEY" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - key: '{{ getenv "ANDROID_PR_BUILD_CACHE_KEY" }}' + - paths: |- + /tmp/android-cache + - script@1: + title: Save last successful build commit (Android) + run_if: '{{and (getenv "GITHUB_PR_NUMBER" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - content: |- + #!/usr/bin/env bash + # Create a marker file with the current commit + mkdir -p /tmp/last-build-commit + echo "$(git rev-parse HEAD 2>/dev/null || echo ${BITRISE_GIT_COMMIT})" > /tmp/last-build-commit/commit + echo "Build completed successfully at $(date)" >> /tmp/last-build-commit/commit + - save-cache@1: + title: Save last successful build commit marker (Android) + run_if: '{{and (getenv "GITHUB_PR_NUMBER" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - key: 'last-e2e-build-commit-pr-{{ getenv "GITHUB_PR_NUMBER" }}' + - paths: /tmp/last-build-commit + + # Template for E2E Android builds + + _android_e2e_build_template: + before_run: + - code_setup + - set_commit_hash + after_run: + - notify_failure + steps: + - script@1: + title: Generating ccache key using native folder checksum + inputs: + - content: |- + #!/usr/bin/env bash + ./scripts/cache/set-cache-envs.sh android + - restore-gradle-cache@2: {} + - install-missing-android-tools@3: + inputs: + - ndk_version: $NDK_VERSION + - gradlew_path: $PROJECT_LOCATION/gradlew + - file-downloader@1: + inputs: + - source: $KEYSTORE_URL + - destination: $KEYSTORE_PATH + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + # Note - This step will fail if stack is not Linux + - script@1: + title: Install CCache, ICU libraries & symlink + inputs: + - content: |- + #!/usr/bin/env bash + sudo apt update + sudo apt install ccache libicu-dev -y + - restore-cache@2: + title: Restore CCache + inputs: + - key: '{{ getenv "CCACHE_KEY" }}' + - script@1: + title: Set skip ccache upload + run_if: '{{ enveq "BITRISE_CACHE_HIT" "exact" }}' + inputs: + - content: |- + #!/usr/bin/env bash + envman add --key SKIP_CCACHE_UPLOAD --value "true" + - script@1: + title: "[Phase 1.5] Verify builds.yml config" + is_skippable: true + inputs: + - content: |- + #!/usr/bin/env bash + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Phase 1.5: Parallel Validation ║" + echo "║ Comparing Bitrise env vars with builds.yml config ║" + echo "╚════════════════════════════════════════════════════════════╝" + + # Set defaults if not already set + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-'e2e'} + + # Run verification (auto-detects build from env vars) + # Using --verbose to see all checks in build logs + node scripts/verify-build-config.js --verbose || true + + # Note: Currently running without --strict + # Once verified, change to: node scripts/verify-build-config.js --strict + - script@1: + title: Run detox build + timeout: 1800 + is_always_run: true + inputs: + - content: |- + #!/usr/bin/env bash + ./scripts/cache/setup-ccache.sh + node -v + export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-'dev'} + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + IGNORE_BOXLOGS_DEVELOPMENT="true" $BUILD_COMMAND + - save-gradle-cache@1: {} + - save-cache@1: + title: Save CCache + run_if: '{{not (enveq "SKIP_CCACHE_UPLOAD" "true")}}' + inputs: + - key: '{{ getenv "CCACHE_KEY" }}' + - paths: |- + ccache + - script@1: + title: Debug Android build outputs before caching + inputs: + - content: |- + #!/usr/bin/env bash + echo "=== Android Build Output Debug ===" + echo "Checking for Android build outputs to cache..." + + if [ -d "android/app/build/outputs" ]; then + echo "✅ android/app/build/outputs directory exists" + echo "Directory size: $(du -sh android/app/build/outputs 2>/dev/null || echo 'Could not calculate')" + echo "Contents:" + find android/app/build/outputs -type f -name "*.apk" -o -name "*.aab" | head -10 + echo "Total files: $(find android/app/build/outputs -type f | wc -l)" + else + echo "❌ android/app/build/outputs directory does not exist!" + echo "This means the Android build failed or output path is incorrect" + fi + + echo "Current working directory: $(pwd)" + echo "Listing android/app/build directory:" + ls -la android/app/build/ 2>/dev/null || echo "android/app/build does not exist" + - script@1: + title: Prepare Android build outputs for caching + inputs: + - content: |- + #!/usr/bin/env bash + echo "=== Preparing Android cache with proper paths ===" + echo "Current working directory: $(pwd)" + + # Create a clean cache directory structure + mkdir -p /tmp/android-cache/build/outputs + + if [ -d "android/app/build/outputs" ]; then + echo "Copying Android build outputs to cache staging area..." + cp -r android/app/build/outputs/* /tmp/android-cache/build/outputs/ + echo "Cache staging completed" + + echo "Cache contents:" + find /tmp/android-cache -type f | head -10 + echo "Total cache size: $(du -sh /tmp/android-cache 2>/dev/null || echo 'Unknown')" + else + echo "Warning: android/app/build/outputs not found!" + fi + - save-cache@1: + title: Save Android PR Build Cache + inputs: + - key: '{{ getenv "ANDROID_PR_BUILD_CACHE_KEY" }}' + - paths: |- + /tmp/android-cache + - script@1: + title: Save last successful build commit (Android) + run_if: '{{getenv "GITHUB_PR_NUMBER" | ne ""}}' + inputs: + - content: |- + #!/usr/bin/env bash + # Create a marker file with the current commit + mkdir -p /tmp/last-build-commit + echo "$(git rev-parse HEAD 2>/dev/null || echo ${BITRISE_GIT_COMMIT})" > /tmp/last-build-commit/commit + echo "Android build completed successfully at $(date)" >> /tmp/last-build-commit/commit + - save-cache@1: + title: Save last successful build commit marker (Android) + run_if: '{{getenv "GITHUB_PR_NUMBER" | ne ""}}' + inputs: + - key: 'last-e2e-build-commit-pr-{{ getenv "GITHUB_PR_NUMBER" }}' + - paths: /tmp/last-build-commit + - deploy-to-bitrise-io@2.2.3: + inputs: + - pipeline_intermediate_files: android/app/build/outputs:INTERMEDIATE_ANDROID_BUILD_DIR + title: Save Android build + - save-cache@1: + title: Save node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - paths: node_modules + # Actual workflows that inherit from templates + # TODO: Remove this workflow once new build configuration is consolidated + build_android_release: + after_run: + - build_android_main_prod + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_flask_prod: + envs: + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_FLASK_KEYSTORE_URL_URL + - KEYSTORE_PATH: 'android/keystores/flaskRelease.keystore' + - APP_NAME: 'flask' + - OUTPUT_PATH: 'flaskRelease' + - COMMAND_YARN: 'build:android:flask:prod' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_flask_test: + envs: + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_FLASK_UAT_URL + - KEYSTORE_PATH: 'android/keystores/flask-uat.keystore' + - APP_NAME: 'flask' + - OUTPUT_PATH: 'flaskRelease' + - COMMAND_YARN: 'build:android:flask:test' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_flask_e2e: + envs: + - COMMAND_YARN: 'build:android:flask:e2e' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_FLASK_UAT_URL + - KEYSTORE_PATH: 'android/keystores/flask-uat.keystore' + - APP_NAME: 'flask' + - OUTPUT_PATH: 'flaskRelease' + - SHARE_WITH_DETOX: 'true' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_prod: + envs: + - CONFIGURATION: 'Release' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/release.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + - COMMAND_YARN: 'build:android:main:prod' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_beta: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:android:main:beta' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_MAIN_RC_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/rc.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_rc: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:android:main:rc' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_MAIN_RC_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/rc.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_test: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:android:main:test' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_QA_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/internalRelease.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_e2e: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:android:main:e2e' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_QA_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/internalRelease.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + - SHARE_WITH_DETOX: 'true' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_exp: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:android:main:exp' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_QA_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/internalRelease.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_release_and_upload_sourcemaps: + envs: + - SENTRY_DISABLE_AUTO_UPLOAD: 'false' + after_run: + - build_android_main_prod + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_rc_and_upload_sourcemaps: + envs: + - SENTRY_DISABLE_AUTO_UPLOAD: 'false' + after_run: + - build_android_main_rc + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow once new build configuration is consolidated + build_android_flask_release: + after_run: + - build_android_flask_prod + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_dev: + envs: + - CONFIGURATION: 'Debug' + - IS_DEV_BUILD: 'true' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: 'prod' + - BUILD_COMMAND: 'yarn build:android:main:dev' + after_run: + - _android_e2e_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow once new build configuration is consolidated + build_android_devbuild: + after_run: + - build_android_main_dev + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_flask_dev: + envs: + - IS_DEV_BUILD: 'true' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - APP_NAME: 'flask' + - BUILD_COMMAND: 'yarn build:android:flask:dev' + after_run: + - _android_e2e_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow once new build configuration is consolidated + build_android_flask_devbuild: + after_run: + - build_android_flask_dev + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_qa_dev: + envs: + - IS_DEV_BUILD: 'true' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: 'qa' + - BUILD_COMMAND: 'yarn build:android:qa:dev' + after_run: + - _android_e2e_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow once new build configuration is consolidated + build_android_qa_devbuild: + after_run: + - build_android_qa_dev + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_qa_prod: + envs: + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_QA_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/internalRelease.keystore' + - APP_NAME: 'qa' + - OUTPUT_PATH: 'qaRelease' + - COMMAND_YARN: 'build:android:qa:prod' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow once new build configuration is consolidated + build_android_qa: + after_run: + - build_android_qa_prod + - _upload_apk_to_browserstack_qa + _upload_apk_to_browserstack_flask: + steps: + - script@1: + title: Upload Flask APK to Browserstack + inputs: + - content: |- + #!/usr/bin/env bash + set -e + set -x + set -o pipefail + APK_PATH=$PROJECT_LOCATION/app/build/outputs/apk/flask/release/app-flask-release.apk + CUSTOM_ID="flask-$BITRISE_GIT_BRANCH-$FLASK_VERSION_NAME-$FLASK_VERSION_NUMBER" + CUSTOM_ID=${CUSTOM_ID////-} + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@$APK_PATH" -F 'data={"custom_id": "'$CUSTOM_ID'"}' | jq -j '.app_url' | envman add --key BROWSERSTACK_ANDROID_FLASK_APP_URL + APK_PATH_FOR_APP_LIVE=$PROJECT_LOCATION/app/build/outputs/apk/flask/release/"$CUSTOM_ID".apk + cp "$APK_PATH" "$APK_PATH_FOR_APP_LIVE" + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@$APK_PATH_FOR_APP_LIVE" -F 'data={"custom_id": "'$CUSTOM_ID'"}' + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X GET https://api-cloud.browserstack.com/app-automate/recent_apps | jq > browserstack_uploaded_flask_apps.json + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: $BITRISE_SOURCE_DIR/browserstack_uploaded_flask_apps.json:BROWSERSTACK_UPLOADED_FLASK_APPS_LIST + title: Save Browserstack uploaded Flask apps JSON + _upload_apk_to_browserstack_qa: + steps: + - script@1: + title: Upload APK to Browserstack + inputs: + - content: |- + #!/usr/bin/env bash + set -e + set -x + set -o pipefail + APK_DIR="$PROJECT_LOCATION/app/build/outputs/apk/qa/release" + ORIGINAL_APK="$APK_DIR/app-qa-release.apk" + + CUSTOM_ID="$BITRISE_GIT_BRANCH-$VERSION_NAME-$VERSION_NUMBER" + CUSTOM_ID=${CUSTOM_ID////-} + + cp "$ORIGINAL_APK" "$APK_DIR/$CUSTOM_ID.apk" + APK_PATH="$APK_DIR/$CUSTOM_ID.apk" + + # Upload to app-automate + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ + -F "file=@$APK_PATH" \ + -F 'data={"custom_id": "'$CUSTOM_ID'"}' \ + | jq -j '.app_url' \ + | envman add --key BROWSERSTACK_ANDROID_APP_URL + + # Upload to app-live + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-live/upload" \ + -F "file=@$APK_PATH" \ + -F 'data={"custom_id": "'$CUSTOM_ID'"}' + + # Get recent apps + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X GET https://api-cloud.browserstack.com/app-automate/recent_apps \ + | jq > browserstack_uploaded_apps.json + - share-pipeline-variable@1: + title: Persist BROWSERSTACK_ANDROID_APP_URL across all stages + inputs: + - variables: |- + BROWSERSTACK_ANDROID_APP_URL + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: $BITRISE_SOURCE_DIR/browserstack_uploaded_apps.json:BROWSERSTACK_UPLOADED_APPS_LIST + title: Save Browserstack uploaded apps JSON + deploy_android_to_store: + steps: + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + - google-play-deploy: + inputs: + - app_path: $BITRISE_PLAY_STORE_ABB_PATH + - track: internal + - service_account_json_key_path: $BITRISEIO_BITRISEIO_SERVICE_ACCOUNT_JSON_KEY_URL_URL + - package_name: $MM_ANDROID_PACKAGE_NAME + envs: + - opts: + is_expand: true + MM_ANDROID_PACKAGE_NAME: io.metamask + deploy_ios_to_store: + steps: + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + - deploy-to-itunesconnect-application-loader@1: + inputs: + - ipa_path: $BITRISE_APP_STORE_IPA_PATH + # iOS Builds + _ios_build_template: + before_run: + - code_setup + - extract_version_info + after_run: + - notify_failure + steps: + - certificate-and-profile-installer@1: { + run_if: '{{not (enveq "IS_SIM_BUILD" "true")}}' # Only run for physical builds + } + - script@1: + title: "[Phase 1.5] Verify builds.yml config" + is_skippable: true + inputs: + - content: |- + #!/usr/bin/env bash + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Phase 1.5: Parallel Validation ║" + echo "║ Comparing Bitrise env vars with builds.yml config ║" + echo "╚════════════════════════════════════════════════════════════╝" + + # Set defaults if not already set + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-'production'} + + # Run verification (auto-detects build from env vars) + # Using --verbose to see all checks in build logs + node scripts/verify-build-config.js --verbose || true + + # Note: Currently running without --strict + # Once verified, change to: node scripts/verify-build-config.js --strict + - script@1: + title: iOS Sourcemaps & Build + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + echo 'This is the current build type: $METAMASK_BUILD_TYPE' + if [ -n "$COMMAND_YARN" ]; then + GIT_BRANCH=$BITRISE_GIT_BRANCH yarn "$COMMAND_YARN" + else + echo "No COMMAND_YARN provided" + exit 1 + fi + - deploy-to-bitrise-io@2.2.3: + title: Share Detox files between pipelines + run_if: '{{getenv "SHARE_WITH_DETOX" | eq "true"}}' + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: |- + ios/build/Build/Products/$CONFIGURATION-iphonesimulator:INTERMEDIATE_IOS_BUILD_DIR + ../Library/Detox/ios:INTERMEDIATE_IOS_DETOX_DIR + - script@1: + title: Rename iOS artifact files + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Set base paths + if [ "$IS_SIM_BUILD" = "true" ]; then + BUILD_DIR="ios/build/Build/Products/${CONFIGURATION}-iphonesimulator" + DEVICE_TYPE="simulator" + BINARY_EXTENSION=".app" + else + BUILD_DIR="ios/build/output" + DEVICE_TYPE="device" + BINARY_EXTENSION=".ipa" + fi + + # Generate new name based on build type and version + if [ -n "$COMMAND_YARN" ]; then + NAME_FROM_YARN_COMMAND="$(cut -d':' -f3- <<< "$COMMAND_YARN" | sed 's/:/-/g')" + NEW_BASE_NAME="metamask-${DEVICE_TYPE}-${NAME_FROM_YARN_COMMAND}-${APP_BUILD_NUMBER}" + else + NEW_BASE_NAME="metamask-${DEVICE_TYPE}-${METAMASK_ENVIRONMENT}-${METAMASK_BUILD_TYPE}-${APP_SEM_VER_NAME}-${APP_BUILD_NUMBER}" + fi + + # Copy binary with new name (preserve original) + OLD_BINARY="$BUILD_DIR/$APP_NAME$BINARY_EXTENSION" + NEW_BINARY="$BUILD_DIR/$NEW_BASE_NAME$BINARY_EXTENSION" + # Need to copy recursively so that .app files are fully copied + cp -r "$OLD_BINARY" "$NEW_BINARY" + + # Copy xcarchive with new name (only for non-simulator builds, preserve original) + if [ "$IS_SIM_BUILD" != "true" ]; then + ARCHIVE_DIR="ios/build" + OLD_ARCHIVE="$ARCHIVE_DIR/$APP_NAME.xcarchive" + NEW_ARCHIVE="$ARCHIVE_DIR/$NEW_BASE_NAME.xcarchive" + cp -r "$OLD_ARCHIVE" "$NEW_ARCHIVE" + fi + + # Export new names as environment variables + envman add --key RENAMED_ARCHIVE_FILE --value "$NEW_BASE_NAME.xcarchive" + envman add --key BINARY_DEPLOY_PATH --value "$BUILD_DIR/$NEW_BASE_NAME$BINARY_EXTENSION" + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: $BINARY_DEPLOY_PATH:BITRISE_APP_STORE_IPA_PATH + - deploy_path: $BINARY_DEPLOY_PATH + - is_compress: true + title: Deploy iOS Binary + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_SIM_BUILD" "true")}}' # Only run for physical builds + inputs: + - deploy_path: ios/build/$RENAMED_ARCHIVE_FILE + title: Deploy Symbols File + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_SIM_BUILD" "true")}}' # Only run for physical builds + inputs: + - pipeline_intermediate_files: sourcemaps/ios/index.js.map:BITRISE_APP_STORE_SOURCEMAP_PATH + - deploy_path: sourcemaps/ios/index.js.map + title: Deploy Source Map + - save-cache@1: + title: Save iOS PR Build Cache + run_if: '{{and (getenv "IOS_PR_BUILD_CACHE_KEY" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - key: '{{ getenv "IOS_PR_BUILD_CACHE_KEY" }}' + - paths: |- + ios/build/Build/Products/Release-iphonesimulator + ../Library/Detox/ios + - script@1: + title: Save last successful build commit + run_if: '{{and (getenv "GITHUB_PR_NUMBER" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - content: |- + #!/usr/bin/env bash + # Create a marker file with the current commit + mkdir -p /tmp/last-build-commit + echo "$(git rev-parse HEAD 2>/dev/null || echo ${BITRISE_GIT_COMMIT})" > /tmp/last-build-commit/commit + echo "Build completed successfully at $(date)" >> /tmp/last-build-commit/commit + - save-cache@1: + title: Save last successful build commit marker + run_if: '{{and (getenv "GITHUB_PR_NUMBER" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - key: 'last-e2e-build-commit-pr-{{ getenv "GITHUB_PR_NUMBER" }}' + - paths: /tmp/last-build-commit + meta: + bitrise.io: + stack: osx-xcode-26.2.x + machine_type_id: g2.mac.4large + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_release: + after_run: + - build_ios_main_prod + build_ios_main_prod: + envs: + - CONFIGURATION: 'Release' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + - COMMAND_YARN: 'build:ios:main:prod' + after_run: + - _ios_build_template + build_ios_main_beta: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:ios:main:beta' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + after_run: + - _ios_build_template + build_ios_main_rc: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:ios:main:rc' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + after_run: + - _ios_build_template + build_ios_main_exp: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:ios:main:exp' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + after_run: + - _ios_build_template + build_ios_main_test: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:ios:main:test' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + after_run: + - _ios_build_template + build_ios_main_e2e: + envs: + - CONFIGURATION: 'Release' + - IS_SIM_BUILD: 'true' + - SHARE_WITH_DETOX: 'true' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + - COMMAND_YARN: 'build:ios:main:e2e' + after_run: + - _ios_build_template + build_ios_main_e2e_gns_disabled: + envs: + - CONFIGURATION: 'Release' + - IS_SIM_BUILD: 'true' + - SHARE_WITH_DETOX: 'true' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + - COMMAND_YARN: 'build:ios:main:e2e' + after_run: + - _ios_build_template + build_ios_release_and_upload_sourcemaps: + envs: + - SENTRY_DISABLE_AUTO_UPLOAD: 'false' + after_run: + - build_ios_main_prod + build_ios_rc_and_upload_sourcemaps: + envs: + - SENTRY_DISABLE_AUTO_UPLOAD: 'false' + after_run: + - build_ios_main_rc + build_ios_flask_prod: + envs: + - CONFIGURATION: 'Release' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - APP_NAME: "MetaMask-Flask" + - INFO_PLIST_NAME: "MetaMask-Flask-Info.plist" + - COMMAND_YARN: 'build:ios:flask:prod' + after_run: + - _ios_build_template + build_ios_flask_test: + envs: + - CONFIGURATION: 'Release' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - APP_NAME: "MetaMask-Flask" + - INFO_PLIST_NAME: "MetaMask-Flask-Info.plist" + - COMMAND_YARN: 'build:ios:flask:test' + after_run: + - _ios_build_template + build_ios_flask_e2e: + envs: + - CONFIGURATION: 'Release' + - IS_SIM_BUILD: 'true' + - SHARE_WITH_DETOX: 'true' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - APP_NAME: "MetaMask-Flask" + - INFO_PLIST_NAME: "MetaMask-Flask-Info.plist" + - COMMAND_YARN: 'build:ios:flask:e2e' + after_run: + - _ios_build_template + build_ios_main_dev: + envs: + - CONFIGURATION: 'Debug' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: 'MetaMask' + - INFO_PLIST_NAME: 'Info.plist' + - COMMAND_YARN: 'build:ios:main:dev' + after_run: + - _ios_build_template + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_simbuild: + envs: + - IS_SIM_BUILD: 'true' + after_run: + - build_ios_main_dev + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_devbuild: + after_run: + - build_ios_main_dev + build_ios_flask_dev: + envs: + - CONFIGURATION: 'Debug' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - APP_NAME: "MetaMask-Flask" + - INFO_PLIST_NAME: "MetaMask-Flask-Info.plist" + - COMMAND_YARN: 'build:ios:flask:dev' + after_run: + - _ios_build_template + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_flask_devbuild: + after_run: + - build_ios_flask_dev + build_ios_flask_simbuild: + envs: + - IS_SIM_BUILD: 'true' + after_run: + - build_ios_flask_dev + build_ios_qa_dev: + envs: + - CONFIGURATION: 'Debug' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask-QA" + - INFO_PLIST_NAME: "MetaMask-QA-Info.plist" + - COMMAND_YARN: 'build:ios:qa:dev' + after_run: + - _ios_build_template + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_qa_devbuild: + after_run: + - build_ios_qa_dev + build_ios_qa_simbuild: + envs: + - IS_SIM_BUILD: 'true' + after_run: + - build_ios_qa_dev + build_ios_qa_prod: + envs: + - CONFIGURATION: 'Release' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask-QA" + - INFO_PLIST_NAME: "MetaMask-QA-Info.plist" + - COMMAND_YARN: 'build:ios:qa:prod' + after_run: + - _ios_build_template + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_qa: + after_run: + - build_ios_qa_prod + - _upload_ipa_to_browserstack_qa + _upload_ipa_to_browserstack_qa: + steps: + - script@1: + title: Upload IPA to Browserstack + inputs: + - content: |- + #!/usr/bin/env bash + set -e + set -x + set -o pipefail + + IPA_DIR="ios/build/output" + ORIGINAL_IPA="$IPA_DIR/MetaMask-QA.ipa" + + CUSTOM_ID="$BITRISE_GIT_BRANCH-$VERSION_NAME-$VERSION_NUMBER" + CUSTOM_ID=${CUSTOM_ID////-} + + cp "$ORIGINAL_IPA" "$IPA_DIR/$CUSTOM_ID.ipa" + IPA_PATH="$IPA_DIR/$CUSTOM_ID.ipa" + + # Upload to app-automate + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ + -F "file=@$IPA_PATH" \ + -F 'data={"custom_id": "'$CUSTOM_ID'"}' \ + | jq -j '.app_url' \ + | envman add --key BROWSERSTACK_IOS_APP_URL + echo "BROWSERSTACK_IOS_APP_URL: $BROWSERSTACK_IOS_APP_URL" + # Upload to app-live + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-live/upload" \ + -F "file=@$IPA_PATH" \ + -F 'data={"custom_id": "'$CUSTOM_ID'"}' + + # Get recent apps + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X GET https://api-cloud.browserstack.com/app-automate/recent_apps \ + | jq > browserstack_uploaded_apps.json + - share-pipeline-variable@1: + title: Persist BROWSERSTACK_IOS_APP_URL across all stages + inputs: + - variables: |- + BROWSERSTACK_IOS_APP_URL + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - deploy_path: browserstack_uploaded_apps.json + title: Bitrise Deploy Browserstack Uploaded Apps + build_ios_flask_release: + before_run: + - code_setup + after_run: + - notify_failure + steps: + - certificate-and-profile-installer@1: {} + - set-xcode-build-number@1: + inputs: + - build_short_version_string: $FLASK_VERSION_NAME + - build_version: $FLASK_VERSION_NUMBER + - plist_path: $PROJECT_LOCATION_IOS/MetaMask/MetaMask-Flask-Info.plist + - script@1: + inputs: + - content: |- + #!/usr/bin/env bash + node -v + METAMASK_BUILD_TYPE='flask' METAMASK_ENVIRONMENT='production' yarn build:ios:pre-flask + title: iOS Sourcemaps & Build + is_always_run: false + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: ios/build/output/MetaMask-Flask.ipa:BITRISE_APP_STORE_IPA_PATH + - deploy_path: ios/build/output/MetaMask-Flask.ipa + title: Deploy iOS IPA + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - deploy_path: ios/build/MetaMask-Flask.xcarchive:BITRISE_APP_STORE_XCARCHIVE_PATH + title: Deploy Symbols File + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: sourcemaps/ios/index.js.map:BITRISE_APP_STORE_SOURCEMAP_PATH + - deploy_path: sourcemaps/ios/index.js.map + title: Deploy Source Map + _upload_ipa_to_browserstack_flask: + steps: + - script@1: + title: Upload Flask IPA to Browserstack + inputs: + - content: |- + #!/usr/bin/env bash + set -e + set -x + set -o pipefail + CUSTOM_ID="flask-$BITRISE_GIT_BRANCH-$FLASK_VERSION_NAME-$FLASK_VERSION_NUMBER" + CUSTOM_ID=${CUSTOM_ID////-} + IPA_PATH=ios/build/output/MetaMask-Flask.ipa + IPA_PATH_FOR_APP_LIVE=ios/build/output/"$CUSTOM_ID".ipa + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@$IPA_PATH" -F 'data={"custom_id": "'$CUSTOM_ID'"}' | jq -j '.app_url' | envman add --key BROWSERSTACK_IOS_FLASK_APP_URL + cp "$IPA_PATH" "$IPA_PATH_FOR_APP_LIVE" + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@$IPA_PATH_FOR_APP_LIVE" -F 'data={"custom_id": "'$CUSTOM_ID'"}' + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X GET https://api-cloud.browserstack.com/app-automate/recent_apps | jq > browserstack_uploaded_flask_apps.json + - share-pipeline-variable@1: + title: Persist BROWSERSTACK_IOS_FLASK_APP_URL across all stages + inputs: + - variables: |- + BROWSERSTACK_IOS_FLASK_APP_URL + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - deploy_path: browserstack_uploaded_flask_apps.json + title: Bitrise Deploy Browserstack Uploaded Flask Apps + upload_ios_main_to_testflight: + before_run: + - code_setup + after_run: + - notify_failure + steps: + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull iOS build artifacts + - script@1: + title: Setup App Store Connect API Key + inputs: + - content: |- + #!/usr/bin/env bash + set -e + + ./scripts/setup-app-store-connect-api-key.sh \ + "$BITRISE_APP_STORE_CONNECT_API_KEY_ISSUER_ID" \ + "$BITRISE_APP_STORE_CONNECT_API_KEY_KEY_ID" \ + "$BITRISE_APP_STORE_CONNECT_API_KEY_KEY_CONTENT" + - script@1: + title: Upload to TestFlight via Fastlane + timeout: 2700 + inputs: + - content: |- + #!/usr/bin/env bash + set -e + + bash ./scripts/upload-to-testflight.sh \ + "${BITRISEIO_PIPELINE_TITLE:-Unknown}" \ + "${BITRISE_GIT_BRANCH:-Unknown}" \ + "${BITRISE_APP_STORE_IPA_PATH:-}" + - script@1: + title: Cleanup API Key + is_always_run: true + inputs: + - content: |- + #!/usr/bin/env bash + rm -f ios/AuthKey.p8 + echo "🧹 Cleaned up API key file" + meta: + bitrise.io: + stack: osx-xcode-26.2.x + machine_type_id: g2.mac.4large + set_main_target_workflow: + steps: + - share-pipeline-variable@1: + title: Persist METAMASK_BUILD_TYPE across all stages and workflows + inputs: + - variables: |- + METAMASK_BUILD_TYPE=main + set_flask_target_workflow: + steps: + - share-pipeline-variable@1: + title: Persist METAMASK_BUILD_TYPE across all stages and workflows + inputs: + - variables: |- + METAMASK_BUILD_TYPE=flask + +app: + envs: + - opts: + is_expand: false + MM_NOTIFICATIONS_UI_ENABLED: true + - opts: + is_expand: false + MM_NETWORK_UI_REDESIGN_ENABLED: false + - opts: + is_expand: false + MM_SECURITY_ALERTS_API_ENABLED: true + - opts: + is_expand: false + BRIDGE_USE_DEV_APIS: false + - opts: + is_expand: false + MM_PERPS_ENABLED: true + - opts: + is_expand: false + MM_PERPS_BLOCKED_REGIONS: "US,CA-ON,GB,BE" + - opts: + is_expand: false + MM_PERPS_HIP3_ENABLED: true + - opts: + is_expand: false + MM_PERPS_HIP3_ALLOWLIST_MARKETS: "" + - opts: + is_expand: false + MM_PERPS_HIP3_BLOCKLIST_MARKETS: "" + - opts: + is_expand: false + MM_CHARTING_LIBRARY_URL: 'https://charting-assets.static.metamask.io/tradingview/advanced-charts/v30.1.0/' + - opts: + is_expand: false + MM_MUSD_CONVERSION_FLOW_ENABLED: false + - opts: + is_expand: false + PROJECT_LOCATION: android + - opts: + is_expand: false + NDK_VERSION: 26.1.10909125 + - opts: + is_expand: false + CMAKE_VERSION: '3.22.1' + - opts: + is_expand: false + QA_APK_NAME: app-qa-release + - opts: + is_expand: false + MODULE: app + - opts: + is_expand: false + VARIANT: '' + - opts: + is_expand: false + BITRISE_PROJECT_PATH: ios/MetaMask.xcworkspace + - opts: + is_expand: false + BITRISE_SCHEME: MetaMask + - opts: + is_expand: false + BITRISE_EXPORT_METHOD: enterprise + - opts: + is_expand: false + PROJECT_LOCATION_ANDROID: android + - opts: + is_expand: false + PROJECT_LOCATION_IOS: ios + - opts: + is_expand: false + VERSION_NAME: 7.81.0 + - opts: + is_expand: false + VERSION_NUMBER: 4823 + - opts: + is_expand: false + FLASK_VERSION_NAME: 7.81.0 + - opts: + is_expand: false + FLASK_VERSION_NUMBER: 4823 + - opts: + is_expand: false + ANDROID_APK_LINK: '' + - opts: + is_expand: false + ANDROID_AAP_LINK: '' + - opts: + is_expand: false + IOS_APP_LINK: '' + - opts: + is_expand: false + NVM_VERSION: 0.39.7 + - opts: + is_expand: false + NVM_SHA256SUM: '8e45fa547f428e9196a5613efad3bfa4d4608b74ca870f930090598f5af5f643' + - opts: + is_expand: false + NODE_VERSION: 20.18.0 + - opts: + is_expand: false + YARN_VERSION: 4.14.1 + - opts: + is_expand: false + COREPACK_VERSION: 0.28.0 + - opts: + is_expand: false + SEEDLESS_ONBOARDING_ENABLED: true +meta: + bitrise.io: + stack: osx-xcode-26.2.x + machine_type_id: g2.mac.4large +trigger_map: + # Disable auto RC generation + # - push_branch: release/* + # pipeline: pr_rc_rwy_pipeline + # - push_branch: main + # pipeline: expo_dev_pipeline + # - tag: 'qa-*' + # pipeline: create_qa_builds_pipeline + # - tag: 'v*.*.*' + # pipeline: create_qa_builds_pipeline From dbd867846b8debedb8efffe23cd4be3b85101eb3 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Sat, 30 May 2026 00:40:36 +0300 Subject: [PATCH 12/15] feat(predict): add predictHomeRedesign feature flag and selector (#30816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # feat(predict): add predictHomeRedesign feature flag and selector ## **Description** Part of the **PRED-834 — IA & Navigation Overhaul** epic. This PR adds a standalone, version-gated remote feature flag `predictHomeRedesign` so the redesigned Predict homepage/feed IA can be rolled out independently from the PRED-835 portfolio work. **Reason for the change:** PRED-834 (homepage/feed IA) and PRED-835 (portfolio module) need separate rollout controls. Reusing the existing `predictPortfolio` flag would couple the two efforts; a dedicated flag keeps them independently gated. **What it does:** - Adds a new Predict feature flag shaped as `{ enabled: boolean; minimumVersion: string }` (a standard `VersionGatedFeatureFlag`), defaulting to disabled. - Resolves it through the existing Predict feature flag pipeline and exposes a selector, mirroring the existing `predictPortfolio` flag one-for-one. - No routing or UI changes: nothing consumes the selector yet. The redesigned homepage shell and the market-list route mount land in the follow-up PRED-834 ticket. As a result, existing Predict UI is unchanged while the flag is disabled (the default). **Scope / constraints:** - Does **not** reuse the PRED-835 `predictPortfolio` flag; that module remains separately gated by `predictPortfolio.enabled`. - No `DEFAULT_*` constant added (consistent with `predictPortfolio`); the `{ enabled: false, minimumVersion: '' }` default is handled by the resolver's fallback-to-`false` plus version gating. ### Changes - `app/components/UI/Predict/types/flags.ts` — added `predictHomeRedesignEnabled: boolean` to the `PredictFeatureFlags` interface. - `app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts` — resolves `predictHomeRedesignEnabled` via `resolveVersionGatedBooleanFlag(flags.predictHomeRedesign)` and returns it. - `app/components/UI/Predict/selectors/featureFlags/index.ts` — added `selectPredictHomeRedesignEnabledFlag`. - `app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts` — added default expectation + `predictHomeRedesignEnabled` resolution tests. - `app/components/UI/Predict/selectors/featureFlags/index.test.ts` — added `selectPredictHomeRedesignEnabledFlag` selector tests. ## **Changelog** CHANGELOG entry: null ## **Related issues** Refs: PRED-834 ## **Manual testing steps** ```gherkin Feature: predictHomeRedesign feature flag Scenario: Flag disabled by default leaves existing Predict UI unchanged Given the predictHomeRedesign remote flag is absent or disabled When the user opens the Predict tab Then the existing Predict feed renders exactly as before Scenario: Selector returns false by default Given no predictHomeRedesign remote config is present When selectPredictHomeRedesignEnabledFlag is evaluated Then it returns false Scenario: Malformed remote config falls back to disabled Given the predictHomeRedesign remote flag has an invalid shape When selectPredictHomeRedesignEnabledFlag is evaluated Then it returns false Scenario: Minimum-version gating Given the predictHomeRedesign flag is enabled with a minimumVersion above the app version When selectPredictHomeRedesignEnabledFlag is evaluated Then it returns false ``` Automated coverage (run locally): ```bash yarn jest \ app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts \ app/components/UI/Predict/selectors/featureFlags/index.test.ts ``` Result: 2 suites passed, 124 tests passed. ## **Screenshots/Recordings** N/A — no user-facing UI changes (flag has no consumer yet). ### **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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example ## **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** > Flag plumbing only with no runtime UI consumer; axios is a minor dependency patch bump with existing test coverage for flag behavior. > > **Overview** > Adds a **standalone, version-gated** remote flag `predictHomeRedesign` so the Predict homepage/feed IA (PRED-834) can roll out **independently** of `predictPortfolio` (PRED-835). > > The flag is wired through the existing Predict pipeline: `predictHomeRedesignEnabled` on `PredictFeatureFlags`, resolution via `resolveVersionGatedBooleanFlag(flags.predictHomeRedesign)` (defaults to **false**), and `selectPredictHomeRedesignEnabledFlag`. Tests mirror `predictPortfolio` (missing, disabled, malformed, version gate, progressive rollout unwrap). **No UI or routing** consumes the selector yet. > > Also bumps **axios** from `^1.15.x` to **`^1.16.0`** in `package.json` and `yarn.lock`. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c514bf247df98b04c7aa6bf4683b2e152b1612eb. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- .../polymarket/PolymarketProvider.test.ts | 1 + .../selectors/featureFlags/index.test.ts | 123 +++++++++++++++++ .../Predict/selectors/featureFlags/index.ts | 5 + app/components/UI/Predict/types/flags.ts | 1 + .../utils/resolvePredictFeatureFlags.test.ts | 129 ++++++++++++++++++ .../utils/resolvePredictFeatureFlags.ts | 4 + 6 files changed, 263 insertions(+) diff --git a/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts b/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts index be2fbeb1f45..30bc0e5f21c 100644 --- a/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts +++ b/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts @@ -349,6 +349,7 @@ const defaultFeatureFlags: PredictFeatureFlags = { predictWithAnyTokenEnabled: false, predictUpDownEnabled: false, predictPortfolioEnabled: false, + predictHomeRedesignEnabled: false, predictHomepageDiscoveryNbaChampionEnabled: true, predictWorldCup: DEFAULT_PREDICT_WORLD_CUP_FLAG, }; diff --git a/app/components/UI/Predict/selectors/featureFlags/index.test.ts b/app/components/UI/Predict/selectors/featureFlags/index.test.ts index f2e65ac5176..7d23cdb1e8a 100644 --- a/app/components/UI/Predict/selectors/featureFlags/index.test.ts +++ b/app/components/UI/Predict/selectors/featureFlags/index.test.ts @@ -8,6 +8,7 @@ import { selectPredictFeeCollectionFlag, selectPredictGtmOnboardingModalEnabledFlag, selectPredictHomeFeaturedVariant, + selectPredictHomeRedesignEnabledFlag, selectPredictHomepageDiscoveryNbaChampionEnabledFlag, selectPredictHotTabFlag, selectPredictPortfolioEnabledFlag, @@ -1665,6 +1666,128 @@ describe('Predict Feature Flag Selectors', () => { }); }); + describe('selectPredictHomeRedesignEnabledFlag', () => { + it('returns false when flag is missing', () => { + const result = selectPredictHomeRedesignEnabledFlag( + mockedEmptyFlagsState, + ); + + expect(result).toBe(false); + }); + + it('returns true when flag is enabled and version requirement is met', () => { + mockHasMinimumRequiredVersion.mockReturnValue(true); + const state = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: true, + minimumVersion: '1.0.0', + }, + }, + cacheTimestamp: 0, + }, + }, + }, + }; + + const result = selectPredictHomeRedesignEnabledFlag(state); + + expect(result).toBe(true); + }); + + it('returns false when flag is disabled', () => { + mockHasMinimumRequiredVersion.mockReturnValue(true); + const state = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: false, + minimumVersion: '1.0.0', + }, + }, + cacheTimestamp: 0, + }, + }, + }, + }; + + const result = selectPredictHomeRedesignEnabledFlag(state); + + expect(result).toBe(false); + }); + + it('returns false when flag is malformed', () => { + const state = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: 'true', + minimumVersion: '1.0.0', + }, + }, + cacheTimestamp: 0, + }, + }, + }, + }; + + const result = selectPredictHomeRedesignEnabledFlag(state); + + expect(result).toBe(false); + }); + + it('returns false when app version is below minimum required', () => { + mockHasMinimumRequiredVersion.mockReturnValue(false); + const state = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: true, + minimumVersion: '99.0.0', + }, + }, + cacheTimestamp: 0, + }, + }, + }, + }; + + const result = selectPredictHomeRedesignEnabledFlag(state); + + expect(result).toBe(false); + }); + + it('returns false when minimumVersion is the default empty string', () => { + const state = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: true, + }, + }, + cacheTimestamp: 0, + }, + }, + }, + }; + + const result = selectPredictHomeRedesignEnabledFlag(state); + + expect(result).toBe(false); + }); + }); + describe('selectPredictHomepageDiscoveryNbaChampionEnabledFlag', () => { it('returns false when the remote flag is disabled', () => { mockHasMinimumRequiredVersion.mockReturnValue(true); diff --git a/app/components/UI/Predict/selectors/featureFlags/index.ts b/app/components/UI/Predict/selectors/featureFlags/index.ts index 7cd3ccedd50..084d315a76b 100644 --- a/app/components/UI/Predict/selectors/featureFlags/index.ts +++ b/app/components/UI/Predict/selectors/featureFlags/index.ts @@ -183,6 +183,11 @@ export const selectPredictPortfolioEnabledFlag = createSelector( (flags) => flags.predictPortfolioEnabled, ); +export const selectPredictHomeRedesignEnabledFlag = createSelector( + selectPredictFeatureFlags, + (flags) => flags.predictHomeRedesignEnabled, +); + export const selectPredictFeaturedCarouselEnabledFlag = createSelector( selectRemoteFeatureFlags, (remoteFeatureFlags) => diff --git a/app/components/UI/Predict/types/flags.ts b/app/components/UI/Predict/types/flags.ts index 4490f87cbaa..112ffcb8ada 100644 --- a/app/components/UI/Predict/types/flags.ts +++ b/app/components/UI/Predict/types/flags.ts @@ -56,6 +56,7 @@ export interface PredictFeatureFlags { predictHomepageDiscoveryNbaChampionEnabled: boolean; predictWorldCup: PredictWorldCupConfig; predictPortfolioEnabled: boolean; + predictHomeRedesignEnabled: boolean; } export interface PredictHotTabFlag extends VersionGatedFeatureFlag { diff --git a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts index ff9a93020d2..dce827338c8 100644 --- a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts +++ b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts @@ -33,6 +33,7 @@ describe('resolvePredictFeatureFlags', () => { predictWithAnyTokenEnabled: false, predictUpDownEnabled: false, predictPortfolioEnabled: false, + predictHomeRedesignEnabled: false, predictHomepageDiscoveryNbaChampionEnabled: true, predictWorldCup: DEFAULT_PREDICT_WORLD_CUP_FLAG, }); @@ -452,6 +453,134 @@ describe('resolvePredictFeatureFlags', () => { }); }); + describe('predictHomeRedesignEnabled', () => { + it('returns false when flag is missing', () => { + const result = resolvePredictFeatureFlags({}); + + expect(result.predictHomeRedesignEnabled).toBe(false); + }); + + it('returns true when enabled and version gate passes', () => { + mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => { + if ( + flag && + typeof flag === 'object' && + 'minimumVersion' in flag && + !('leagues' in flag) && + !('seriesId' in flag) + ) { + return true; + } + return undefined; + }); + + const result = resolvePredictFeatureFlags({ + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: true, + minimumVersion: '1.0.0', + }, + }, + }); + + expect(result.predictHomeRedesignEnabled).toBe(true); + }); + + it('returns false when flag is disabled', () => { + mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => { + if ( + flag && + typeof flag === 'object' && + 'minimumVersion' in flag && + !('leagues' in flag) && + !('seriesId' in flag) + ) { + return false; + } + return undefined; + }); + + const result = resolvePredictFeatureFlags({ + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: false, + minimumVersion: '1.0.0', + }, + }, + }); + + expect(result.predictHomeRedesignEnabled).toBe(false); + }); + + it('returns false when flag is malformed', () => { + const result = resolvePredictFeatureFlags({ + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: 'true', + minimumVersion: '1.0.0', + }, + }, + }); + + expect(result.predictHomeRedesignEnabled).toBe(false); + }); + + it('returns false when version gate fails', () => { + mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => { + if ( + flag && + typeof flag === 'object' && + 'minimumVersion' in flag && + !('leagues' in flag) && + !('seriesId' in flag) + ) { + return false; + } + return undefined; + }); + + const result = resolvePredictFeatureFlags({ + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: true, + minimumVersion: '99.0.0', + }, + }, + }); + + expect(result.predictHomeRedesignEnabled).toBe(false); + }); + + it('unwraps progressive rollout shape', () => { + mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => { + if ( + flag && + typeof flag === 'object' && + 'minimumVersion' in flag && + !('leagues' in flag) && + !('seriesId' in flag) + ) { + return true; + } + return undefined; + }); + + const result = resolvePredictFeatureFlags({ + remoteFeatureFlags: { + predictHomeRedesign: { + name: 'group-a', + value: { + enabled: true, + minimumVersion: '1.0.0', + }, + }, + }, + }); + + expect(result.predictHomeRedesignEnabled).toBe(true); + }); + }); + describe('extendedSportsMarketsLeagues', () => { it('returns empty array when flag is missing', () => { const result = resolvePredictFeatureFlags({}); diff --git a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts index 1c3238edcbc..1945ab7ab62 100644 --- a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts +++ b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts @@ -102,6 +102,9 @@ export function resolvePredictFeatureFlags( const predictPortfolioEnabled = resolveVersionGatedBooleanFlag( flags.predictPortfolio, ); + const predictHomeRedesignEnabled = resolveVersionGatedBooleanFlag( + flags.predictHomeRedesign, + ); const predictHomepageDiscoveryNbaChampionEnabled = resolveVersionGatedBooleanFlag( flags.predictHomepageDiscoveryNbaChampionEnabled, @@ -129,6 +132,7 @@ export function resolvePredictFeatureFlags( predictWithAnyTokenEnabled, predictUpDownEnabled, predictPortfolioEnabled, + predictHomeRedesignEnabled, predictHomepageDiscoveryNbaChampionEnabled, predictWorldCup, }; From ae601d99cb85b4793f1a5a25b257563224ccb3c9 Mon Sep 17 00:00:00 2001 From: samiracle <12882259+samir-acle@users.noreply.github.com> Date: Fri, 29 May 2026 17:59:31 -0400 Subject: [PATCH 13/15] fix(engagement): latch startup marketing consent prompt (#30808) ## **Description** Fixes an issue where the marketing_consent notification pre-prompt could be re-triggered during the same app session after the user turned marketing consent off in Settings. The pre-prompt is intended to behave as a startup flow. This change latches the marketing consent value used for startup prompt resolution, so once startup eligibility has resolved, later Redux updates from user actions do not cause the prompt to appear unexpectedly. Social-login marketing consent backfill is still respected as part of startup resolution. If backfill is pending, the prompt waits for it to clear, then decides once using the resolved consent value. Risk Low risk. This change is scoped to usePushPrePromptVariant, which only decides which notification pre-prompt variant to show. It does not change notification registration, OS permission requests, Settings behavior, analytics opt-in/out behavior, or persisted storage keys. The change preserves existing behavior for the main paths: Users without OS push permission still resolve to the push permission prompt first. Users who already saw the pre-prompt remain suppressed by PUSH_PRE_PROMPT_SHOWN. Users with marketing consent enabled do not see the marketing consent prompt. Users with marketing consent disabled at startup can still see the one-time startup prompt if otherwise eligible. Social-login users still wait for marketing consent backfill before the marketing prompt decision is made. The risk is low because the fix narrows when the marketing consent prompt can appear rather than expanding eligibility. The only behavior removed is the unintended mid-session re-trigger after Settings opt-out. ## **Changelog** CHANGELOG entry: Fixed an issue where the marketing consent notification pre-prompt could reappear after turning marketing consent off in Settings. ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../hooks/usePushPrePromptVariant.test.ts | 72 ++++++++++++++++++- .../hooks/usePushPrePromptVariant.ts | 38 +++++++++- 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/app/util/notifications/hooks/usePushPrePromptVariant.test.ts b/app/util/notifications/hooks/usePushPrePromptVariant.test.ts index 10217c430cd..ba69fcb7765 100644 --- a/app/util/notifications/hooks/usePushPrePromptVariant.test.ts +++ b/app/util/notifications/hooks/usePushPrePromptVariant.test.ts @@ -5,7 +5,11 @@ import * as NotificationSelectors from '../../../selectors/notifications'; import * as OnboardingSelectors from '../../../selectors/onboarding'; // eslint-disable-next-line import-x/no-namespace import * as SettingsSelectors from '../../../selectors/settings'; -import { setCompletedOnboarding } from '../../../actions/onboarding'; +import { + setCompletedOnboarding, + setPendingSocialLoginMarketingConsentBackfill, +} from '../../../actions/onboarding'; +import { setDataCollectionForMarketing } from '../../../actions/security'; import { PUSH_PRE_PROMPT_SHOWN, TRUE } from '../../../constants/storage'; import storageWrapper from '../../../store/storage-wrapper'; import { renderHookWithProvider } from '../../test/renderWithProvider'; @@ -282,7 +286,7 @@ describe('usePushPrePromptVariant', () => { expect(mockResolveNativePushPermissionStatus).not.toHaveBeenCalled(); }); - it('returns the marketing consent prompt when OS push is enabled and Redux marketing consent is missing', async () => { + it('returns the marketing consent prompt when OS push is enabled and Redux marketing consent is not enabled', async () => { const { result } = renderUsePushPrePromptVariant(); await waitFor(() => { @@ -304,6 +308,28 @@ describe('usePushPrePromptVariant', () => { expect(result.current.nativeOsPermissionEnabled).toBe(true); }); + it('does not show the marketing consent prompt when marketing consent is turned off after startup resolution', async () => { + const { result, store } = renderUsePushPrePromptVariant({ + hasMarketingConsent: true, + }); + + await waitFor(() => { + expect(result.current.isResolving).toBe(false); + }); + expect(result.current.variant).toBeNull(); + expect(result.current.nativeOsPermissionEnabled).toBe(true); + + await act(async () => { + store.dispatch(setDataCollectionForMarketing(false)); + }); + + await waitFor(() => { + expect(result.current.isResolving).toBe(false); + }); + + expect(result.current.variant).toBeNull(); + }); + it('defers the marketing consent prompt while social login marketing consent backfill is pending', async () => { const { result } = renderUsePushPrePromptVariant({ pendingSocialLoginMarketingConsentBackfill: 'google', @@ -317,6 +343,48 @@ describe('usePushPrePromptVariant', () => { expect(mockResolveNativePushPermissionStatus).toHaveBeenCalledTimes(1); }); + it('returns the marketing consent prompt after social login marketing consent backfill clears without consent', async () => { + const { result, store } = renderUsePushPrePromptVariant({ + pendingSocialLoginMarketingConsentBackfill: 'google', + }); + + await waitFor(() => { + expect(result.current.isResolving).toBe(false); + }); + expect(result.current.variant).toBeNull(); + + await act(async () => { + store.dispatch(setPendingSocialLoginMarketingConsentBackfill(null)); + }); + + await waitFor(() => { + expect(result.current.variant).toBe('marketing_consent'); + }); + expect(result.current.nativeOsPermissionEnabled).toBe(true); + }); + + it('does not return the marketing consent prompt after social login marketing consent backfill clears with consent', async () => { + const { result, store } = renderUsePushPrePromptVariant({ + pendingSocialLoginMarketingConsentBackfill: 'google', + }); + + await waitFor(() => { + expect(result.current.isResolving).toBe(false); + }); + expect(result.current.variant).toBeNull(); + + await act(async () => { + store.dispatch(setDataCollectionForMarketing(true)); + store.dispatch(setPendingSocialLoginMarketingConsentBackfill(null)); + }); + + await waitFor(() => { + expect(result.current.isResolving).toBe(false); + }); + expect(result.current.variant).toBeNull(); + expect(result.current.nativeOsPermissionEnabled).toBe(true); + }); + it('does not defer the push permission prompt for social login marketing consent backfill', async () => { mockNativePushPermissionStatus({ nativeOsPermissionEnabled: false, diff --git a/app/util/notifications/hooks/usePushPrePromptVariant.ts b/app/util/notifications/hooks/usePushPrePromptVariant.ts index cd6c8dc85cb..e961e9a25f2 100644 --- a/app/util/notifications/hooks/usePushPrePromptVariant.ts +++ b/app/util/notifications/hooks/usePushPrePromptVariant.ts @@ -31,6 +31,7 @@ interface PushPrePromptEligibility { canShowPrePrompt: boolean; hasPrePromptBeenShown: boolean; hasMarketingConsent: boolean; + isMarketingConsentResolutionPending: boolean; pendingSocialLoginMarketingConsentBackfill: string | null; } @@ -38,12 +39,14 @@ const getResolutionKey = ({ canShowPrePrompt, hasPrePromptBeenShown, hasMarketingConsent, + isMarketingConsentResolutionPending, pendingSocialLoginMarketingConsentBackfill, }: PushPrePromptEligibility) => [ `canShowPrePrompt:${canShowPrePrompt}`, `hasPrePromptBeenShown:${hasPrePromptBeenShown}`, `hasMarketingConsent:${hasMarketingConsent}`, + `isMarketingConsentResolutionPending:${isMarketingConsentResolutionPending}`, `pendingSocialLoginMarketingConsentBackfill:${ pendingSocialLoginMarketingConsentBackfill ?? 'null' }`, @@ -96,7 +99,10 @@ const resolvePrePromptVariant = async ( }; } - if (eligibility.pendingSocialLoginMarketingConsentBackfill) { + if ( + eligibility.isMarketingConsentResolutionPending || + eligibility.pendingSocialLoginMarketingConsentBackfill + ) { return { nativeOsPermissionEnabled, variant: null, @@ -149,6 +155,30 @@ export function usePushPrePromptVariant(): { selectPendingSocialLoginMarketingConsentBackfill, ); + const [startupMarketingConsent, setStartupMarketingConsent] = useState< + boolean | null + >(() => + pendingSocialLoginMarketingConsentBackfill ? null : hasMarketingConsent, + ); + + useEffect(() => { + if ( + startupMarketingConsent !== null || + pendingSocialLoginMarketingConsentBackfill + ) { + return; + } + + setStartupMarketingConsent(hasMarketingConsent); + }, [ + hasMarketingConsent, + pendingSocialLoginMarketingConsentBackfill, + startupMarketingConsent, + ]); + + const isMarketingConsentResolutionPending = startupMarketingConsent === null; + const startupHasMarketingConsent = startupMarketingConsent === true; + const canShowPrePrompt = Boolean(completedOnboarding) && isNotificationsFeatureAvailable && @@ -167,13 +197,15 @@ export function usePushPrePromptVariant(): { () => ({ canShowPrePrompt, hasPrePromptBeenShown, - hasMarketingConsent, + hasMarketingConsent: startupHasMarketingConsent, + isMarketingConsentResolutionPending, pendingSocialLoginMarketingConsentBackfill, }), [ canShowPrePrompt, hasPrePromptBeenShown, - hasMarketingConsent, + isMarketingConsentResolutionPending, + startupHasMarketingConsent, pendingSocialLoginMarketingConsentBackfill, ], ); From f6df783d64d4d78e651d6bcc196ad41dd1adabdd Mon Sep 17 00:00:00 2001 From: Vince Howard Date: Fri, 29 May 2026 16:07:03 -0600 Subject: [PATCH 14/15] chore(pure black): elevate MMDS BottomSheet surfaces via useElevatedSurface shim (#30708) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Part of the pure-black dark mode rollout. The MMDS package's `BottomSheet` paints `bg-default` internally, which collapses to `#000` against the pure-black screen background when `MM_PURE_BLACK_PREVIEW=true`. Until the package ships its own pure-black-aware surface, every consumer of the DS package `BottomSheet` now passes `twClassName={surfaceClass}` (sourced from `useElevatedSurface()`). The hook returns `bg-default` when the flag is off (no-op for current users) and `bg-section` under pure-black dark mode. 50 files updated. No behavioral change with the flag off. Cleanup once the package ships is mechanical: `grep -r "useElevatedSurface\|surfaceClass" app/` → remove. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TMCU-798 ## **Manual testing steps** ```gherkin Feature: MMDS BottomSheet surfaces stay visible under MM_PURE_BLACK_PREVIEW Scenario: A bottom sheet is opened under pure-black dark mode Given MM_PURE_BLACK_PREVIEW=true and the app is in dark mode When the user triggers any flow that opens an MMDS BottomSheet (e.g. Onboarding sign-in sheet, Address selector, Bridge price impact, Money transfer, Rewards opt-in, Perps tooltips, Ramp modals) Then the sheet panel renders an elevated surface (background.section) against the dimmed scrim, not a pure-black panel And dismissing/reopening the sheet preserves the elevation Scenario: Flag off — appearance unchanged Given MM_PURE_BLACK_PREVIEW is unset or false When the user opens any of the same sheets Then they render exactly as they do on main ``` ## **Screenshots/Recordings** ### Example Instance | Before | After | |--------|-------| | ![Before](https://github.com/user-attachments/assets/60c48680-2826-42a1-b9e9-8c894fcd16d6) | ![After](https://github.com/user-attachments/assets/c9122a74-4766-46a6-8779-eba071ae3b4b) | ### **Before** `~` ### **After** `~` ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [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** > Flag-gated UI background classes only; no auth, transactions, or data-path changes, and default builds remain visually unchanged. > > **Overview** > Adds a **pure-black preview stopgap** so MMDS `BottomSheet` panels stay visually elevated when `MM_PURE_BLACK_PREVIEW=true` in dark mode (where default sheet background would match a `#000` screen). > > **`themeUtils`** documents `useElevatedSurface` / `getElevatedSurfaceColor`: flag off → `bg-default` (unchanged); flag on + dark → `bg-alternative`; light mode unchanged. > > **~50 call sites** import `useElevatedSurface`, set `const surfaceClass = useElevatedSurface()`, and pass **`twClassName={surfaceClass}`** on design-system `BottomSheet` (Bridge batch-sell and alerts, Money sheets, Ramp modals/checkout, Perps/Predict/Rewards, onboarding, address/SRP selectors, confirmations pay-with and send alerts, NFT grid actions, etc.). Some files only reorder imports alongside this wiring. > > No product logic changes; behavior with the flag off should match main. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2ac348cf8f85b4a8d8c3c90a6a58eb813f6e9952. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- .../index.tsx | 3 + .../BatchSellFinalReviewModal/index.tsx | 3 + .../index.tsx | 3 + .../BatchSellNetworkFeeInfoModal/index.tsx | 3 + .../BatchSellQuoteDetailsModal/index.tsx | 3 + .../components/HighRateAlertModal/index.tsx | 3 + .../components/MissingPriceModal/index.tsx | 11 +- .../components/PriceImpactModal/index.tsx | 4 +- .../AccessRestrictedModal.tsx | 16 +- .../ConfirmTurnOnBackupAndSyncModal.tsx | 7 +- .../MoneyApyInfoSheet/MoneyApyInfoSheet.tsx | 5 +- .../MoneyBalanceInfoSheet.tsx | 5 +- .../MoneyEarnCryptoInfoSheet.tsx | 6 +- .../MoneyEarningsInfoSheet.tsx | 5 +- .../MoneyLinkCardSheet/MoneyLinkCardSheet.tsx | 3 + .../MoneyMoreSheet/MoneyMoreSheet.tsx | 7 +- .../MoneyTransactionDetailsSheet.tsx | 5 +- .../MoneyTransferSheet/MoneyTransferSheet.tsx | 7 +- .../UI/NftGrid/NftGridItemBottomSheet.tsx | 14 +- .../PerpsTooltipView/PerpsTooltipView.tsx | 3 + .../PerpsBottomSheetTooltip.tsx | 9 +- .../PerpsQuoteExpiredModal.tsx | 4 +- .../PredictPreviewSheet.tsx | 3 + .../PredictWithdrawUnavailableSheet.tsx | 3 + .../UI/Ramp/Views/Checkout/Checkout.tsx | 7 +- .../ErrorDetailsModal/ErrorDetailsModal.tsx | 20 ++- .../PaymentSelectionModal.tsx | 12 +- .../ProcessingInfoModal.tsx | 20 ++- .../ProviderSelectionModal.tsx | 3 + .../Modals/SettingsModal/SettingsModal.tsx | 13 +- .../StateSelectorModal/StateSelectorModal.tsx | 17 +- .../TokenNotAvailableModal.tsx | 14 +- .../UnsupportedStateModal.tsx | 19 ++- .../UnsupportedTokenModal.tsx | 10 +- .../EligibilityFailedModal.tsx | 12 +- .../RampUnsupportedModal.tsx | 12 +- .../Campaigns/CampaignOptInSheet.tsx | 8 +- .../Campaigns/OndoAccountPickerSheet.tsx | 4 +- .../Campaigns/OndoAfterHoursSheet.tsx | 9 +- .../Campaigns/OndoNotEligibleSheet.tsx | 154 +++++++++--------- .../RwaUnavailableBottomSheet.tsx | 13 +- .../Views/AddressSelector/AddressSelector.tsx | 11 +- .../IntroModal/LearnMoreBottomSheet.tsx | 15 +- .../components/DeleteNetworkModal.tsx | 10 +- .../Views/OnboardingSheet/index.tsx | 10 +- .../Views/SelectSRP/SelectSRPBottomSheet.tsx | 12 +- .../components/QuickBuy/QuickBuyRoot.tsx | 3 + .../AccountSelector/AccountSelector.tsx | 5 +- .../pay-with-bottom-sheet.tsx | 5 +- .../send-alert-modal/send-alert-modal.tsx | 12 +- app/util/theme/themeUtils.ts | 15 +- 51 files changed, 389 insertions(+), 191 deletions(-) diff --git a/app/components/UI/Bridge/components/BatchSellDestinationTokenSelectorModal/index.tsx b/app/components/UI/Bridge/components/BatchSellDestinationTokenSelectorModal/index.tsx index 1c06e9390e9..bd03756a8c1 100644 --- a/app/components/UI/Bridge/components/BatchSellDestinationTokenSelectorModal/index.tsx +++ b/app/components/UI/Bridge/components/BatchSellDestinationTokenSelectorModal/index.tsx @@ -36,6 +36,7 @@ import { RootState } from '../../../../../reducers'; import { BridgeToken } from '../../types'; import { formatTokenBalance } from '../../utils'; import { BatchSellDestinationTokenSelectorModalSelectorsIDs } from './BatchSellDestinationTokenSelectorModal.testIds'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; const getTokenKey = (token: BridgeToken) => `${token.chainId}:${token.address}`; @@ -86,6 +87,7 @@ export function BatchSellDestinationTokenSelectorModal() { chainIds: destinationChainIds ?? (sourceChainId ? [sourceChainId] : undefined), }); + const surfaceClass = useElevatedSurface(); const handleClose = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -104,6 +106,7 @@ export function BatchSellDestinationTokenSelectorModal() { ref={sheetRef} goBack={navigation.goBack} testID={BatchSellDestinationTokenSelectorModalSelectorsIDs.SHEET} + twClassName={surfaceClass} > @@ -362,6 +364,7 @@ export function BatchSellFinalReviewModal() { navigation.replace(sourceModal.screen, sourceModal.params) : undefined; + const surfaceClass = useElevatedSurface(); return ( navigation.replace(sourceModal.screen, sourceModal.params) : undefined; + const surfaceClass = useElevatedSurface(); return ( >>(); const sourceTokens = useSelector(selectBatchSellSourceTokens); + const surfaceClass = useElevatedSurface(); const batchSellQuoteData = useBatchSellQuoteData({ shouldUpdateBatchSellTrades: false, }); @@ -48,6 +50,7 @@ export function BatchSellQuoteDetailsModal() { { sheetRef.current?.onCloseBottomSheet(); @@ -49,6 +51,7 @@ export function HighRateAlertModal() { ref={sheetRef} goBack={goBack} testID={HighRateAlertModalSelectorsIDs.SHEET} + twClassName={surfaceClass} > { const sheetRef = useRef(null); const [loading, setLoading] = useState(false); const { location } = useParams(); - + const surfaceClass = useElevatedSurface(); const sourceToken = useSelector(selectSourceToken); const tokenBalance = useLatestBalance({ address: sourceToken?.address, @@ -62,7 +63,11 @@ export const MissingPriceModal = () => { }, [confirmBridge]); return ( - + { const { goBack } = useNavigation(); @@ -42,6 +43,7 @@ export const PriceImpactModal = () => { const priceImpactViewData = usePriceImpactViewData( activeQuote?.quote.priceData?.priceImpact, ); + const surfaceClass = useElevatedSurface(); const isDangerousPriceImpact = useMemo( () => @@ -62,7 +64,7 @@ export const PriceImpactModal = () => { }, [confirmBridge]); return ( - + = ({ isVisible, onClose, onContactSupport, }) => { + const surfaceClass = useElevatedSurface(); + if (!isVisible) return null; return ( { const bottomSheetRef = useRef(null); const { enableBackupAndSync, trackEnableBackupAndSyncEvent } = useParams(); const dispatch = useThunkDispatch(); + const surfaceClass = useElevatedSurface(); const enableBasicFunctionality = async () => { await dispatch(toggleBasicFunctionality(true)); @@ -51,7 +54,7 @@ const ConfirmTurnOnBackupAndSyncModal = () => { }; return ( - + { const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); const { apy } = useParams(); + const surfaceClass = useElevatedSurface(); const handleGoBack = useCallback(() => { navigation.goBack(); @@ -39,6 +41,7 @@ const MoneyApyInfoSheet = () => { goBack={handleGoBack} testID={MoneyApyInfoSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > diff --git a/app/components/UI/Money/components/MoneyBalanceInfoSheet/MoneyBalanceInfoSheet.tsx b/app/components/UI/Money/components/MoneyBalanceInfoSheet/MoneyBalanceInfoSheet.tsx index 3b3ec1a9841..eaf32842a2e 100644 --- a/app/components/UI/Money/components/MoneyBalanceInfoSheet/MoneyBalanceInfoSheet.tsx +++ b/app/components/UI/Money/components/MoneyBalanceInfoSheet/MoneyBalanceInfoSheet.tsx @@ -4,19 +4,21 @@ import { useNavigation } from '@react-navigation/native'; import { BottomSheet, BottomSheetHeader, - type BottomSheetRef, Text, TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { strings } from '../../../../../../locales/i18n'; import { useStyles } from '../../../../../component-library/hooks'; import styleSheet from './MoneyBalanceInfoSheet.styles'; import { MoneyBalanceInfoSheetTestIds } from './MoneyBalanceInfoSheet.testIds'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; const MoneyBalanceInfoSheet = () => { const sheetRef = useRef(null); const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); const handleGoBack = useCallback(() => { navigation.goBack(); @@ -32,6 +34,7 @@ const MoneyBalanceInfoSheet = () => { goBack={handleGoBack} testID={MoneyBalanceInfoSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > { @@ -22,6 +24,7 @@ const MoneyEarnCryptoInfoSheet = () => { const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); const { apyPercent } = useMoneyAccountBalance(); + const surfaceClass = useElevatedSurface(); const handleGoBack = useCallback(() => { navigation.goBack(); @@ -37,6 +40,7 @@ const MoneyEarnCryptoInfoSheet = () => { goBack={handleGoBack} testID={MoneyEarnCryptoInfoSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > { const sheetRef = useRef(null); const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); const handleGoBack = useCallback(() => { navigation.goBack(); @@ -32,6 +34,7 @@ const MoneyEarningsInfoSheet = () => { goBack={handleGoBack} testID={MoneyEarningsInfoSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > diff --git a/app/components/UI/Money/components/MoneyLinkCardSheet/MoneyLinkCardSheet.tsx b/app/components/UI/Money/components/MoneyLinkCardSheet/MoneyLinkCardSheet.tsx index 76b676ed810..90779198256 100644 --- a/app/components/UI/Money/components/MoneyLinkCardSheet/MoneyLinkCardSheet.tsx +++ b/app/components/UI/Money/components/MoneyLinkCardSheet/MoneyLinkCardSheet.tsx @@ -26,6 +26,7 @@ import mmCardRegular from '../../../../../images/mm_card_regular.png'; import mmCardMetal from '../../../../../images/mm_card_metal.png'; import styleSheet from './MoneyLinkCardSheet.styles'; import { MoneyLinkCardSheetTestIds } from './MoneyLinkCardSheet.testIds'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; /** * "Spend and earn" confirmation bottom sheet shown before the Money Account ↔ @@ -43,6 +44,7 @@ const MoneyLinkCardSheet = () => { const { confirmLinkInBackground } = useMoneyAccountCardLinkage(); const { apyPercent } = useMoneyAccountBalance(); const cardHomeData = useSelector(selectCardHomeData); + const surfaceClass = useElevatedSurface(); const isMetalCard = cardHomeData?.card?.type === CardType.METAL; const handleGoBack = useCallback(() => { @@ -83,6 +85,7 @@ const MoneyLinkCardSheet = () => { goBack={handleGoBack} testID={MoneyLinkCardSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > { const sheetRef = useRef(null); const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); const closeAndNavigate = useCallback((navigateFn: () => void) => { sheetRef.current?.onCloseBottomSheet(navigateFn); @@ -86,6 +88,7 @@ const MoneyMoreSheet = () => { goBack={handleGoBack} testID={MoneyMoreSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > sheetRef.current?.onCloseBottomSheet()}> diff --git a/app/components/UI/Money/components/MoneyTransactionDetailsSheet/MoneyTransactionDetailsSheet.tsx b/app/components/UI/Money/components/MoneyTransactionDetailsSheet/MoneyTransactionDetailsSheet.tsx index 09d66cb94f9..11fcd7cda8a 100644 --- a/app/components/UI/Money/components/MoneyTransactionDetailsSheet/MoneyTransactionDetailsSheet.tsx +++ b/app/components/UI/Money/components/MoneyTransactionDetailsSheet/MoneyTransactionDetailsSheet.tsx @@ -2,10 +2,10 @@ import React, { useCallback, useRef } from 'react'; import { useNavigation } from '@react-navigation/native'; import { BottomSheet, - type BottomSheetRef, BottomSheetHeader, Text, TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { type TransactionMeta, @@ -14,6 +14,7 @@ import { import { strings } from '../../../../../../locales/i18n'; import { TransactionDetails } from '../../../../Views/confirmations/components/activity/transaction-details/transaction-details'; import { useTransactionDetails } from '../../../../Views/confirmations/hooks/activity/useTransactionDetails'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; import { MoneyReceivedDetails } from './MoneyReceivedDetails'; const RECEIVED_TYPES: TransactionType[] = [ @@ -58,6 +59,7 @@ const MoneyTransactionDetailsSheet = () => { const sheetRef = useRef(null); const navigation = useNavigation(); const { transactionMeta } = useTransactionDetails(); + const surfaceClass = useElevatedSurface(); const title = getTitle(transactionMeta); const isReceived = Boolean( transactionMeta?.type && RECEIVED_TYPES.includes(transactionMeta.type), @@ -73,6 +75,7 @@ const MoneyTransactionDetailsSheet = () => { isFullscreen goBack={navigation.goBack} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > {title} diff --git a/app/components/UI/Money/components/MoneyTransferSheet/MoneyTransferSheet.tsx b/app/components/UI/Money/components/MoneyTransferSheet/MoneyTransferSheet.tsx index b37f8850e10..06c6eb81f1d 100644 --- a/app/components/UI/Money/components/MoneyTransferSheet/MoneyTransferSheet.tsx +++ b/app/components/UI/Money/components/MoneyTransferSheet/MoneyTransferSheet.tsx @@ -4,15 +4,15 @@ import { useNavigation } from '@react-navigation/native'; import { BottomSheet, BottomSheetHeader, - type BottomSheetRef, FontWeight, Icon, + IconColor, IconName, IconSize, - IconColor, Text, TextColor, TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import Tag from '../../../../../component-library/components/Tags/Tag'; import { strings } from '../../../../../../locales/i18n'; @@ -21,6 +21,7 @@ import { useMoneyAccountWithdrawal } from '../../hooks/useMoneyAccount'; import Logger from '../../../../../util/Logger'; import styleSheet from './MoneyTransferSheet.styles'; import { MoneyTransferSheetTestIds } from './MoneyTransferSheet.testIds'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; interface ActiveOption { label: string; @@ -40,6 +41,7 @@ const MoneyTransferSheet = () => { const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); const { initiateWithdrawal } = useMoneyAccountWithdrawal(); + const surfaceClass = useElevatedSurface(); const handleGoBack = useCallback(() => { navigation.goBack(); @@ -106,6 +108,7 @@ const MoneyTransferSheet = () => { goBack={handleGoBack} testID={MoneyTransferSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > sheetRef.current?.onCloseBottomSheet()}> diff --git a/app/components/UI/NftGrid/NftGridItemBottomSheet.tsx b/app/components/UI/NftGrid/NftGridItemBottomSheet.tsx index 40c2819218a..d295e323c52 100644 --- a/app/components/UI/NftGrid/NftGridItemBottomSheet.tsx +++ b/app/components/UI/NftGrid/NftGridItemBottomSheet.tsx @@ -1,12 +1,12 @@ import { - Box, + BottomSheet, BottomSheetHeader, + BottomSheetRef, + Box, Button, ButtonVariant, Text, TextVariant, - BottomSheet, - BottomSheetRef, } from '@metamask/design-system-react-native'; import React, { useCallback, useRef } from 'react'; import { Alert, Modal, View } from 'react-native'; @@ -14,6 +14,7 @@ import { strings } from '../../../../locales/i18n'; import { Nft } from '@metamask/assets-controllers'; import Engine from '../../../core/Engine'; import { toHex } from '@metamask/controller-utils'; +import { useElevatedSurface } from '../../../util/theme/themeUtils'; interface NftGridItemBottomSheetProps { isVisible: boolean; @@ -27,6 +28,7 @@ const NftGridItemBottomSheet: React.FC = ({ nft, }) => { const sheetRef = useRef(null); + const surfaceClass = useElevatedSurface(); const handleSheetClose = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -75,7 +77,11 @@ const NftGridItemBottomSheet: React.FC = ({ return ( - + {strings('wallet.collectible_action_title')} diff --git a/app/components/UI/Perps/Views/PerpsTooltipView/PerpsTooltipView.tsx b/app/components/UI/Perps/Views/PerpsTooltipView/PerpsTooltipView.tsx index 57ec0878b29..07d56183135 100644 --- a/app/components/UI/Perps/Views/PerpsTooltipView/PerpsTooltipView.tsx +++ b/app/components/UI/Perps/Views/PerpsTooltipView/PerpsTooltipView.tsx @@ -15,6 +15,7 @@ import { strings } from '../../../../../../locales/i18n'; import { PerpsTooltipContentKey } from '../../components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.types'; import { tooltipContentRegistry } from '../../components/PerpsBottomSheetTooltip/content/contentRegistry'; import { PerpsBottomSheetTooltipSelectorsIDs } from '../../Perps.testIds'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; interface PerpsTooltipViewRouteParams { contentKey: PerpsTooltipContentKey; @@ -26,6 +27,7 @@ const PerpsTooltipView: React.FC = () => { const route = useRoute, string>>(); const bottomSheetRef = useRef(null); + const surfaceClass = useElevatedSurface(); const { contentKey, data } = route.params || {}; @@ -67,6 +69,7 @@ const PerpsTooltipView: React.FC = () => { ref={bottomSheetRef} goBack={navigation.goBack} testID="perps-tooltip-bottom-sheet" + twClassName={surfaceClass} > {!hasCustomHeader && ( diff --git a/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx b/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx index 820596ae8be..1891022a10b 100644 --- a/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx +++ b/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx @@ -22,6 +22,7 @@ import { } from '@metamask/perps-controller'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { MetaMetricsEvents } from '../../../../../core/Analytics/MetaMetrics.events'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; /** * Tip: If want to render the PerpsBottomSheetTooltip from the root (not constrained by a parent component), @@ -81,6 +82,7 @@ const PerpsBottomSheetTooltip = React.memo( }; const { track } = usePerpsEventTracking(); + const surfaceClass = useElevatedSurface(); const handleClose = useCallback(() => { bottomSheetRef.current?.onCloseBottomSheet(); @@ -122,7 +124,12 @@ const PerpsBottomSheetTooltip = React.memo( if (!isVisible || !title) return null; return ( - + {!hasCustomHeader && ( { const navigation = useNavigation(); const { styles } = useStyles(createStyles, {}); + const surfaceClass = useElevatedSurface(); const refreshRate = DEPOSIT_CONFIG.RefreshRate / 1000; // Convert to seconds const handleGetNewQuote = () => { @@ -27,7 +29,7 @@ const PerpsQuoteExpiredModal = () => { }; return ( - + {strings('perps.deposit.quote_expired_modal.title')} diff --git a/app/components/UI/Predict/components/PredictPreviewSheet/PredictPreviewSheet.tsx b/app/components/UI/Predict/components/PredictPreviewSheet/PredictPreviewSheet.tsx index 595d9540b31..eff77b3d777 100644 --- a/app/components/UI/Predict/components/PredictPreviewSheet/PredictPreviewSheet.tsx +++ b/app/components/UI/Predict/components/PredictPreviewSheet/PredictPreviewSheet.tsx @@ -16,6 +16,7 @@ import { usePredictBottomSheet, type PredictBottomSheetRef, } from '../../hooks/usePredictBottomSheet'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; interface PredictPreviewSheetProps { renderHeader?: () => React.ReactNode; @@ -55,6 +56,7 @@ const PredictPreviewSheet = forwardRef< handleSheetClosed, getRefHandlers, } = usePredictBottomSheet({ onDismiss }); + const surfaceClass = useElevatedSurface(); useImperativeHandle(ref, getRefHandlers, [getRefHandlers]); @@ -69,6 +71,7 @@ const PredictPreviewSheet = forwardRef< isFullscreen={isFullscreen} onClose={handleSheetClosed} testID={testID} + twClassName={surfaceClass} > ( handleSheetClosed, getRefHandlers, } = usePredictBottomSheet(); + const surfaceClass = useElevatedSurface(); useImperativeHandle(ref, getRefHandlers, [getRefHandlers]); @@ -44,6 +46,7 @@ const PredictWithdrawUnavailableSheet = forwardRef( isInteractable onClose={handleSheetClosed} testID={PREDICT_BALANCE_TEST_IDS.WITHDRAW_UNAVAILABLE_SHEET} + twClassName={surfaceClass} > { /* no-op until initialized */ }); const closeHeadlessOnUnmountRef = useRef<() => void>(() => undefined); + const surfaceClass = useElevatedSurface(); closeHeadlessOnUnmountRef.current = () => { if (!headlessSessionId || hasTerminatedHeadlessSessionRef.current) { return; @@ -584,6 +586,7 @@ const Checkout = () => { goBack={navigation.goBack} isFullscreen keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > {sharedHeader} @@ -618,6 +621,7 @@ const Checkout = () => { isFullscreen isInteractable={!Device.isAndroid()} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > {sharedHeader} { goBack={navigation.goBack} isFullscreen keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > {sharedHeader} diff --git a/app/components/UI/Ramp/Views/Modals/ErrorDetailsModal/ErrorDetailsModal.tsx b/app/components/UI/Ramp/Views/Modals/ErrorDetailsModal/ErrorDetailsModal.tsx index 5d2329bddb5..b661da85573 100644 --- a/app/components/UI/Ramp/Views/Modals/ErrorDetailsModal/ErrorDetailsModal.tsx +++ b/app/components/UI/Ramp/Views/Modals/ErrorDetailsModal/ErrorDetailsModal.tsx @@ -5,18 +5,18 @@ import { useNavigation, type ParamListBase } from '@react-navigation/native'; import type { StackNavigationProp } from '@react-navigation/stack'; import { BottomSheet, - type BottomSheetRef, - Text, - TextVariant, - TextColor, Button, - ButtonVariant, ButtonBaseSize, + ButtonVariant, HeaderStandard, Icon, + IconColor, IconName, IconSize, - IconColor, + Text, + TextColor, + TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { useStyles } from '../../../../../hooks/useStyles'; import { @@ -27,6 +27,7 @@ import Routes from '../../../../../../constants/navigation/Routes'; import { strings } from '../../../../../../../locales/i18n'; import Logger from '../../../../../../util/Logger'; import styleSheet from './ErrorDetailsModal.styles'; +import { useElevatedSurface } from '../../../../../../util/theme/themeUtils'; export interface ErrorDetailsModalParams { errorMessage: string; @@ -57,6 +58,7 @@ function ErrorDetailsModal() { showChangeProvider, amount, } = useParams(); + const surfaceClass = useElevatedSurface(); const handleClose = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -91,7 +93,11 @@ function ErrorDetailsModal() { }, [navigation, amount]); return ( - + { trackEvent( @@ -256,7 +258,11 @@ function PaymentSelectionModal() { }; return ( - + (); + const surfaceClass = useElevatedSurface(); const handleClose = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -96,7 +98,11 @@ function ProcessingInfoModal() { ]); return ( - + { @@ -180,6 +182,7 @@ function ProviderSelectionModal() { ref={sheetRef} goBack={navigation.goBack} onClose={handleDismiss} + twClassName={surfaceClass} > (false); @@ -196,7 +197,11 @@ function SettingsModal() { }, []); return ( - + @@ -160,7 +161,11 @@ function StateSelectorModal() { }, [scrollToTop]); return ( - + sheetRef.current?.onCloseBottomSheet()} diff --git a/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx b/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx index a110e10b27c..09a20830fcb 100644 --- a/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx +++ b/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx @@ -3,14 +3,14 @@ import { View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { BottomSheet, - type BottomSheetRef, - Text, - TextVariant, - TextColor, Button, - ButtonVariant, ButtonBaseSize, + ButtonVariant, HeaderStandard, + Text, + TextColor, + TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { strings } from '../../../../../../../locales/i18n'; import { @@ -26,8 +26,8 @@ import { createProviderSelectionModalNavigationDetails } from '../ProviderSelect import { useAnalytics } from '../../../../../hooks/useAnalytics/useAnalytics'; import { MetaMetricsEvents } from '../../../../../../core/Analytics'; import { TOKEN_NOT_AVAILABLE_MODAL_TEST_IDS } from './TokenNotAvailableModal.testIds'; - import type { BuyFlowOrigin } from '../../BuildQuote/BuildQuote'; +import { useElevatedSurface } from '../../../../../../util/theme/themeUtils'; export interface TokenNotAvailableModalParams { assetId: string; @@ -50,6 +50,7 @@ function TokenNotAvailableModal() { const { selectedProvider } = useRampsProviders(); const { selectedToken } = useRampsTokens(); + const surfaceClass = useElevatedSurface(); const tokenName = selectedToken?.name ?? ''; const providerName = selectedProvider?.name ?? ''; @@ -161,6 +162,7 @@ function TokenNotAvailableModal() { goBack={navigation.goBack} onClose={handleDismiss} testID={TOKEN_NOT_AVAILABLE_MODAL_TEST_IDS.MODAL} + twClassName={surfaceClass} > (); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); const closeBottomSheetAndNavigate = useCallback( (navigateFunc: () => void) => { @@ -81,6 +81,7 @@ function UnsupportedStateModal() { ref={sheetRef} goBack={navigation.goBack} isInteractable={false} + twClassName={surfaceClass} > diff --git a/app/components/UI/Ramp/Views/Modals/UnsupportedTokenModal/UnsupportedTokenModal.tsx b/app/components/UI/Ramp/Views/Modals/UnsupportedTokenModal/UnsupportedTokenModal.tsx index 19be6b42275..e18678aa9ea 100644 --- a/app/components/UI/Ramp/Views/Modals/UnsupportedTokenModal/UnsupportedTokenModal.tsx +++ b/app/components/UI/Ramp/Views/Modals/UnsupportedTokenModal/UnsupportedTokenModal.tsx @@ -4,16 +4,17 @@ import { useNavigation } from '@react-navigation/native'; import { BottomSheet, - type BottomSheetRef, HeaderStandard, Text, TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { strings } from '../../../../../../../locales/i18n'; import styleSheet from './UnsupportedTokenModal.styles'; import { useStyles } from '../../../../../hooks/useStyles'; import { createNavigationDetails } from '../../../../../../util/navigation/navUtils'; import Routes from '../../../../../../constants/navigation/Routes'; +import { useElevatedSurface } from '../../../../../../util/theme/themeUtils'; export const createUnsupportedTokenModalNavigationDetails = createNavigationDetails( @@ -25,9 +26,14 @@ function UnsupportedTokenModal() { const sheetRef = useRef(null); const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); return ( - + sheetRef.current?.onCloseBottomSheet()} diff --git a/app/components/UI/Ramp/components/EligibilityFailedModal/EligibilityFailedModal.tsx b/app/components/UI/Ramp/components/EligibilityFailedModal/EligibilityFailedModal.tsx index 7665cc1ba3b..d127592438f 100644 --- a/app/components/UI/Ramp/components/EligibilityFailedModal/EligibilityFailedModal.tsx +++ b/app/components/UI/Ramp/components/EligibilityFailedModal/EligibilityFailedModal.tsx @@ -4,15 +4,14 @@ import { useNavigation } from '@react-navigation/native'; import { BottomSheet, BottomSheetHeader, - type BottomSheetRef, - Text, - TextVariant, - TextColor, Button, ButtonSize, ButtonVariant, + Text, + TextColor, + TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; - import styleSheet from './EligibilityFailedModal.styles'; import { useStyles } from '../../../../hooks/useStyles'; import { createNavigationDetails } from '../../../../../util/navigation/navUtils'; @@ -20,6 +19,7 @@ import Routes from '../../../../../constants/navigation/Routes'; import { strings } from '../../../../../../locales/i18n'; import { ELIGIBILITY_FAILED_MODAL_TEST_IDS } from './EligibilityFailedModal.testIds'; import { METAMASK_SUPPORT_URL } from '../../../../../constants/urls'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; const SUPPORT_URL = METAMASK_SUPPORT_URL; @@ -33,6 +33,7 @@ function EligibilityFailedModal() { const sheetRef = useRef(null); const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); const navigateToContactSupport = useCallback(() => { Linking.openURL(SUPPORT_URL).catch((error: unknown) => { @@ -50,6 +51,7 @@ function EligibilityFailedModal() { goBack={navigation.goBack} isInteractable={false} testID={ELIGIBILITY_FAILED_MODAL_TEST_IDS.MODAL} + twClassName={surfaceClass} > (null); const navigation = useNavigation(); + const surfaceClass = useElevatedSurface(); const handleClose = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -38,6 +39,7 @@ function RampUnsupportedModal() { goBack={navigation.goBack} isInteractable={false} testID={RAMP_UNSUPPORTED_MODAL_TEST_IDS.MODAL} + twClassName={surfaceClass} > = ({ const { trackEvent, createEventBuilder } = useAnalytics(); const { optInToCampaign, isOptingIn, optInError } = useOptInToCampaign(); const { showToast, RewardsToastOptions } = useRewardsToast(); + const surfaceClass = useElevatedSurface(); const handleOptIn = useCallback(async () => { try { @@ -76,7 +78,7 @@ const CampaignOptInSheet: React.FC = ({ ]); return ( - + {/* Header: centered title + close button */} = ({ }) => { const selectedGroup = useSelector(selectResolvedSelectedAccountGroup); const { height: screenHeight } = useWindowDimensions(); + const surfaceClass = useElevatedSurface(); const listStyle = useMemo( () => ({ maxHeight: screenHeight * 0.4 }), [screenHeight], ); return ( - + sheetRef.current?.onCloseBottomSheet(onClose)} > diff --git a/app/components/UI/Rewards/components/Campaigns/OndoAfterHoursSheet.tsx b/app/components/UI/Rewards/components/Campaigns/OndoAfterHoursSheet.tsx index 522f16b5820..1327a060d14 100644 --- a/app/components/UI/Rewards/components/Campaigns/OndoAfterHoursSheet.tsx +++ b/app/components/UI/Rewards/components/Campaigns/OndoAfterHoursSheet.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { + BottomSheet, Box, BoxAlignItems, + BoxBackgroundColor, BoxFlexDirection, BoxJustifyContent, - BoxBackgroundColor, - BottomSheet, Button, ButtonIcon, ButtonSize, @@ -21,6 +21,7 @@ import { } from '@metamask/design-system-react-native'; import { strings } from '../../../../../../locales/i18n'; import { formatTimeRemaining } from '../../utils/formatUtils'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; interface OndoAfterHoursSheetProps { onClose: () => void; @@ -35,8 +36,10 @@ const OndoAfterHoursSheet: React.FC = ({ }) => { const countdownText = nextOpenAt ? formatTimeRemaining(nextOpenAt) : null; + const surfaceClass = useElevatedSurface(); + return ( - + {/* Header row: spacer + close button */} = ({ onClose, onConfirm, -}) => ( - - - {/* Close button */} - - - +}) => { + const surfaceClass = useElevatedSurface(); - {/* Warning icon */} - - - - - {/* Title */} - - + + {/* Close button */} + - {strings('rewards.ondo_campaign_not_eligible.title')} - - + + - {/* Body */} - - - {strings('rewards.ondo_campaign_not_eligible.body', { - days: ONDO_GM_REQUIRED_QUALIFIED_DAYS, - })} - - + {/* Warning icon */} + + + - {/* Buttons */} - - - + {/* Title */} + + + {strings('rewards.ondo_campaign_not_eligible.title')} + + + + {/* Body */} + + + {strings('rewards.ondo_campaign_not_eligible.body', { + days: ONDO_GM_REQUIRED_QUALIFIED_DAYS, + })} + + + + {/* Buttons */} + + + + - - -); + + ); +}; export default OndoNotEligibleSheet; diff --git a/app/components/UI/TokenDetails/components/RwaUnavailableBottomSheet/RwaUnavailableBottomSheet.tsx b/app/components/UI/TokenDetails/components/RwaUnavailableBottomSheet/RwaUnavailableBottomSheet.tsx index 79c5af2219a..32aa7ebb9cd 100644 --- a/app/components/UI/TokenDetails/components/RwaUnavailableBottomSheet/RwaUnavailableBottomSheet.tsx +++ b/app/components/UI/TokenDetails/components/RwaUnavailableBottomSheet/RwaUnavailableBottomSheet.tsx @@ -13,14 +13,15 @@ import { BottomSheetHeader, BottomSheetRef, Box, - Text, - TextVariant, BoxAlignItems, BoxJustifyContent, + Text, + TextVariant, } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; import { useNavigation } from '@react-navigation/native'; import { strings } from '../../../../../../locales/i18n'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; const ONDO_ELIGIBILITY_URL = 'https://docs.ondo.finance/ondo-global-markets/eligibility'; @@ -42,6 +43,7 @@ const RwaUnavailableBottomSheet = forwardRef< const [isVisible, setIsVisible] = useState(false); const tw = useTailwind(); const navigation = useNavigation(); + const surfaceClass = useElevatedSurface(); const handleSheetClosed = useCallback(() => { setIsVisible(false); @@ -99,7 +101,12 @@ const RwaUnavailableBottomSheet = forwardRef< } return ( - + {strings('rwa.unavailable.title')} diff --git a/app/components/Views/AddressSelector/AddressSelector.tsx b/app/components/Views/AddressSelector/AddressSelector.tsx index ed5392bcada..cc869d414d6 100644 --- a/app/components/Views/AddressSelector/AddressSelector.tsx +++ b/app/components/Views/AddressSelector/AddressSelector.tsx @@ -9,12 +9,12 @@ import { AddressSelectorParams } from './AddressSelector.types'; import { AccountGroupId } from '@metamask/account-api'; import { BottomSheet, - type BottomSheetRef, BottomSheetHeader, Box, BoxAlignItems, BoxFlexDirection, BoxJustifyContent, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { isCaipChainId } from '@metamask/utils'; @@ -40,6 +40,7 @@ import { createAccountSelectorNavDetails } from '../AccountSelector'; import { NetworkConfiguration } from '@metamask/network-controller'; import { strings } from '../../../../locales/i18n'; import { AddressSelectorSelectors } from './AddressSelector.testIds'; +import { useElevatedSurface } from '../../../util/theme/themeUtils'; export const createAddressSelectorNavDetails = createNavigationDetails( @@ -72,6 +73,7 @@ const AddressSelector = () => { ); const accountName = useAccountName(); + const surfaceClass = useElevatedSurface(); const selectedCaipChainId = isCaipChainId(selectedChainId) ? selectedChainId : toEvmCaipChainId(selectedChainId); @@ -151,7 +153,12 @@ const AddressSelector = () => { }, [internalAccountsSpreadByScopes, isEvmOnly, displayOnlyCaipChainIds]); return ( - + sheetRef.current?.onCloseBottomSheet()}> {strings('address_selector.select_an_address')} diff --git a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx index daad79080bb..adb83c249ec 100644 --- a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx +++ b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx @@ -1,16 +1,16 @@ import React, { useState, useCallback, useRef } from 'react'; import { View } from 'react-native'; import { - Text, + BottomSheet, + Button, + ButtonBaseSize, ButtonIcon, + ButtonVariant, Checkbox, - TextVariant, IconName, + Text, TextColor, - Button, - ButtonVariant, - ButtonBaseSize, - BottomSheet, + TextVariant, type BottomSheetRef, } from '@metamask/design-system-react-native'; import { useNavigation, useTheme } from '@react-navigation/native'; @@ -22,6 +22,7 @@ import { RootState } from '../../../../reducers'; import { useDispatch, useSelector } from 'react-redux'; import { setMultichainAccountsIntroModalSeen } from '../../../../actions/user'; import { LEARN_MORE_BOTTOM_SHEET_TEST_IDS } from './LearnMoreBottomSheet.testIds'; +import { useElevatedSurface } from '../../../../util/theme/themeUtils'; interface LearnMoreBottomSheetProps { onClose?: () => void; @@ -39,6 +40,7 @@ const LearnMoreBottomSheet: React.FC = ({ const isBasicFunctionalityEnabled = useSelector( (state: RootState) => state?.settings?.basicFunctionalityEnabled, ); + const surfaceClass = useElevatedSurface(); const handleBack = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -66,6 +68,7 @@ const LearnMoreBottomSheet: React.FC = ({ ref={sheetRef} onClose={onClose} testID={LEARN_MORE_BOTTOM_SHEET_TEST_IDS.BOTTOM_SHEET} + twClassName={surfaceClass} > diff --git a/app/components/Views/NetworksManagement/components/DeleteNetworkModal.tsx b/app/components/Views/NetworksManagement/components/DeleteNetworkModal.tsx index 21baec712bc..8c48e170750 100644 --- a/app/components/Views/NetworksManagement/components/DeleteNetworkModal.tsx +++ b/app/components/Views/NetworksManagement/components/DeleteNetworkModal.tsx @@ -1,19 +1,20 @@ import React, { forwardRef } from 'react'; import { - Box, BottomSheet, BottomSheetFooter, BottomSheetHeader, - type BottomSheetRef, + Box, + BoxAlignItems, ButtonSize, Text, TextVariant, - BoxAlignItems, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { strings } from '../../../../../locales/i18n'; import { NetworksManagementViewSelectorsIDs } from '../NetworksManagementView.testIds'; +import { useElevatedSurface } from '../../../../util/theme/themeUtils'; interface DeleteNetworkModalProps { networkName: string; @@ -37,12 +38,15 @@ const DeleteNetworkModal = forwardRef( testID: NetworksManagementViewSelectorsIDs.DELETE_CONFIRM_BUTTON, }; + const surfaceClass = useElevatedSurface(); + return ( {`${strings('app_settings.delete')} ${networkName} ${strings('app_settings.network')}`} diff --git a/app/components/Views/OnboardingSheet/index.tsx b/app/components/Views/OnboardingSheet/index.tsx index c77ef589989..cbf0dab4398 100644 --- a/app/components/Views/OnboardingSheet/index.tsx +++ b/app/components/Views/OnboardingSheet/index.tsx @@ -2,6 +2,8 @@ import React, { useRef } from 'react'; import { strings } from '../../../../locales/i18n'; import { useTheme } from '../../../util/theme'; import { AppThemeKey } from '../../../util/theme/models'; +import { useElevatedSurface } from '../../../util/theme/themeUtils'; + import GoogleIcon from 'images/google.svg'; import AppleIcon from 'images/apple.svg'; import AppleWhiteIcon from 'images/apple-white.svg'; @@ -57,7 +59,6 @@ const OnboardingSheet = () => { } = params ?? {}; const { colors } = useTheme(); const tw = useTailwind(); - const onPressCreateAction = () => { if (onPressCreate) { onPressCreate(); @@ -109,10 +110,15 @@ const OnboardingSheet = () => { }; const { themeAppearance } = useTheme(); + const surfaceClass = useElevatedSurface(); const isDark = themeAppearance === AppThemeKey.dark; return ( - + { const bottomSheetRef = useRef(null); const navigation = useNavigation(); - + const surfaceClass = useElevatedSurface(); const goBack = useCallback(() => { goBackIfFocused(navigation); }, [navigation]); return ( - + = ({ const [isContentReady, setIsContentReady] = useState(false); const [activeScreen, setActiveScreen] = useState('amount'); const isSubmittingTx = useSelector(selectIsSubmittingTx); + const surfaceClass = useElevatedSurface(); useEffect(() => { bottomSheetRef.current?.onOpenBottomSheet(() => { @@ -82,6 +84,7 @@ const QuickBuyRootInner: React.FC = ({ ref={bottomSheetRef} isInteractable={!isSubmittingTx} onClose={onClose} + twClassName={surfaceClass} > {isContentReady ? ( = ({ }) => { const [isModalVisible, setIsModalVisible] = useState(false); const bottomSheetRef = useRef(null); - + const surfaceClass = useElevatedSurface(); const { styles } = useStyles(stylesheet, {}); const internalAccountsById = useSelector(selectInternalAccountsById); @@ -209,6 +211,7 @@ const AccountSelector: React.FC = ({ isFullscreen keyboardAvoidingViewEnabled={false} onClose={handleSheetClosed} + twClassName={surfaceClass} > {title} diff --git a/app/components/Views/confirmations/components/send/send-alert-modal/send-alert-modal.tsx b/app/components/Views/confirmations/components/send/send-alert-modal/send-alert-modal.tsx index 7a3734b7fc3..f962e02eb2b 100644 --- a/app/components/Views/confirmations/components/send/send-alert-modal/send-alert-modal.tsx +++ b/app/components/Views/confirmations/components/send/send-alert-modal/send-alert-modal.tsx @@ -1,12 +1,12 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Modal } from 'react-native'; import { + BottomSheet, + BottomSheetRef, Box, BoxAlignItems, BoxFlexDirection, BoxJustifyContent, - BottomSheet, - BottomSheetRef, ButtonIcon, Icon, IconColor, @@ -24,6 +24,7 @@ import { } from '../../../../../../component-library/components/Buttons/Button'; import type { SendAlert } from '../../../hooks/send/alerts/types'; import { SendAlertModalProps } from './send-alert-modal.types'; +import { useElevatedSurface } from '../../../../../../util/theme/themeUtils'; function PageNavigation({ alerts, @@ -82,6 +83,7 @@ export const SendAlertModal = ({ onClose, }: SendAlertModalProps) => { const bottomSheetRef = useRef(null); + const surfaceClass = useElevatedSurface(); const [currentIndex, setCurrentIndex] = useState(0); const alertKeys = alerts.map((a) => a.key).join('|'); @@ -124,7 +126,11 @@ export const SendAlertModal = ({ return ( - + { From c6cc0b99ade54172498b7216074cb3de8322fe55 Mon Sep 17 00:00:00 2001 From: Wei Sun Date: Fri, 29 May 2026 15:50:00 -0700 Subject: [PATCH 15/15] ci: remove OTA tag creation step (#30813) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Remove OTA publishing and release-tag creation from the automated/Runway CI flows. OTA updates are now published and tagged manually in Runway, so CI only needs to detect OTA bumps (to skip native builds) and otherwise build. **Changes** **auto-rc-ota-build-core.yml (Auto RC build/OTA core)** - Removed the trigger-ota job (no longer pushes EAS OTA updates via push-eas-update.yml). - Removed the validate-ota-pr job (only existed to guard the OTA push). - Removed the create-ota-production-tag job and the create_production_ota_tag + ota_channel inputs. - Kept OTA detection: resolve-context still runs and trigger-build remains gated on ota_bump != 'true', so OTA-only branches skip the native build instead of building. - Tightened permissions from contents: write + actions: write + pull-requests: read + id-token: write down to contents: read + pull-requests: read + id-token: write. **runway-ota-production.yml (Runway OTA Production)** - Removed the create-ota-production-tag job — the v tag is now created manually in Runway. - Tightened permissions contents: write → contents: read (write was only needed to push the tag). - Updated header comment + source_branch input description. **runway-create-ota-production-tag.yml** Deleted — it was the reusable tag-creation workflow and is no longer referenced by any caller after the changes above. ## **Changelog** CHANGELOG entry:null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [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 release automation boundaries—OTA-only RC pushes will skip native builds without CI publishing OTA, so Runway/manual steps must stay in sync or releases can stall. > > **Overview** > CI no longer publishes EAS OTA updates or creates `v` release tags; those steps move to manual Runway handling. > > **`auto-rc-ota-build-core.yml`** drops `trigger-ota`, `validate-ota-pr`, and `create-ota-production-tag`, plus inputs `ota_channel` and `create_production_ota_tag`. It still runs `resolve-context` and only runs native **`build.yml`** (and TestFlight on iOS) when `ota_bump != 'true'`, so OTA-only commits skip binaries. Permissions are reduced to `contents: read` (no tag push or workflow dispatch for OTA). > > **`runway-ota-production.yml`** still pushes production OTA via `push-eas-update.yml` but no longer creates the production release tag. **`runway-create-ota-production-tag.yml`** is deleted. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0a209ffda3c285091f4397e28ce129bff76dc895. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). Co-authored-by: João Loureiro <175489935+joaoloureirop@users.noreply.github.com> --- .github/workflows/auto-rc-ota-build-core.yml | 73 ++++--------------- .../runway-create-ota-production-tag.yml | 69 ------------------ .github/workflows/runway-ota-production.yml | 16 +--- 3 files changed, 16 insertions(+), 142 deletions(-) delete mode 100644 .github/workflows/runway-create-ota-production-tag.yml diff --git a/.github/workflows/auto-rc-ota-build-core.yml b/.github/workflows/auto-rc-ota-build-core.yml index 2f7e63d830c..f6ab2a9c92d 100644 --- a/.github/workflows/auto-rc-ota-build-core.yml +++ b/.github/workflows/auto-rc-ota-build-core.yml @@ -2,12 +2,14 @@ # # Auto RC OTA / build core (reusable) # -# Shared logic for the Auto RC flow (build-rc-auto.yml): detect an OTA_VERSION bump and either -# dispatch push-eas-update.yml, or fall through to build.yml. +# Shared logic for the Auto RC flow (build-rc-auto.yml): detect whether the push is an +# OTA_VERSION bump and, if so, skip the native build (OTA-only changes are published +# separately). Otherwise fall through to a native build.yml build (and TestFlight for iOS). # -# Runway's manual entry workflows no longer use this file — they call the dedicated OTA-only or -# build-only workflows (runway-ota-*.yml, runway-*-builds.yml) directly. Kept here to preserve -# automatic OTA-vs-build detection on every push to a release branch. +# This workflow does not push OTA updates — OTA publishing is handled outside this flow. +# +# Runway's manual entry workflows do not use this file — they call the dedicated OTA-only or +# build-only workflows (runway-ota-*.yml, runway-*-builds.yml) directly. # ############################################################################################## name: Auto RC OTA Build Core @@ -16,7 +18,7 @@ on: workflow_call: inputs: platform: - description: 'Target platform passed to push-eas-update and build.yml (android or ios)' + description: 'Target platform passed to build.yml (android or ios)' required: true type: string source_branch: @@ -26,21 +28,11 @@ on: required: false type: string default: '' - ota_channel: - description: 'push-eas-update channel input (e.g. rc, production)' - required: false - type: string - default: rc build_name: description: 'build.yml build_name (e.g. main-rc, main-prod)' required: false type: string default: main-rc - create_production_ota_tag: - description: 'If true, create OTA release tag after production trigger-ota (callers: *production* only)' - required: false - type: boolean - default: false environment: description: 'Build environment / track passed to upload-to-testflight (e.g. rc, prod)' required: false @@ -48,19 +40,18 @@ on: default: 'rc' outputs: semantic_version: - description: 'package.json version at the built commit (empty when OTA path taken)' + description: 'package.json version at the built commit (empty when OTA bump skips the build)' value: ${{ jobs.trigger-build.outputs.semantic_version }} ios_version_code: - description: 'iOS CURRENT_PROJECT_VERSION at the built commit (empty when OTA path taken)' + description: 'iOS CURRENT_PROJECT_VERSION at the built commit (empty when OTA bump skips the build)' value: ${{ jobs.trigger-build.outputs.ios_version_code }} android_version_code: - description: 'Android versionCode at the built commit (empty when OTA path taken)' + description: 'Android versionCode at the built commit (empty when OTA bump skips the build)' value: ${{ jobs.trigger-build.outputs.android_version_code }} permissions: - contents: write - pull-requests: read - actions: write + contents: read + pull-requests: read # required by runway-ota-resolve-context.yml id-token: write # required by build.yml jobs: @@ -71,34 +62,6 @@ jobs: source_branch: ${{ inputs.source_branch }} secrets: inherit - validate-ota-pr: - name: Validate PR for OTA - needs: resolve-context - if: needs.resolve-context.outputs.ota_bump == 'true' - runs-on: ubuntu-latest - steps: - - name: Validate PR number - run: | - if [[ -z "${{ needs.resolve-context.outputs.pr_number }}" ]]; then - echo "::error::No PR found for this branch. OTA update requires a PR number." - echo "::error::If you ran the workflow manually (workflow_dispatch), select your release branch in the 'Use workflow from' dropdown (e.g. release/7.71.0), not main." - exit 1 - fi - echo "Using PR #${{ needs.resolve-context.outputs.pr_number }}" - - trigger-ota: - name: Trigger OTA update - needs: [resolve-context, validate-ota-pr] - if: needs.resolve-context.outputs.ota_bump == 'true' - uses: ./.github/workflows/push-eas-update.yml - with: - pr_number: ${{ needs.resolve-context.outputs.pr_number }} - base_branch: ${{ needs.resolve-context.outputs.base_ref }} - message: ${{ needs.resolve-context.outputs.ota_version }} - channel: ${{ inputs.ota_channel }} - platform: ${{ inputs.platform }} - secrets: inherit - trigger-build: name: Trigger build mobile app needs: resolve-context @@ -111,16 +74,6 @@ jobs: upload_to_sentry: true secrets: inherit - create-ota-production-tag: - name: Create OTA production release tag - needs: [resolve-context, trigger-ota] - if: ${{ inputs.create_production_ota_tag == true }} - uses: ./.github/workflows/runway-create-ota-production-tag.yml - with: - tag_name: ${{ needs.resolve-context.outputs.ota_version }} - checkout_ref: ${{ inputs.source_branch || github.ref_name }} - secrets: inherit - upload-ios-testflight: name: Upload iOS to TestFlight needs: [trigger-build] diff --git a/.github/workflows/runway-create-ota-production-tag.yml b/.github/workflows/runway-create-ota-production-tag.yml deleted file mode 100644 index 6119c040b7a..00000000000 --- a/.github/workflows/runway-create-ota-production-tag.yml +++ /dev/null @@ -1,69 +0,0 @@ -############################################################################################## -# -# Reusable: create SemVer release tag after production OTA (idempotent). -# -# Callers: runway_*_production_workflow.yml after trigger-ota succeeds. -# Skips if the tag already points at the checked-out commit; fails if the tag exists elsewhere. -# -############################################################################################## -name: Create OTA production release tag - -on: - workflow_call: - inputs: - tag_name: - description: 'Annotated tag to create; must match OTA_VERSION (app/constants/ota.ts) / decide ota_version' - required: true - type: string - checkout_ref: - description: 'Branch or ref that received the OTA (same as workflow source)' - required: true - type: string - -permissions: - contents: write - -jobs: - create-tag: - name: Create release tag (production OTA) - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ inputs.checkout_ref }} - - - name: Create or skip release tag - env: - TAG_NAME: ${{ inputs.tag_name }} - run: | - set -euo pipefail - if [[ -z "${TAG_NAME}" ]]; then - echo '::error::tag_name is empty; cannot create release tag' - exit 1 - fi - if [[ ! "${TAG_NAME}" =~ ^v[^[:space:]]+$ ]]; then - echo "::error::tag_name must be non-empty and start with v (no spaces), got: ${TAG_NAME}" - exit 1 - fi - - HEAD_SHA=$(git rev-parse HEAD) - git fetch origin --tags --force 2>/dev/null || true - - if git rev-parse -q --verify "refs/tags/${TAG_NAME}" >/dev/null 2>&1; then - TAG_SHA=$(git rev-parse "${TAG_NAME}^{commit}") - if [[ "${HEAD_SHA}" == "${TAG_SHA}" ]]; then - echo "Tag \`${TAG_NAME}\` already points at this commit (${HEAD_SHA}); skipping create and push." - exit 0 - fi - echo "::error::Tag \`${TAG_NAME}\` already exists at ${TAG_SHA} but HEAD is ${HEAD_SHA}. Refusing to move the tag." - exit 1 - fi - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - git tag -a "${TAG_NAME}" -m "Production OTA release ${TAG_NAME}" - git push origin "refs/tags/${TAG_NAME}" - echo "Created and pushed tag \`${TAG_NAME}\` at ${HEAD_SHA}" diff --git a/.github/workflows/runway-ota-production.yml b/.github/workflows/runway-ota-production.yml index 1fa49b2aa59..55a9ae703a8 100644 --- a/.github/workflows/runway-ota-production.yml +++ b/.github/workflows/runway-ota-production.yml @@ -2,8 +2,7 @@ # # Runway OTA Production # -# Triggered from Runway to push an OTA update to the production channel (iOS + Android) and -# create the corresponding `v` release tag. +# Triggered from Runway to push an OTA update to the production channel (iOS + Android). # # This workflow does not build binaries and does not bump the build version — the release PR is # expected to bump OTA_VERSION (app/constants/ota.ts) before dispatch. @@ -16,13 +15,13 @@ on: inputs: source_branch: description: >- - Optional branch, tag, or SHA for OTA publish + tag creation. + Optional branch, tag, or SHA for OTA publish. Empty uses the branch selected in the "Use workflow from" UI. required: false type: string permissions: - contents: write # required to push the v tag + contents: read pull-requests: read id-token: write # required by push-eas-update.yml @@ -59,12 +58,3 @@ jobs: channel: production platform: all secrets: inherit - - create-ota-production-tag: - name: Create OTA production release tag - needs: [resolve-context, push-ota] - uses: ./.github/workflows/runway-create-ota-production-tag.yml - with: - tag_name: ${{ needs.resolve-context.outputs.ota_version }} - checkout_ref: ${{ inputs.source_branch || github.ref_name }} - secrets: inherit