Skip to content

feat: adds support for custom actions such as paypal#26949

Closed
georgeweiler wants to merge 22 commits intomainfrom
v2-custom-actions
Closed

feat: adds support for custom actions such as paypal#26949
georgeweiler wants to merge 22 commits intomainfrom
v2-custom-actions

Conversation

@georgeweiler
Copy link
Contributor

@georgeweiler georgeweiler commented Mar 3, 2026

Description

  1. Integrates new ramps-controller API for custom-action flows (e.g., PayPal) removes Redux-based custom ID tracking.

  2. Simplifies ramps-controller state hydration system into a single init call on app start up.

What changed:

  • BuildQuote — Uses getBuyWidgetData instead of getWidgetUrl. For isCustomAction quotes, calls addPrecreatedOrder before opening external browser (InAppBrowser or Linking.openURL) and passes orderId to Checkout for WebView flows.
  • Checkout — Uses addPrecreatedOrder instead of Redux addFiatCustomIdData. Supports both orderId and customOrderId for backwards compatibility.
  • HooksuseRampsOrders exposes addPrecreatedOrder and AddPrecreatedOrderParams; useRampsQuotes uses getBuyWidgetData returning Promise<BuyWidget | null>; useRampsController exposes both.
  • Provider/payment filteringPaymentSelectionModal and ProviderSelection filter out isCustomAction quotes where appropriate.

What stays untouched: Non-custom-action flows (standard WebView checkout) unchanged. Existing ramps navigation and token selection preserved.

Dependencies: Requires MetaMask/core#8100 for the new controller API.

Changelog

CHANGELOG entry: Fixes Paypal order tracking

Related issues

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

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

High Risk
Changes the on-ramp checkout flow to support external-browser/deeplink providers (e.g., PayPal) and introduces new order hydration/tracking paths, which can break purchase flows if edge cases aren’t handled. Additionally, android/app/build.gradle contains unresolved merge-conflict markers in versionName/versionCode, which is build-breaking.

Overview
Adds support for custom-action on-ramp providers (e.g., PayPal) by switching from getWidgetUrl to the new getBuyWidgetData API (url + orderId + browser) and using addPrecreatedOrder to track orders before redirecting out of the app.

Updates BuildQuote to override redirectUrl to a metamask:// deep link, choose between WebView checkout vs external browser (Linking/InAppBrowser.openAuth), and navigate directly to RAMPS_ORDER_DETAILS for external flows while passing orderId into Checkout for WebView flows. Checkout now registers precreated orders from orderId/legacy customOrderId and removes the prior Redux custom-id tracking.

Provider/payment selection UI now filters out isCustomAction quotes, order list merging hides Precreated/IdExpired V2 orders, and multiple modules normalize provider codes via normalizeProviderCode. Build/version numbers were bumped in CI/iOS configs, but Android build.gradle currently includes conflict markers that must be resolved.

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

@georgeweiler georgeweiler requested a review from a team as a code owner March 3, 2026 20:25
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

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-ramp issues related to Ramp features label Mar 3, 2026
@github-actions github-actions bot added the size-M label Mar 3, 2026
@metamaskbot metamaskbot requested a review from a team as a code owner March 4, 2026 23:14
…l external browser flow

The external browser flow (PayPal) was using an HTTPS fake-callback URL
for InAppBrowser.openAuth, which cannot redirect back to the app. This
replaces it with a metamask:// deep link matching the legacy aggregator
pattern. After the browser flow completes, the user is now navigated to
the Order Details screen via navigation.reset() so they can see the
pre-created order's polling status.

Made-with: Cursor
The buyURL baked into quotes at fetch-time uses the HTTPS fake-callback
as the redirectUrl. External browser providers (e.g. PayPal) need a
metamask:// deep link instead so they redirect back to the app after
payment. This replaces the redirectUrl on the buyURL before calling
getBuyWidgetData, matching the legacy SDK BuyAction.createWidget
behaviour.

Made-with: Cursor
When the user manually closes the PayPal browser without completing
payment, InAppBrowser.openAuth resolves with type 'cancel'. We now
check the result and return early, staying on the BuildQuote screen
instead of navigating to an empty Order Details screen. This matches
the legacy aggregator's useInAppBrowser behaviour.

Made-with: Cursor
The previous fix conditionally overrode redirectUrl based on
needsDeepLink, which checked isCustomAction and the inline
buyWidget.browser. However, the browser type is only known AFTER the
API fetch (chicken-and-egg problem). If neither isCustomAction nor
the inline buyWidget were set on the quote, the override was skipped,
and PayPal received the HTTPS fake-callback URL instead of the
metamask:// deep link.

Now we unconditionally set the deep link redirectUrl on the buyURL
before every getBuyWidgetData call. This is safe because in-app
browser providers ignore the redirectUrl parameter, and it matches
how the legacy SDK always passed the deep link to createWidget().

Made-with: Cursor
Logs buyURL override, buyWidget API response, and InAppBrowser
result to help diagnose the redirect issue in testing.
All log lines are marked with TODO for removal after verification.

Made-with: Cursor
@github-actions github-actions bot added the size-L label Mar 9, 2026
@lorenzosantos
Copy link

I have read the CLA Document and I hereby sign the CLA

@socket-security
Copy link

socket-security bot commented Mar 11, 2026

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

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​@​metamask/​ramps-controller@​10.2.0 ⏵ 12.0.09610078 +198 +1100

View full report

@github-actions github-actions bot added size-XL and removed size-L labels Mar 11, 2026
return match ?? null;
}
return null;
const [quote] = quotesResponse.success;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Simplifies extremely defensive quote context checking that is not needed. We only ever fetch 1 quote on BuildQuote. A simple check for provider matching is enough.

createEventBuilder,
]);

const handleContinuePress = useCallback(async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

handleContinuePress was managing 2 separate branches for Native, Aggregator and now would have to handle custom actions. I've split the logic into separate functions for readability, testability, etc..

if (isNativeProvider(selectedQuote)) {
  await handleNativeProviderContinue();
} else if (isCustomAction(selectedQuote)) {
  await handleCustomActionContinue();
} else {
  await handleAggregatorContinue();
}

);
const useExternalBrowser = buyWidget.browser === 'IN_APP_OS_BROWSER';

if (useExternalBrowser) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This shouldn't be necessary as all aggregator orders should be opening in the widget. However the mobile app has no way to know this, so we have to handle the case when API returns in-app browser for a non-custom-action.

@github-actions
Copy link
Contributor

🔍 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/ramps-controller. Running all tests.

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

View GitHub Actions results

selectedQuote !== null &&
quoteMatchesAmount &&
quoteMatchesCurrentContext;
hasAmount && !selectedQuoteLoading && selectedQuote !== null;
Copy link

Choose a reason for hiding this comment

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

Removed quote validation allows stale/mismatched quote submission

High Severity

The old code validated that the selected quote's amountIn matched the current amountAsNumber, the provider matched, and the payment method matched before enabling the continue button and before proceeding in handleContinuePress. All three guards were removed. Now canContinue only checks hasAmount && !selectedQuoteLoading && selectedQuote !== null, and handleContinuePress has no amount/payment-method validation. Because quote fetching is debounced (500ms), a user can change the amount or payment method and immediately tap Continue, submitting a stale quote for the wrong amount or payment method to the provider. The old quoteMatchesAmount and quoteMatchesCurrentContext guards explicitly prevented this race.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Contributor Author

@georgeweiler georgeweiler Mar 11, 2026

Choose a reason for hiding this comment

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

This is true, but it's an edgecase issue. I've moved several lines of defensive code because quote amounts are not contractual anyway and can be updated when the user gets to the provider widget.

}
return null;
const [quote] = quotesResponse.success;
return quote?.provider === selectedProvider.id ? quote : null;
Copy link

Choose a reason for hiding this comment

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

selectedQuote ignores payment method when multiple quotes returned

Medium Severity

The selectedQuote memo was simplified to const [quote] = quotesResponse.success; return quote?.provider === selectedProvider.id ? quote : null. The old logic handled multi-quote responses by finding a quote matching both the provider and selectedPaymentMethod.id. The new code blindly takes the first array element, ignoring selectedPaymentMethod even though it's still in the dependency array. If the API returns multiple quotes for different payment methods, the wrong quote (for a different payment method) can be selected and submitted.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not an issue. build quote only ever fetches 1 quote

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
46.6% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@github-actions
Copy link
Contributor

E2E Fixture Validation — Schema is up to date
11 value mismatches detected (expected — fixture represents an existing user).
View details

- Pass providerCode and walletAddress from BuildQuote to OrderDetails
  so it can fetch the order from the API when not yet in state
- Add hydration effect in OrderDetails that calls refreshOrder when
  order is missing but fallback params are available
- Show loading spinner and error states during hydration
- Remove all debug instrumentation (rampsDebugLog) from BuildQuote,
  OrderDetails, and Checkout

Made-with: Cursor
Copy link

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

=======
versionName "7.70.0"
versionCode 3607
>>>>>>> 1242530285d27aa5c0f2a7b7b53cc417231b3d58
Copy link

Choose a reason for hiding this comment

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

Unresolved Git Merge Conflict in Build File

High Severity

The file contains raw git merge conflict markers (<<<<<<< HEAD, =======, >>>>>>>) for the versionName and versionCode fields. This is a Gradle syntax error that will cause every Android build to fail immediately. The conflict between version 7.69.0/code 3892 (HEAD) and 7.70.0/code 3607 (the other branch) was never resolved before committing.

Fix in Cursor Fix in Web

@github-actions github-actions bot locked and limited conversation to collaborators Mar 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

size-XL team-ramp issues related to Ramp features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants