Skip to content

feat(ramps): refactors order state into the controller#26596

Merged
georgeweiler merged 28 commits intomainfrom
orders-refactor
Mar 2, 2026
Merged

feat(ramps): refactors order state into the controller#26596
georgeweiler merged 28 commits intomainfrom
orders-refactor

Conversation

@georgeweiler
Copy link
Copy Markdown
Contributor

@georgeweiler georgeweiler commented Feb 25, 2026

Description

Moves V2 ramp order state management from Redux (FiatOrder) into the RampsController, eliminating the conversion layer where clean RampsOrder API data was mangled into a legacy shape and immediately cast back.

This PR will allow for future PRs such as a headless ramps experience where callers can subscribe to order updates:
#26728

or implementing websocket connections for real-time order status updates:
MetaMask/core#8075

Please reference loom videos below for a demo of this PR.

What changed:

  • Order creation (Checkout + Transak native): addOrder(rampsOrder) stores directly in controller state. No more createInitialFiatOrder(), rampsOrderToFiatOrder(), or dispatch(addFiatOrder()). Transak deposit orders merge paymentDetails from the TransakDepositOrder onto the RampsOrder before storing, preserving inline bank transfer details.

  • Order detail screen: OrderDetails + OrderContent read RampsOrder from controller state via useRampsOrders hook. No Redux, no processFiatOrder, no useInterval polling. Pull-to-refresh calls refreshOrder(). Controller polling keeps data fresh automatically.

  • Bank details screen: V2BankDetails reads order lifecycle from controller state and fetches deposit-specific data (payment details) from TransakService. Cancel and confirm use providerOrderId directly.

  • Orders list: OrdersList merges legacy FiatOrder[] from Redux with V2 RampsOrder[] from controller state via DisplayOrder projection. Legacy orders route to the aggregator detail screen; V2 orders route to the new controller-native detail screen.

  • Side effects: ramps-controller-init.ts subscribes to RampsController:orderStatusChanged for notification and analytics handlers, and starts controller order polling on init. Follows the same pattern as TransactionController:transactionConfirmed handlers.

  • Messenger: RampsControllerInitMessenger delegates the orderStatusChanged event so init-level subscriptions work.

What stays untouched: Legacy AGGREGATOR, DEPOSIT, and older orders remain in Redux. Their processors, detail screens, types, and polling are unchanged.

Changelog

CHANGELOG entry: null

Related issues

Core PR that added orders MetaMask/core#8045

Fixes:

Manual testing steps

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

Image

Loom video of the controller state being polled, created and updated for Aggregator order (Moonpay)
https://www.loom.com/share/70658e66e55c444cadc02aad407c3da2

Loom video of the controller state being polled, created and updated for Native order (Transak)
https://www.loom.com/share/2c6d5941cbfa48c4be7cc36000e40e89

Pre-merge author checklist

Pre-merge reviewer checklist

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

Note

Medium Risk
Updates unified ramp checkout/order list navigation and switches V2 order creation/storage to controller state, which could impact order visibility and detail routing for buy/deposit flows. Risk is mitigated by updated unit tests, but regressions could affect transaction history UX.

Overview
V2 ramp orders are now stored and read from RampsController instead of being converted into legacy FiatOrder Redux state. Checkout uses useRampsOrders (getOrderFromCallback, addOrder) and always resets into Routes.RAMP.RAMPS_ORDER_DETAILS with showCloseButton, even when order IDs are null.

Orders list UI and navigation were reworked to merge legacy Redux orders with controller V2 orders. OrdersList now projects both into a unified DisplayOrder list (mergeDisplayOrders), renders new list rows, and routes presses by source/provider (legacy aggregator -> OrderDetails, V2 -> RampsOrderDetails, deposit -> new DepositOrderDetails, created deposit -> deposit flow).

Adds Routes.DEPOSIT.ORDER_DETAILS to the main navigator and updates provider selection to consider completed controller orders when computing previously-used providers. Tests/snapshots were updated accordingly, and V2 toast wiring now passes status (typed as RampsOrderStatus) instead of legacy state.

Written by Cursor Bugbot for commit f7c7f73. This will update automatically on new commits. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbot metamaskbot added the team-money-movement issues related to Money Movement features label Feb 25, 2026
@socket-security
Copy link
Copy Markdown

socket-security bot commented Feb 27, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​metamask/​ramps-controller@​10.0.0-preview-225638478100100100100100

View full report

@georgeweiler georgeweiler marked this pull request as ready for review February 27, 2026 05:32
@georgeweiler georgeweiler requested a review from a team as a code owner February 27, 2026 05:32
github-merge-queue bot pushed a commit to MetaMask/core that referenced this pull request Feb 27, 2026
## Explanation

<!--
Thanks for your contribution! Take a moment to answer these questions so
that reviewers have the information they need to properly understand
your changes:

* What is the current state of things and why does it need to change?
* What is the solution your changes offer and how does it work?
* Are there any changes whose purpose might not obvious to those
unfamiliar with the domain?
* If your primary goal was to update one package but you found you had
to update another one along the way, why did you do so?
* If you had to upgrade a dependency, why did you do so?
-->

The ramps controller currently has no concept of orders -- it can fetch
them from the V2 API via `getOrder()` and `getOrderFromCallback()`, but
doesn't store, track, or poll them. The mobile app stores V2 orders in
Redux as `FiatOrder` objects (the legacy aggregator type), which
requires a wasteful round-trip: the V2 API returns a clean `RampsOrder`,
`rampsOrderToFiatOrder()` converts it to `FiatOrder` for Redux, then
every consumer casts `order.data as RampsOrder` to get the original data
back.

This PR makes the controller the authority for V2 orders:

**State**: Adds `orders: RampsOrder[]` to `RampsControllerState` with
`persist: true`. Orders are stored as the native V2 type -- no wrapper,
no conversion.

**Order management**: Adds `addOrder(order)` and
`removeOrder(providerOrderId)` methods. `getOrder()` now also updates
the order in state if it already exists there, keeping the persisted
data fresh on every fetch.

**Polling**: Adds `startOrderPolling()` / `stopOrderPolling()`. The
controller polls pending orders (status `PENDING`, `CREATED`, `UNKNOWN`,
`PRECREATED`) at a 30-second interval with exponential backoff on errors
and respect for `pollingSecondsMinimum` from the API response. Polling
metadata (`lastTimeFetched`, `errorCount`) is tracked in a private map,
keeping `RampsOrder` as a clean API response type. `destroy()` stops
polling automatically.

**Domain event**: Adds `RampsController:orderStatusChanged` event,
published when a polled order's status transitions. Payload is `{ order:
RampsOrder, previousStatus: RampsOrderStatus }`. Mobile subscribes to
this at init time for notifications and analytics -- same pattern as
`TransactionController:transactionConfirmed`.

**Type addition**: Adds `OrderPaymentDetail` type to `RampsOrder` for
bank transfer instruction fields (account number, IBAN, routing number,
etc.) that providers like Transak attach to manual payment orders. This
is a provider-agnostic type with the same shape as
`TransakOrderPaymentMethod` but defined in `RampsService` to avoid
cross-service coupling.


## References

<!--
Are there any issues that this pull request is tied to?
Are there other links that reviewers should consult to understand these
changes better?
Are there client or consumer pull requests to adopt any breaking
changes?

For example:

* Fixes #12345
* Related to #67890
-->

MetaMask/metamask-mobile#26596

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [ ] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds new persisted `orders` state plus background polling/timer logic
and a new event, which could affect performance and order-update
correctness if polling/backoff or merge semantics are wrong.
> 
> **Overview**
> **Adds first-class V2 order tracking to `RampsController`.** The
controller state now persists `orders: RampsOrder[]`, and `getOrder()`
will merge API responses into any matching in-state order to keep cached
fields (e.g. `paymentDetails`).
> 
> **Introduces order CRUD + polling + status eventing.** New
`addOrder`/`removeOrder` APIs manage orders by `providerOrderId`,
`startOrderPolling`/`stopOrderPolling` periodically refresh *pending*
orders with backoff and `pollingSecondsMinimum`, and a new
`RampsController:orderStatusChanged` event is published on status
transitions.
> 
> **Extends order typing and tests.** `RampsOrder` gains optional
`paymentDetails: OrderPaymentDetail[]`, exports are updated, changelog
updated, and the test suite adds extensive coverage for merging,
polling, backoff, terminal statuses, and event publication.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8469a69. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
return [statusColor, statusText];
}

function DisplayOrderListItem({ item }: { item: DisplayOrder }) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👆 V2 and Legacy orders in harmony

);

const renderItem = ({ item }: { item: FiatOrder }) => (
const handleItemPress = useCallback(
Copy link
Copy Markdown
Contributor Author

@georgeweiler georgeweiler Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

V2 orders go to new page. legacy orders go to old page. This can/should be fixed by migrating users from legacy to V2 orders and dropping the concept of legacy orders in the future

Image

@georgeweiler
Copy link
Copy Markdown
Contributor Author

@cursoragent review this PR

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 1, 2026

Taking a look!

Open in Web Open in Cursor 

@georgeweiler
Copy link
Copy Markdown
Contributor Author

@cursoragent fix this test app/core/Engine/Engine.test.ts

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 1, 2026

Fixed the failing Engine.test.ts test! The issue was that the PR adds order state management to the RampsController, which now includes an orders array in its state. The test "matches initial state fixture" was failing because the initial background state fixture didn't include this new field.

What I did:

  • Added "orders": [] to the RampsController state in /workspace/app/util/test/initial-background-state.json
  • Committed the change with a descriptive message
  • Pushed to the remote branch

The test should now pass since the initial state fixture matches the actual controller state.

Open in Web Open in Cursor 

The RampsController now manages V2 order state via an orders array.
Update the initial background state fixture to include this field
so the 'matches initial state fixture' test passes.

Co-authored-by: George Weiler <georgejweiler@gmail.com>
@georgeweiler georgeweiler changed the title feat: refactor order state into the controller feat(ramps): refactors order state into the controller Mar 2, 2026
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 2, 2026

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeAccounts, SmokeConfirmations, SmokeIdentity, SmokeNetworkAbstractions, SmokeNetworkExpansion, SmokeTrade, SmokeWalletPlatform, SmokeCard, SmokePerps, SmokeRamps, SmokeMultiChainAPI, SmokePredictions, FlaskBuildTests
  • Selected Performance tags: @PerformanceAccountList, @PerformanceOnboarding, @PerformanceLogin, @PerformanceSwaps, @PerformanceLaunch, @PerformanceAssetLoading, @PerformancePredict, @PerformancePreps
  • Risk Level: high
  • AI Confidence: 100%
click to see 🤖 AI reasoning details

E2E Test Selection:
Hard rule (controller-version-update): @MetaMask controller package version updated in package.json: @metamask/assets-controllers@npm:^99.4.0, @metamask/ramps-controller. Running all tests.

Performance Test Selection:
Hard rule (controller-version-update): @MetaMask controller package version updated in package.json: @metamask/assets-controllers@npm:^99.4.0, @metamask/ramps-controller. Running all tests.

View GitHub Actions results

@georgeweiler georgeweiler enabled auto-merge March 2, 2026 17:13
@georgeweiler georgeweiler added this pull request to the merge queue Mar 2, 2026
Merged via the queue into main with commit 5b9331e Mar 2, 2026
125 of 126 checks passed
@georgeweiler georgeweiler deleted the orders-refactor branch March 2, 2026 17:36
@github-actions github-actions bot locked and limited conversation to collaborators Mar 2, 2026
@metamaskbot metamaskbot added the release-7.69.0 Issue or pull request that will be included in release 7.69.0 label Mar 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.69.0 Issue or pull request that will be included in release 7.69.0 size-XL team-money-movement issues related to Money Movement features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants