Skip to content

[pull] main from MetaMask:main#575

Merged
pull[bot] merged 13 commits intoReality2byte:mainfrom
MetaMask:main
Mar 4, 2026
Merged

[pull] main from MetaMask:main#575
pull[bot] merged 13 commits intoReality2byte:mainfrom
MetaMask:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Mar 4, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

dan437 and others added 13 commits March 4, 2026 17:37
## **Description**
Disables the Max button for Predict Withdraw. We will enable it once we
support Max with a Predict balance.

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: Disable the Max button for Predict Withdraw

## **Related issues**

Fixes: #26944

## **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**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: a small UI-prop change that only affects whether the Max
button is shown for Predict withdraw, with a focused unit test to
prevent regressions.
> 
> **Overview**
> Disables the **Max** button in the Predict Withdraw confirmation flow
by no longer passing `hasMax` into `CustomAmountInfo` from
`PredictWithdrawInfo`.
> 
> Adds a unit test asserting `PredictWithdrawInfo` does not enable
`hasMax`, ensuring the Max control stays off until a Predict
balance-backed max is supported.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
21b8547. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com>
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

Hides the 'quote expired' bottom sheet when outside of the bridge view
(for example, looking at the tokens list).

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: hides a quote expired element when outside of the
bridge page

## **Related issues**

Fixes:

## **Manual testing steps**

```gherkin
Feature: swaps and bridge

  Scenario: user let's a quote expire

    When user navigates away from the bridge view
    Then we should not show the quote expired bottom sheet
```

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: adds a focus gate around existing modal navigation logic and
extends unit tests; no changes to quote computation or transaction flow.
> 
> **Overview**
> Prevents the Bridge "quote expired" bottom sheet from appearing when
the user has navigated away from the Bridge screen by gating
`useRenderQuoteExpireModal` on `useIsFocused()`.
> 
> Updates the hook tests to mock focus state and cover the new behavior,
including not showing while unfocused and showing once focus is regained
while the quote remains expired.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
23aa0ea. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
)

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**
This PR improves Android Detox launch reliability and debuggability by
adding targeted handling for intermittent adb reverse collisions that
happen during app launch (outside our fixture port-forwarding flow).

### What changed
- Added pre-launch diagnostics in tests/helpers.js to log current
Android reverse mappings:
- Runs adb -s <device> reverse --list immediately before
device.launchApp()
- Logs the same list again when launch fails and after cleanup, to aid
CI triage
- Added guarded launch recovery for a known flaky Detox failure mode:
- Detects launch errors containing adb reverse ... Address already in
use
- Extracts the conflicting ephemeral port from the error (pattern
`reverse tcp:<PORT> tcp:<PORT>`)
- Removes only that conflicting reverse mapping (`adb reverse --remove
tcp:<PORT>`)
  - Retries device.launchApp() once
- Reused the same recovery path in both:
  - standard launch flow (launchApp)
  - debug/deep-link launch flow (launchAppForDebugBuild)

### Why this is needed
- Our fixture forwarding uses static→allocated mappings (e.g. 12345 ->
40151) and logs through FixtureUtils.
- The CI failure seen (reverse tcp:39825 tcp:39825) is Detox-managed
ephemeral forwarding during launch, not fixture forwarding.
- When that ephemeral reverse mapping is already present/stale, launch
fails before Detox can connect to the app.
- This PR adds observability plus a minimal, targeted recovery for that
exact case without disturbing normal fixture forwards.

 ### Additional test update in this branch
- Updated tests/smoke/card/card-home-add-funds.spec.ts analytics checks
to use:
  - Assertions.checkIfArrayHasLength(<events>, 1)
- This prevents false positives where filter(...) returns [] (truthy)
and previously passed checkIfValueIsDefined.



<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry:

## **Related issues**

Fixes: https://consensyssoftware.atlassian.net/browse/MMQA-1548

## **Manual testing steps**
N/A

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**
N/A
<!-- [screenshots/recordings] -->

### **After**
N/A
<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds Android-specific `adb` reverse inspection and cleanup plus a
one-time relaunch retry, which can change CI/device launch behavior and
relies on parsing error strings and executing shell commands. Scope is
limited to Detox test helpers and one smoke spec assertion change.
> 
> **Overview**
> Improves Android Detox launch reliability by routing all
`TestHelpers.launchApp()` (including debug/deeplink launches) through a
new `launchAppWithRecovery()` wrapper that logs current `adb reverse
--list`, detects the flaky *"Address already in use"* reverse-port
collision, removes the conflicting `tcp:<port>` mapping, and retries
`device.launchApp()` once.
> 
> Also tightens the Card Home smoke test analytics assertions by
checking filtered event arrays have length `1` (and removes an unused
expected event), preventing false positives when no events are emitted.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5f8cfec. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**
Don't show OTA modal when users are on the onboarding screen, the
updates will apply in the next session automatically.

Ticket: https://consensyssoftware.atlassian.net/browse/MCWP-374
Testing workflow:
https://github.com/MetaMask/metamask-mobile/actions/runs/22643630725/job/65629911055

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: Removed showing OTA modal when users are on onboarding
screen

## **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**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**
1. When users are on the onboarding screen and close the app, there is
no modal and the updates apply in the next session

![onboarding](https://github.com/user-attachments/assets/525a471b-d441-4529-85b3-5a83fc351a4a)

2. When users are on the onboarding screen and continue the onboarding,
there is no modal and the updates apply in the next session
![logging
in](https://github.com/user-attachments/assets/2c46f9c3-a086-4c17-988e-cf0659119027)
second launch
<img width="463" height="930" alt="Screenshot 2026-03-03 at 2 26 06 PM"
src="https://github.com/user-attachments/assets/47d3d1d1-e0aa-4306-8679-dea48fbc36ac"
/>



4. When users are already logged in (no changes from current behaviour)
![logged
in](https://github.com/user-attachments/assets/52f0508f-0914-4896-b77a-121eaaa38068)



## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: small conditional change to OTA-update UX gating with
targeted test updates; main risk is accidentally suppressing the update
modal for some users if onboarding state is misreported.
> 
> **Overview**
> Prevents the OTA update modal from showing during onboarding by gating
the `useOTAUpdates` post-download behavior on
`selectCompletedOnboarding`.
> 
> When a new update is fetched while onboarding is incomplete, the hook
now **logs and defers applying the update to the next app launch**
instead of navigating to `OTAUpdatesModal`; existing behavior (showing
the modal after interactions) remains for users who already completed
onboarding. Tests are updated to cover both onboarding-complete and
onboarding-incomplete cases and to assert no `reloadAsync` call from the
hook.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0074e7c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

This PR updates the Activity (transactions) screen to use the shared
header components **HeaderRoot** and **HeaderCompactStandard** with
inline headers and proper safe area handling, following the same pattern
as settings-general.

**Reason for change:** The Activity screen previously used a custom
`HeaderBase` with manual back-button logic and relied on
`navigation.setOptions(getTransactionsNavbarOptions(...))` for the stack
header. That `setOptions` call had no visible effect because the screen
is already registered with `headerShown: false` in the navigator.
Unifying on HeaderRoot/HeaderCompactStandard improves consistency,
removes dead code, and ensures the screen is wrapped in SafeAreaView for
correct insets.

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: null

## **Related issues**

Fixes: https://consensyssoftware.atlassian.net/browse/DSYS-503

## **Manual testing steps**

```gherkin
Feature: Activity screen header and safe area

  Scenario: Activity tab shows HeaderRoot with title only
    Given the app is on the main tab bar
    When the user taps the Activity tab
    Then the Activity screen shows with a header that displays "Activity" and has no back button and no end accessory

  Scenario: Perps Activity (from See all) shows HeaderCompactStandard with back button
    Given the user is in the Perps section (e.g. Perps home with recent activity or market trades)
    When the user taps "See all" to open Activity with the Perps tab focused
    Then the Activity screen shows with a header that displays "Activity" and has a back button; tapping back returns to the previous screen

  Scenario: Safe area and layout
    Given the app is on the Activity screen (tab or Perps entry)
    When the user views the screen on a device with a notch or safe area
    Then the header and content respect the safe area (no overlap with status bar or home indicator)
```

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->


### **After**

<!-- [screenshots/recordings] -->


https://github.com/user-attachments/assets/f3174511-a4a6-4121-89ed-435b28820d75

## **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.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> User-visible layout/navigation changes on the Activity screen
(safe-area handling and header rendering/back behavior) could regress
spacing or back navigation in some entry points. Also removes previously
wired metrics/navigation option code, which may affect analytics
expectations if still relied on elsewhere.
> 
> **Overview**
> Updates the Activity screen to render an inline `SafeAreaView` and
shared header components (`HeaderRoot` by default,
`HeaderCompactStandard` when `showBackButton` is true), instead of the
previous `HeaderBase` + `navigation.setOptions(...)` approach.
> 
> Removes now-unused account-switcher/metrics and safe-area inset
plumbing, adds new testIDs for the safe area + headers, and updates unit
tests/snapshots to assert the new header/safe-area structure.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
bdcaf99. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**
Display OTA Summary in the workflow

test workflow:
https://github.com/MetaMask/metamask-mobile/actions/runs/22647455875

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: Added OTA Summary in the workflow

## **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**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**
<img width="612" height="358" alt="Screenshot 2026-03-03 at 3 25 27 PM"
src="https://github.com/user-attachments/assets/13dbf239-8bdb-4198-9df0-16a072b37a19"
/>


## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: adds a new GitHub Actions job that only writes informational
output to the workflow run summary and does not change build/publish
logic.
> 
> **Overview**
> Adds an **"OTA Update Summary"** job to `push-eas-update.yml` that
writes key dispatch inputs (update message/version, base ref, channel,
and target commit) to `$GITHUB_STEP_SUMMARY` for easier visibility when
running the workflow.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
7cfb169. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

Fixes Ledger blind signing errors showing as "Something went wrong"
instead of "Blind Signing Disabled" during swaps and contract
interactions.
`@metamask/eth-ledger-bridge-keyring` wraps the Ledger
`TransportStatusError(0x6985)` into a `HardwareWalletError` with
`ErrorCode.Unknown` due to a cross-realm instanceof failure in the RN
bundle. Our parser accepted that classification without checking the
message, which clearly contains "Blind signing".
The fix re-parses `ErrorCode.Unknown` errors by checking the message
against known patterns before giving up.

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: null

## **Related issues**

Fixes:

## **Manual testing steps**

no manual testing steps

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [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.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes error classification for `ErrorCode.Unknown` objects, which
can affect downstream UI/flows that key off specific error codes. Scope
is small and limited to message-based re-parsing before falling back to
`Unknown`.
> 
> **Overview**
> Ensures `parseErrorByType` doesn’t blindly accept `{ code:
ErrorCode.Unknown, message }` objects: it now attempts
`parseErrorByMessage` first and returns a more specific
`HardwareWalletError` when the message matches known patterns (notably
Ledger blind-signing/contract-data prompts).
> 
> Adds a unit test covering re-parsing an `ErrorCode.Unknown` object
into `ErrorCode.DeviceStateBlindSignNotSupported`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
386cba3. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**
## Summary

This PR adds **Sentry instrumentation for Performance E2E tests** so
timer-based metrics are sent per scenario, with CI controls to choose
whether data goes to **test** or **real** Sentry.

---

## What changed

### 1) New Sentry publisher for performance scenarios

- Added:
  - `tests/reporters/providers/sentry/PerformanceSentryPublisher.ts`
- `tests/reporters/providers/sentry/PerformanceSentryPublisher.test.ts`

The publisher sends one Sentry `transaction` per scenario, including:

- timer durations as `measurements` (ms),
- scenario metadata (`project`, `tags`, `status`, `retry`, `worker`,
team),
- timer validation details under `extra.timer_steps`.

---

### 2) Integrated publishing into performance fixture (best-effort)

- Updated:
  - `tests/framework/fixtures/performance/performance-fixture.ts`

After metrics are attached, the fixture attempts Sentry publishing.  
Publishing is wrapped in `try/catch` so Sentry failures do **not**
block:

- quality gate validation,
- session-data attachment.

---

### 3) CI workflow selector for Sentry target (`test | real`)

- Updated:
  - `.github/workflows/run-performance-e2e.yml`
  - `.github/workflows/run-performance-e2e-release.yml`
  - `.github/workflows/run-performance-e2e-experimental.yml`
  - `.github/workflows/performance-test-runner.yml`

New workflow input: `sentry_target`

Behavior:

- `test` → uses `MM_SENTRY_DSN_TEST`
- `real` → uses `MM_SENTRY_DSN`
- invalid value → explicit workflow error

For real target, environment is tagged as:

- `github-actions-performance-e2e-real`

---

### 4) Publisher hardening fixes

In `PerformanceSentryPublisher.ts`:

- Measurement keys now remain within the intended **64-char max**,
including collision suffixes.
- Reserved aggregate keys are protected from timer-key collisions:
  - `scenario_total_time_ms`
  - `scenario_total_threshold_ms`
- Sentry envelope endpoint now includes auth query params:
  - `sentry_key=<publicKey>`
  - `sentry_version=7`

---

### 5) Types and docs updates

- Updated:
  - `tests/reporters/PerformanceTracker.ts` (`MetricsOutput` exported)
  - `tests/performance/README.md` (Sentry instrumentation section)
  - `.e2e.env.example` (new `E2E_PERFORMANCE_SENTRY_*` vars)

---

## Notes for reviewers

- Scope is test infra + CI only (no product runtime behavior changes).
- Sentry publishing is intentionally **best-effort** and non-blocking
for teardown-critical steps.
- Unit tests were expanded to cover:
  - DSN parsing/auth endpoint format,
  - reserved measurement key protection,
  - key-length constraints under collisions,
  - sample-rate validation.
<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

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**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds new outbound Sentry uploads from performance E2E runs and threads
new secrets/inputs through GitHub Actions, which could affect CI
behavior if misconfigured or if event volume is high. Uploads are gated
by env flags/DSN and include sampling controls, reducing blast radius.
> 
> **Overview**
> Adds **optional Sentry instrumentation for performance E2E
scenarios**, emitting one Sentry `transaction` per test with timer
durations as measurements plus scenario metadata.
> 
> Introduces `PerformanceSentryPublisher` (with unit tests) and wires it
into the performance fixture so metrics are published after
`PerformanceTracker.attachToTest()` succeeds; `MetricsOutput` is
exported to support this integration.
> 
> Updates performance GitHub Actions workflows to accept a
`sentry_target` (test vs real), select the appropriate DSN secret, and
export `E2E_PERFORMANCE_SENTRY_*` env vars; documents the new
configuration in `.e2e.env.example` and `tests/performance/README.md`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
2ba0d7f. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: javiergarciavera <javiergarciavera@users.noreply.github.com>
…tItem components (#26953)

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**
This PR fixes an issue where the mUSD "Claim bonus" was displayed for
geo-blocked users.

### Changes:
- Create `selectMerklClaimTransactions` selector with deep-equal
memoization to prevent re-render cascades from unrelated transaction
updates
- Create `useMerklBonusClaim` hook to replace `MerklClaimHandler`
headless component pattern, composing `useMerklRewards`,
`usePendingMerklClaim`, and `useMerklClaim` with internal eligibility
gating
- Update `usePendingMerklClaim` to use targeted Merkl transaction
selector
- Refactor `TokenListItem` and `TokenListItemV2` to use
`useMerklBonusClaim` directly
- Rename `isEligibleForMerklRewards` to `isTokenEligibleForMerklRewards`
- Remove `MerklClaimHandler`
- Rename `useMerklClaim` to `useMerklClaimTransaction` to better
differentiate against `useMerklBonusClaim`
<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: refactored merkl claim headless structure in token list
item components and prevent "claim bonus" cta from rendering when users
are geo-blocked

## **Related issues**

Fixes: [MUSD-450: Claim bonus visible for geo-blocked
users](https://consensyssoftware.atlassian.net/browse/MUSD-450)

## **Manual testing steps**

```gherkin
Feature: Geo-blocked users cannot see Merkl "Claim bonus" CTA

  Scenario: user in allowed region sees "Claim bonus" CTA
    Given user is in a non-blocked region
    And user holds a Merkl-eligible token with claimable rewards
    And the Merkl campaign claiming feature flag is enabled

    When user views the token in the token list
    Then the "Claim bonus" CTA is displayed on the token row

  Scenario: user in blocked region does not see "Claim bonus" CTA
    Given user is in a geo-blocked region
    And user holds a Merkl-eligible token with claimable rewards
    And the Merkl campaign claiming feature flag is enabled

    When user views the token in the token list
    Then the "Claim bonus" CTA is not displayed
    And the standard percentage change is shown instead
```

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**
`"Claim bonus"` CTA is displayed for geo-blocked users.
<!-- [screenshots/recordings] -->

### **After**
`"Claim bonus"` CTA is hidden for geo-blocked users.

<!-- [screenshots/recordings] -->
Recording to show claim experience this functions correctly after the
refactor


https://github.com/user-attachments/assets/6e6af59f-5f0a-4c69-8ad7-bc1a75eae61c

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes the eligibility and pending-transaction plumbing that controls
when the Merkl “Claim bonus” CTA is shown and when claim
spinners/pending state appear, so regressions could hide/show the CTA
incorrectly. Scope is limited to Merkl claim UI/hooks and is covered by
new/updated unit tests.
> 
> **Overview**
> Fixes Merkl “Claim bonus” CTA eligibility by replacing the
`MerklClaimHandler` headless component/state-sync pattern with a new
`useMerklBonusClaim` hook that composes claimable rewards, pending-claim
status, and claim transaction submission while **no-op’ing for
ineligible/geo-blocked users**.
> 
> Adds `selectMerklClaimTransactions` (deep-equal memoized) and updates
`usePendingMerklClaim` to consume this filtered selector instead of
scanning all transactions, reducing unrelated re-render cascades.
> 
> Renames `isEligibleForMerklRewards` to
`isTokenEligibleForMerklRewards`, renames `useMerklClaim` to
`useMerklClaimTransaction`, and refactors
`TokenListItem`/`TokenListItemV2` (and tests) to rely solely on the
composed hook for CTA/spinner behavior.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
820459d. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

Migrates all Card feature data-fetching hooks from a custom Redux-based
caching system (`useWrapWithCache`) to React Query
(`@tanstack/react-query`), reducing complexity and eliminating the
persisted Redux `card` slice.

**Why**: The `useWrapWithCache` hook duplicated caching logic that React
Query handles natively (stale-while-revalidate, garbage collection,
deduplication). React Query keeps this data in-memory only.

**What changed**:
- **Replaced `useWrapWithCache`** with `useQuery` / `useQueryClient`
across 10+ hooks: `useGetDelegationSettings`,
`useGetCardExternalWalletDetails`, `useCardDetails`,
`useGetLatestAllowanceForPriorityToken`, `useGetPriorityCardToken`,
`useGetUserKYCStatus`, `useUserRegistrationStatus`,
`useRegistrationSettings`, `useRegisterUserConsent`.
- **New query key factory** (`queries/keys.ts`): Centralized `cardKeys`
object for consistent cache key management and targeted invalidation.
- **Simplified `useLoadCardData`**: Orchestrates `useQuery` hooks;
`fetchAllData` uses `queryClient.refetchQueries` instead of imperative
fetch functions. Static config (`delegationSettings`) is excluded from
pull-to-refresh to avoid unnecessary round-trips.
- **Mutation hooks updated**: `useSpendingLimit`,
`useUpdateTokenPriority`, `DaimoPayModal` now call
`queryClient.invalidateQueries` instead of Redux `clearCacheData`
dispatches.
- **Auth error cleanup**: `CardHome` now calls
`queryClient.removeQueries` instead of `dispatch(clearAllCache())`.
- **Removed dead code**: Deleted `useWrapWithCache` hook and its
598-line test file. Removed `cache`, `priorityTokensByAddress`,
`lastFetchedByAddress`, `authenticatedPriorityToken`,
`authenticatedPriorityTokenLastFetched` fields and related
actions/selectors from the Redux `card` slice.
- **Redux migration 123**: Purges the removed fields from persisted
state for existing users.
- **All tests updated**: Rewrote/adapted tests across 17 test files
(512+ tests) to work with React Query mocks instead of Redux cache
mocks.

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Fixes:

## **Manual testing steps**

```gherkin
Feature: Card data fetching via React Query

  Scenario: Card Home loads data on mount
    Given the user is authenticated with a valid session
    When the user navigates to the Card Home screen
    Then card details, delegation settings, wallet details, and KYC status load without errors
    And no data is persisted in the Redux card slice cache

  Scenario: Pull-to-refresh re-fetches mutable data
    Given the user is on the Card Home screen with loaded data
    When the user pulls to refresh
    Then wallet details, card details, and KYC status are refetched
    And delegation settings are NOT refetched (static config with long stale time)

  Scenario: Cache invalidation after spending limit change
    Given the user has updated their spending limit
    When the mutation completes successfully
    Then the external wallet details query is invalidated
    And fresh data is fetched on next access

  Scenario: Auth error clears React Query cache
    Given the user encounters an authentication error
    When the error handler runs
    Then all Card React Query caches are removed
    And the user is redirected to the authentication screen

  Scenario: Existing users migrate cleanly
    Given the user has an existing persisted Redux state with card cache data
    When the app launches and migration 123 runs
    Then the cache, priorityTokensByAddress, lastFetchedByAddress, authenticatedPriorityToken, and authenticatedPriorityTokenLastFetched fields are removed from the card slice
```

## **Screenshots/Recordings**

<!-- No UI changes — this is a data-fetching layer refactor -->

### **Before**

<!-- N/A — internal refactor, no visual changes -->

### **After**

<!-- N/A — internal refactor, no visual changes -->

## **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.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it refactors core Card data-fetching, cache
invalidation, and auth-error cleanup paths; regressions could cause
stale/blank Card dashboard data or missed refreshes.
> 
> **Overview**
> **Card dashboard data fetching is migrated to React Query.** Hooks
like `useCardDetails`, `useGetDelegationSettings`,
`useGetCardExternalWalletDetails`, and
`useGetLatestAllowanceForPriorityToken` switch from
`useWrapWithCache`/Redux-driven caching to `useQuery` with centralized
`cardQueries` keys, manual `refetch`-based fetch functions, and updated
stale-time/enablement behavior.
> 
> **Cache invalidation/cleanup behavior changes.** `CardHome` now clears
Card caches via `queryClient.removeQueries` on auth errors and triggers
`fetchAllData` on focus when Card queries are invalidated;
`DaimoPayModal` invalidates `cardDetails` via
`queryClient.invalidateQueries` instead of dispatching Redux
cache-clearing actions.
> 
> **Tests and minor UI behavior are updated for the new model.** Card
tests mock/query React Query clients instead of Redux cache actions, and
the onboarding `SignUp` country selector shows a loading state while
registration settings are fetching and disables selection during that
time.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c8c7a98. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: metamaskbot <metamaskbot@users.noreply.github.com>
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

Refactors Rewards headers to use the shared header components
(`HeaderRoot`, `HeaderCompactStandard`) and SafeAreaView for consistency
with the rest of the app (e.g. Settings).

**Changes:**

- **Rewards Dashboard**: Replaced stack header with inline `HeaderRoot`
and `SafeAreaView`; removed `setOptions` / `getNavigationOptionsTitle`.
Header includes settings and conditional referral icon buttons.
- **Rewards Settings**: Replaced stack header with inline
`HeaderCompactStandard` and `SafeAreaView`; navigator uses `headerShown:
false` for the settings screen. Back button uses `onBack`.
- **Account group modal** (`RewardOptInAccountGroupModal`): Replaced
`BottomSheetHeader` with `HeaderCompactStandard` and added a close (X)
button to dismiss the sheet.
- **Environment toggle sheet** (`RewardsEnvironmentToggle`): Replaced
the title `View`/`Text` with `HeaderCompactStandard` and added a close
(X) button to dismiss the bottom sheet.

Tests updated for SafeAreaView/insets mocks and header behavior; removed
tests that asserted `setOptions` usage.

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: null

## **Related issues**

Fixes: https://consensyssoftware.atlassian.net/browse/DSYS-504

## **Manual testing steps**

```gherkin
Feature: Rewards headers

  Scenario: user sees Rewards dashboard with inline header
    Given the user is on the Rewards tab
    Then the dashboard shows the inline HeaderRoot with title and right-side icon(s)
    And safe area insets are respected

  Scenario: user opens Rewards settings and uses back
    Given the user is on the Rewards tab
    When the user taps the settings icon in the header
    Then Rewards settings opens with inline HeaderCompactStandard and back button
    When the user taps the back button
    Then the user returns to the Rewards dashboard

  Scenario: user opens account group modal and closes via X
    Given the user is on Rewards settings with at least one account group
    When the user taps an account group (e.g. "Account 1")
    Then the account group bottom sheet opens with HeaderCompactStandard and close (X) button
    When the user taps the close button
    Then the bottom sheet dismisses

  Scenario: user opens environment selector and closes via X
    Given the user is on Rewards settings and env switching is allowed
    When the user taps the environment selector button
    Then the environment bottom sheet opens with HeaderCompactStandard and close (X) button
    When the user taps the close button
    Then the bottom sheet dismisses
```

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**


https://github.com/user-attachments/assets/f54afe11-dcf4-4e37-babd-24906789c32b

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk UI refactor that changes header rendering and stack
`headerShown` behavior for Rewards screens; main risk is regressions in
navigation/back behavior or safe-area/layout on different devices.
> 
> **Overview**
> **Rewards screens now render consistent in-screen headers** by moving
away from stack `setOptions`/`getNavigationOptionsTitle` and using
shared header components.
> 
> The Rewards dashboard wraps content in `SafeAreaView` and uses
`HeaderRoot` with settings + conditional referral icon actions; Rewards
settings uses `SafeAreaView` + `HeaderCompactStandard` with an explicit
back handler, and the navigator hides the stack header for settings
(`headerShown: false`).
> 
> Bottom sheets used in Rewards settings are updated to use
`HeaderCompactStandard` (including close buttons) for the account-group
opt-in modal and the environment selector, and tests are adjusted
accordingly (safe-area mocks, header assertions, and more robust
disabled-state checks).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d3cc3e2. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
## Summary
- Migrate `useUnrealizedPnL` from manual `useState`/`useEffect` to React
Query's `useQuery`, completing React Query adoption across the Predict
module
- Extract query key factory and `queryOptions()` into
`queries/unrealizedPnL.ts` following the established pattern
(`balance.ts`, `positions.ts`)
- Register the new query in `predictQueries` index
- Update consumer (`PredictPositionsHeader`) to use the simplified
`loadUnrealizedPnL()` signature (no more `{ isRefresh }` arg — React
Query handles this)

## Test plan
- [ ] `npx jest useUnrealizedPnL` — all tests pass
- [ ] `npx jest PredictPositionsHeader` — consumer tests still pass
- [ ] Verify unrealized P&L displays correctly on the Positions screen
- [ ] Verify pull-to-refresh updates P&L data

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Moderate risk because it changes how unrealized P&L is fetched/cached
(React Query) and when the UI shows/refreshes it (focus +
position-gated), which could affect staleness and error/display states.
> 
> **Overview**
> Migrates `useUnrealizedPnL` from manual `useState`/`useEffect`
fetching to a React Query `useQuery` implementation backed by a new
`predictQueries.unrealizedPnL` entry and `queries/unrealizedPnL.ts`
(keys + `queryOptions`).
> 
> Updates `PredictPositionsHeader` to consume the new hook shape
(`data`/`error` as `Error`), **only render P&L when there are active
(non-claimable) positions**, and refresh by invalidating the unrealized
P&L query on screen focus and via the imperative `refresh()` handler.
> 
> Ensures unrealized P&L cache is refreshed after relevant actions by
invalidating `predictQueries.unrealizedPnL` on order placement and on
confirmed deposit/claim/withdraw toast events; tests are adjusted
accordingly.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
3100866. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

Introduces the `CardController` shell into the Engine as the first step
toward the multi-provider Card architecture.

**Why**: The Card feature is tightly coupled to Baanx with business
logic scattered across hooks, views, and a 2,600-line SDK monolith. To
support multiple card providers and clean up the architecture, we need a
proper Engine controller that owns the Card feature's persistent state.
This PR lays the foundation — an inert controller that holds state and
syncs it to Redux via the standard Engine batcher. No user-facing
changes.

**What changed**:

New files:
- **`app/core/Engine/controllers/card-controller/types.ts`**: Defines
`CardControllerState` (5 fields: `selectedCountry`, `activeProviderId`,
`isAuthenticated`, `cardholderAccounts`, `providerData`), default state
factory, and action/event/messenger types.
- **`app/core/Engine/controllers/card-controller/CardController.ts`**:
Bare-bones controller extending `BaseController`. All state is persisted
and marked `usedInUi: true`. No business logic yet — provider delegation
comes in subsequent PRs.
- **`app/core/Engine/controllers/card-controller/index.ts`**: Init
function following the standard Engine pattern — reads persisted state,
creates controller instance.
- **`app/core/Engine/messengers/card-controller-messenger/index.ts`**:
Simple messenger with no delegated actions/events (those will be added
when the controller needs to talk to `KeyringController`,
`TransactionController`, etc.).
- **`app/selectors/cardController.ts`**: Four selectors:
`selectCardSelectedCountry`, `selectCardActiveProviderId`,
`selectIsCardAuthenticated`, `selectCardholderAccounts`.

Modified files:
- **`app/core/Engine/types.ts`**: Added `CardController` to
`Controllers`, `EngineState`, `GlobalActions`, `GlobalEvents`, and
`ControllersToInitialize`.
- **`app/core/Engine/Engine.ts`**: Registered init function,
destructured from `controllersByName`, added to `this.context` and
`state` getter.
- **`app/core/Engine/messengers/index.ts`**: Added `CardController`
entry to `CONTROLLER_MESSENGERS`.
- **`app/core/Engine/constants.ts`**: Added
`'CardController:stateChange'` to `BACKGROUND_STATE_CHANGE_EVENT_NAMES`.
- **`app/util/test/initial-background-state.json`**: Added
`CardController` default state to the test fixture.
- **`.github/CODEOWNERS`**: Assigned
`app/core/Engine/controllers/card-controller`,
`app/core/Engine/messengers/card-controller-messenger`, and
`app/selectors/cardController.ts` to `@MetaMask/card`.

Test files:
- **`CardController.test.ts`**: Tests controller construction with
default state, partial state, and full persisted state.
- **`index.test.ts`**: Tests the init function returns a controller
instance, uses default state when none is persisted, and uses persisted
state when provided.
- **`cardController.test.ts`**: Tests all four selectors with populated
state, empty state, and undefined `CardController` state (fallback
values).

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Fixes:

## **Manual testing steps**

```gherkin
Feature: CardController initialization

  Scenario: Controller initializes with default state on fresh install
    Given the app is freshly installed
    When the Engine initializes
    Then state.engine.backgroundState.CardController exists
    And selectedCountry is null
    And activeProviderId is null
    And isAuthenticated is false
    And cardholderAccounts is an empty array
    And providerData is an empty object

  Scenario: Controller state persists across app restarts
    Given the app has been launched before
    And CardController state was persisted
    When the Engine initializes again
    Then the persisted CardController state is restored

  Scenario: No user-facing changes
    Given the user is on any screen in the app
    When the app loads
    Then nothing visually changes
    And the Card feature continues to work as before
```

## **Screenshots/Recordings**

No UI changes — this is an infrastructure-only PR.

### **Before**

No `CardController` in Engine. Card state managed entirely by Redux
slice.

### **After**

`CardController` exists in Engine with default state. Syncs to
`state.engine.backgroundState.CardController`. Existing Card feature
behavior is unchanged.

## **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.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it updates core Engine initialization/types and
introduces new persisted background state, which could affect startup or
state serialization despite having no business logic yet.
> 
> **Overview**
> Introduces a new `CardController` (BaseController) that persists Card
feature state (`selectedCountry`, `activeProviderId`, `isAuthenticated`,
`cardholderAccounts`, `providerData`) and exposes it via the standard
Engine background state batching.
> 
> Wires the controller into Engine initialization and messaging
(`Engine.ts`, `messengers/index.ts`, `types.ts`, `constants.ts`) so
`CardController` state is included in `Engine.state`, state-change
subscriptions, and debug/state log fixtures.
> 
> Adds initial selectors in `selectors/cardController.ts`, plus unit
tests for controller construction/init and selector fallbacks, and
updates `CODEOWNERS` for new Card Engine paths.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8363bb1. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@pull pull Bot locked and limited conversation to collaborators Mar 4, 2026
@pull pull Bot added the ⤵️ pull label Mar 4, 2026
@pull pull Bot merged commit 8bab3bf into Reality2byte:main Mar 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants