Skip to content

Keep MoonPay and Paybis sell on their order after Back - #6256

Open
j0ntz wants to merge 7 commits into
developfrom
jon/moonpay-sell-back-v2
Open

j0ntz wants to merge 7 commits into
developfrom
jon/moonpay-sell-back-v2

Conversation

@j0ntz

@j0ntz j0ntz commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Description

Second pass on the MoonPay sell Back fix after QA failed EdgeApp/edge-react-gui#6235 on iOS Staging 26101201. Tapping Back on the prefilled send scene still flashed the "Only the Edge application can open this link" QR page, dropped to the sell scene, then jumped forward into the MoonPay webview by itself. The webview it jumped to was a fresh sale on MoonPay's "Review before you sell" page with a new quote, and continuing from it left another sell order waiting for a deposit.

Root cause:

  • The earlier fix popped the webview on Back and opened the MoonPay widget again with a freshly signed URL. MoonPay treats each widget open as a new sale. Each Back therefore put the user on the review page of a new sale, and the order they had come from was left behind unpaid.
  • The QR page was the webview itself. MoonPay sends the webview to the Edge payment redirect URL, the plugin opens the send scene on top, and the webview underneath finishes loading that URL's web page. Back revealed it until the pop ran.

This PR:

  • FiatPluginWebView takes an optional onShouldStartLoad callback, wired to the WebView's onShouldStartLoadWithRequest, which lets a plugin refuse a load before it starts. Only main-frame loads reach the callback: a frame inside the page cannot trigger it. The react-native-webview patch now reports isTopFrame on Android from WebResourceRequest.isForMainFrame() (stock 13.15.0 leaves the field out of the blocking callback), and a load with no isTopFrame counts as a frame. No other provider passes it.
  • A shared helper, makeSellPaymentRedirect (src/plugins/ramps/utils/sellPaymentRedirect.ts), owns the refusal, the Android fallback and the first-report guard. MoonPay and Paybis both call it.
  • The MoonPay sell refuses its payment redirect and opens the send scene from that callback. The webview stays on MoonPay's page for the order. Back from the send scene returns to that same order, and "Send with Edge" on it opens the send scene again.
  • The widget is never opened a second time. After a Back with nothing broadcast, the payment guard is cleared and the webview is left alone.
  • Android gives the callback 250 ms and loads the page anyway when the answer is late. onUrlChange still handles the redirect in that case and records that the webview is on the redirect page, and Back then closes the webview so the user lands on the payment-method list.
  • The completed-send path is unchanged: the webview closes, Sell_Success is logged, the transaction details are saved and the completion modal shows.

Testing: iOS simulator on the agent test account, with a real MoonPay credit/debit sell order of 0.00034 BTC and no local code changes. "Send with Edge" opened the prefilled send scene. Back returned to the same MoonPay order page: a 10 fps screen recording of the tap shows the send scene sliding away to the order page with no QR page, and the app was still on that page 12 seconds later with no new order. "Send with Edge" opened the send scene again for the same order, one confirm slide sent the deposit, and the app showed "Sell Order Complete" over the payment-method list with the sell saved in the wallet's transaction list. Back from the widget before an order exists lands on the payment-method list. After each of the two review fixes the Back path was run again on the simulator with a new unpaid order and no send: "Send with Edge" opened the send scene, Back returned to the same order page, and Back again left the widget for the payment-method list.

Android: API 36 emulator, debug build, agent test account. MoonPay asks for a sign-in on Android. A local, never-committed injected script therefore sent the MoonPay page to the payment redirect URL for the account's own BTC address; no send was made. In 2 of 3 opens the redirect was refused, the prefilled send scene opened, and Back returned to the same MoonPay page, still there 15 seconds later. The first open after a JS reload answered past the 250 ms limit: the redirect page loaded under the send scene, and Back closed it to the payment-method list with no new MoonPay open. Before the patch reported isTopFrame, the same frame check skipped every Android load and the redirect page always loaded first.

Frame check (Android, cells 37 to 40): the same injected script also pointed frames inside the MoonPay page at the payment redirect URL, by iframe src, by script navigation and by a tap on a link inside a frame. None of them reached onShouldStartLoadWithRequest on this emulator's WebView 133; the tapped frame showed the web fallback page inside the frame and no send scene opened. A main-frame navigation to the same URL then arrived with isTopFrame true, was refused, and opened the send scene, and Back returned to the MoonPay page. No send was confirmed.

Paybis sell

Paybis sell had the same shape: Back on the prefilled send scene popped the webview and opened the Paybis widget again, and the payment return URL page loaded under the send scene. On the iOS simulator before this change, Back flashed the QR page, then the widget opened again on Paybis's "Waiting for your payment" page for the same order. Paybis re-opens with the same request: no new order was created.

This PR applies the MoonPay approach to Paybis:

  • onShouldStartLoad refuses the payment return URL and opens the send scene from that callback. The webview stays on Paybis's page for the order.
  • After a Back with nothing sent, the payment guard is cleared and the webview is left alone. The widget is not opened again.
  • onUrlChange is still the Android fallback for a late answer, and only its first report of the return URL opens the send scene. When that page did load, Back closes it.
  • The completed-send path keeps develop's order: Sell_Success is logged, the transaction details are saved, then the webview closes and opens again on the order's status without the one-time token.

Open product question: after Back, Paybis's own Pay, Cancel and in-page back buttons stay disabled behind a spinner, because the page is still waiting on the navigation that was refused. The user leaves with Edge's Back, which returns to the payment-method list. The alternative is to open the widget again on the same requestId on Back, which brings the order back with Pay enabled and creates no new order, at the cost of the QR flash and the reload. This PR leaves the webview alone; the alternative is a small change in the Paybis catch.

Testing (Paybis): iOS simulator on the agent2 test account with a real Paybis card sell order and no local code changes; no send was confirmed. Pay opened the prefilled send scene with no QR page. Back returned to the same Paybis "Transaction details" page with no new widget open, and Back again left the widget for the payment-method list. On the Android emulator, Paybis's human check fails ("Verification failed") and blocks Continue. A local, never-committed change therefore stubbed the payment details with the account's own BTC address and sent the Paybis page to the payment return URL. The return URL was refused (no onUrlChange report of it), the send scene opened, Back returned to the same Paybis page, and Back again left the widget for the payment-method list.

Unit tests: sellPaymentRedirect.test.ts (6 tests) covers the helper, and with it MoonPay's redirect logic. A sell suite in paybisRampPlugin.test.ts (4 tests) captures the webview and send-scene params and calls their callbacks: a refused redirect, a redirect that loads anyway, a refusal that comes after the URL report, and a completed send. Dropping the first-report guard, returning true from onShouldStartLoad, or putting develop's pop and re-open back in the catch each fails at least one of them.

Re-run on the iOS simulator after the review fixes, with no send confirmed. MoonPay (cells 28 to 30): "Send with Edge" opened the send scene, Back returned to the same order page, and Back again left the widget. Paybis (cells 31 to 34): Pay opened the send scene, Back returned to the same Transaction details page with the Pay button still busy, and Back again left the widget.

The CHANGELOG lines are merged into the 4.52.0 (staging) entry from EdgeApp/edge-react-gui#6235, since neither has shipped.

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

Test evidence

80458b4
2026-10-13

2. moonpay order waiting

3. send scene prefilled

4. back frames no qr flash

5. back landed same order 12s later

6. send scene reopened same order

7. sell order complete

8. saved sell tx details

9. back from widget payment list

11. topframe back same order
f0205e2
2026-10-15
🪓 Local, never-committed stub of the Paybis payment details plus an injected script that sends the Paybis page to the payment return URL (Paybis's human check fails on the Android emulator)

12. 🪓 android send scene from redirect

13. 🪓 android back closes redirect page

14. 🪓 android send scene redirect refused

15. 🪓 android back same moonpay page

16. paybis before send scene

17. paybis before back reveals redirect page

18. paybis before redirect qr page

19. paybis before widget reopens

20. paybis before reopened waiting

21. paybis after pay spinner

22. paybis after send scene

23. paybis after back same paybis page

24. paybis after back again leaves widget

25. 🪓 paybis android send scene

26. 🪓 paybis android back same page

27. 🪓 paybis android back again leaves
d2df60f
fixup! Keep Paybis sell on its page after Back
2026-10-17
🪓 Local, never-committed injected script points the MoonPay sell page and frames inside it at the payment return URL (MoonPay asks for sign-in on Android)

28. moonpay send scene

29. moonpay back same order

30. moonpay back again leaves widget

31. paybis pay opens send scene wallet empty

32. paybis send scene

33. paybis back same transaction page

34. paybis back again leaves widget

37. 🪓 android moonpay frame link to payment url

38. 🪓 android frame tap loads in frame no send scene

39. 🪓 android page redirect opens send scene

40. 🪓 android back returns to moonpay page

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-13T21:00:58.348073Z 7665890 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread src/plugins/ramps/moonpay/moonpayRampPlugin.ts
@j0ntz
j0ntz force-pushed the jon/moonpay-sell-back-v2 branch from 7665890 to e1bc3f4 Compare October 13, 2026 21:12
Comment thread src/plugins/ramps/moonpay/moonpayRampPlugin.ts Outdated
@j0ntz
j0ntz force-pushed the jon/moonpay-sell-back-v2 branch from e1bc3f4 to 80458b4 Compare October 13, 2026 21:29
Comment thread src/plugins/gui/scenes/FiatPluginWebView.tsx
Comment thread CHANGELOG.md Outdated
@j0ntz j0ntz changed the title Keep MoonPay sell on its order after Back Keep MoonPay and Paybis sell on their order after Back Oct 15, 2026

@peachbits peachbits left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Round 2 at f0205e2.

Both round-1 Warnings are fixed:

  • FiatPluginWebView refuses only on event.isTopFrame === false. The local AnyPlatformLoadRequest type is needed: with the library's required boolean, no-unnecessary-boolean-literal-compare flags === false and its fix turns it back into !event.isTopFrame.
  • The MoonPay entry is merged into EdgeApp/edge-react-gui#6235's line in ## 4.52.0 (staging).

tsc, eslint on the changed files, and the full jest suite pass locally.

The new Paybis commit handles each path I traced. A refused redirect opens the send scene once, and Back leaves the WebView alone. A redirect that loads anyway on Android opens it once, Back closes the redirect page, and later URL reports are ignored. A completed send closes the widget and opens it again on the same request without the one-time token. There are two Warnings inline: one on where the Paybis change ships, and one on tests.

A UX observation for the task's open question: after Back, Paybis's Pay and Cancel stay disabled, so a user who backed out by mistake can only leave and start a new order. Develop's re-open loaded the same requestId again, which should bring the order back with Pay enabled. Unlike MoonPay, re-opening Paybis does not create a sale, so re-opening on Back may be the better trade for Paybis. That is a product call, and another reason to land Paybis on its own.

Comment thread CHANGELOG.md
Comment thread src/plugins/ramps/paybis/paybisRampPlugin.ts Outdated
j0ntz added 2 commits October 16, 2026 15:46
Backing out of the send scene popped the webview and opened the
MoonPay widget again. MoonPay treats each open as a new sale, so
every Back left another sell order waiting for a deposit. Until the
pop ran, the webview showed the page behind the payment redirect,
which is the QR fallback for links opened outside the app.

The ramp webview can now refuse a load before it starts. The MoonPay
sell refuses its payment redirect, so the webview stays on MoonPay's
page for the order while the send scene is open, and Back returns
there. The widget is never opened a second time. If Android loads
the redirect page before the refusal arrives, Back closes the
webview instead.
Move the sell redirect handling into makeSellPaymentRedirect, a helper both MoonPay and Paybis call, and unit-test it. The helper owns the refusal, the onUrlChange fallback, the first-report guard and closing only a loaded redirect page on Back. Answers the review request for tests.

Fixup-for: human
@j0ntz
j0ntz force-pushed the jon/moonpay-sell-back-v2 branch from f0205e2 to 8426a1a Compare October 16, 2026 22:46
@j0ntz

j0ntz commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Both inline Warnings are addressed: Paybis stays in this PR under ## 4.52.0 (staging), and the redirect handling now has tests (see the thread on paybisRampPlugin.ts).

Paybis behavior after Back is unchanged in this round. Re-driven on the iOS sim, Back from the send scene returns to the same Transaction details page with the Pay button still busy, as you saw. Whether Paybis should re-open the same requestId on Back instead is the product call you named; it is a small change in the Paybis catch if that is the direction.

@j0ntz j0ntz left a comment

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.

Self-review (medium) of 5b5fa520b..6c6c0261e: 7 findings, 1 fixed, 5 rejected, 1 a duplicate of the fixed one.

Fixed before review (1)
  • src/plugins/ramps/paybis/paybisRampPlugin.ts:1401: after a sent transaction the success path popped and re-opened the widget before Sell_Success and saveTxAction. A throw while re-opening skipped both for a sale that already went out. Develop logged and saved first. Fixed in fixup! Keep Paybis sell on its page after Back (the order is now log, save, then re-open).
Rejected in curation (5)
  • src/plugins/ramps/utils/sellPaymentRedirect.ts:58: closeRedirectPage pops the whole widget scene on Android instead of returning to the order page. Rejected: it pops only after the WebView itself loaded the fallback page, which replaces the order page in the same WebView: no order page is left to return to. This is the "Back pops only the WebView" case from round 2. After a refused redirect it pops nothing, and on the iOS sim Back returns to the same order page for both providers.
  • src/plugins/ramps/utils/sellPaymentRedirect.ts:45: onShouldStartLoad does not set the redirect flag. A late URL report after Back then opens the send scene again. Rejected: setting the flag on a refusal would make Back pop the order page on iOS, and tapping Pay again after Back must open the send scene again. The URL report follows the load within milliseconds, while leaving the send scene takes a user tap, and inPayment covers that window. The "refusal comes too late" test covers the realistic ordering.
  • src/plugins/ramps/paybis/paybisRampPlugin.ts:1442: a failed or cancelled send no longer re-opens the widget. Rejected: removing that re-open is the task. It flashed the QR page, and for MoonPay it created a new order. The Paybis disabled-buttons trade-off is the open product call from round 2.
  • src/plugins/ramps/paybis/paybisRampPlugin.ts:1400: the success path pops and re-opens differently on iOS and Android. Rejected: in both cases the stack holds one widget WebView (the Android fallback page loads inside it). The pop closes the same scene either way. Develop popped and re-opened the same way.
  • src/plugins/ramps/moonpay/moonpayRampPlugin.ts:1013: urlObj in handlePaymentRedirect shadows the sell-quote urlObj. Rejected: the same shadowing exists in develop (the redirect handler's urlObj inside the sell quote's scope). The handler moved here unchanged.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 using default effort and found 1 potential issue.

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 8426a1a. Configure here.

Comment thread src/plugins/ramps/paybis/paybisRampPlugin.ts
@j0ntz
j0ntz force-pushed the jon/moonpay-sell-back-v2 branch from 8426a1a to 86aef37 Compare October 16, 2026 23:19
Comment thread src/plugins/gui/scenes/FiatPluginWebView.tsx Outdated
j0ntz added 4 commits October 16, 2026 16:38
Report isTopFrame on Android and refuse only main-frame loads

Answers the security review finding that a frame inside the MoonPay sell
page could open a locked send from its own URL. The react-native-webview
patch now passes WebResourceRequest.isForMainFrame() through as
isTopFrame, and FiatPluginWebView treats a missing value as a frame, so
losing the patch falls back to the main-frame-only URL report.

Fixup-for: auto
Backing out of the Paybis sell send scene closed the widget and loaded it
again, and the WebView showed the payment redirect page, a QR code meant
for outside the app, underneath the send scene.

The WebView now refuses the payment redirect and opens the send scene from
that callback, so it stays on Paybis's page for the order. Back with
nothing sent clears the payment guard and leaves the widget as it is. On
Android, where the refusal can come too late, the first URL report of the
redirect page opens the send scene instead, and Back closes that page.

A completed send still closes the widget and opens it again so it shows
the order's status.
Call makeSellPaymentRedirect from the Paybis sell flow, and add approveQuote tests that drive the widget and send scene params: a refused redirect opens send2 once and Back pops nothing; a redirect that loads anyway opens send2 once, Back pops only the WebView and a later report opens nothing; a completed send re-opens the widget without oneTimeToken. Answers the review request for tests.

Fixup-for: human
Log the Paybis sale and save its tx action before opening the widget again, as develop does. A throw while re-opening the widget skipped Sell_Success and saveTxAction for a sent transaction (self-review finding).

Open nothing when the user leaves the Paybis widget while the payment-details fetch is in flight. The WebView's onClose now marks the widget closed, and the redirect handler checks it after the fetch and in its catch, so a late fetch neither opens the send scene nor pops another scene. Answers the Bugbot finding on the Back race.

Fixup-for: auto
@j0ntz
j0ntz force-pushed the jon/moonpay-sell-back-v2 branch from 86aef37 to d2df60f Compare October 17, 2026 00:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants