Skip to content

Commit bf4d1ea

Browse files
committed
Fix MoonPay "Send with Edge" sell link
MoonPay surfaces a "Send with Edge" button for pending sell orders (in their email and order-history page) that reflects back the deposit redirect URL our app hands them. That URL was RETURN_URL_PAYMENT = https://edge.app/redirect/payment/, but the apex edge.app host is NOT claimed as a universal link on iOS or Android (only deep.edge.app, dl, and return are), so the button dead-ended on a static redirect page and the user could not complete the deposit. The buy redirects already use deep.edge.app; only the sell payment redirect used the unclaimed apex. Point RETURN_URL_PAYMENT at the claimed deep.edge.app host (in-app webview interception is host-agnostic via startsWith, so it is unaffected) and parse the redirect/payment deep link into a new paymentRedirect link that opens the Send scene, resolving the wallet from the base currency code and pre-filling the deposit address, amount, and destination tag (uniqueIdentifier). Give the sibling ramp redirect URLs the same treatment: RETURN_URL_SUCCESS, RETURN_URL_FAIL, and RETURN_URL_CANCEL now also live on the claimed deep.edge.app host (was the apex edge.app), so an externally-reflected terminal redirect opens the app instead of dead-ending. The host swap is symmetric for the in-webview interception (Banxa matches by exact ===, Paybis/MoonPay by startsWith), so no ramp flow changes behavior. These terminal states carry no actionable payload, so the deep-link parser resolves edge://redirect/{success,fail,cancel} to a no-op rather than the former "Unknown deep link format" error, on the edge://, deep.edge.app, and legacy apex edge.app hosts.
1 parent 1811696 commit bf4d1ea

6 files changed

Lines changed: 215 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased (develop)
44

55
- added: Remote enable/disable of gift card providers via the info server's giftCardInfo config, supporting whole-provider disabling for Phaze and Bitrefill and per-brand disabling for Phaze.
6+
- fixed: MoonPay "Send with Edge" sell link now opens the app to a pre-filled Send scene. All ramp redirect URLs (payment, success, fail, cancel) point at the claimed deep.edge.app universal-link host (was the unclaimed edge.app apex), and the deep-link parser routes the payment redirect to Send with the deposit address, amount, and destination tag while the terminal redirects open the app via a no-op.
67

78
## 4.49.0 (staging)
89

src/__tests__/DeepLink.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,65 @@ describe('parseDeepLink', function () {
189189
})
190190
})
191191

192+
describe('paymentRedirect', () => {
193+
makeLinkTests({
194+
// Real-world MoonPay "Send with Edge" sell link (extra params ignored):
195+
'https://edge.app/redirect/payment/?transactionId=6ae325aa-d930-47cd-9ef0-d26e03b68f3c&baseCurrencyCode=btc&baseCurrencyAmount=0.00212&depositWalletAddress=bc1qqp44yqt9nzrca7cw4hrl2hu5nmpw5fg0r32z62&paymentMethod=moonpay_balance':
196+
{
197+
type: 'paymentRedirect',
198+
currencyCode: 'btc',
199+
depositAddress: 'bc1qqp44yqt9nzrca7cw4hrl2hu5nmpw5fg0r32z62',
200+
amount: '0.00212',
201+
addressTag: undefined
202+
},
203+
// XRP needs a destination tag, carried as depositWalletAddressTag:
204+
'edge://redirect/payment/?baseCurrencyCode=xrp&baseCurrencyAmount=10&depositWalletAddress=rEXAMPLExrpADDRESS&depositWalletAddressTag=123456':
205+
{
206+
type: 'paymentRedirect',
207+
currencyCode: 'xrp',
208+
depositAddress: 'rEXAMPLExrpADDRESS',
209+
amount: '10',
210+
addressTag: '123456'
211+
},
212+
// deep.edge.app is an already-claimed universal-link host:
213+
'https://deep.edge.app/redirect/payment/?baseCurrencyCode=ltc&baseCurrencyAmount=1.5&depositWalletAddress=ltc1qexample':
214+
{
215+
type: 'paymentRedirect',
216+
currencyCode: 'ltc',
217+
depositAddress: 'ltc1qexample',
218+
amount: '1.5',
219+
addressTag: undefined
220+
},
221+
// Missing the deposit address on the edge.app host falls back to a browser:
222+
'https://edge.app/redirect/payment/?baseCurrencyCode=btc': {
223+
type: 'other',
224+
protocol: 'https',
225+
uri: 'https://edge.app/redirect/payment/?baseCurrencyCode=btc'
226+
},
227+
// Missing params on the edge:// (deep.edge.app) path degrade to a no-op
228+
// instead of an "Unknown deep link format" error:
229+
'edge://redirect/payment/?baseCurrencyCode=btc': { type: 'noop' },
230+
'https://deep.edge.app/redirect/payment/': { type: 'noop' }
231+
})
232+
})
233+
234+
describe('redirect terminal states', () => {
235+
makeLinkTests({
236+
// The provider terminal redirects (success/fail/cancel) carry no
237+
// actionable payload, so an externally-tapped link opens the app via a
238+
// no-op on every host: edge://, the claimed deep.edge.app, and the legacy
239+
// apex edge.app (old orders may still point there).
240+
'edge://redirect/success/': { type: 'noop' },
241+
'edge://redirect/fail/': { type: 'noop' },
242+
'edge://redirect/cancel/': { type: 'noop' },
243+
'https://deep.edge.app/redirect/success/': { type: 'noop' },
244+
'https://deep.edge.app/redirect/fail/': { type: 'noop' },
245+
'https://deep.edge.app/redirect/cancel/': { type: 'noop' },
246+
'https://edge.app/redirect/success/': { type: 'noop' },
247+
'https://edge.app/redirect/cancel/': { type: 'noop' }
248+
})
249+
})
250+
192251
describe('plugin', function () {
193252
makeLinkTests({
194253
'edge://plugin/simplex/rabbit/hole?param=alice': {

src/actions/DeepLinkingActions.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { mul } from 'biggystring'
12
import type { EdgeParsedUri, EdgeTokenId } from 'edge-core-js'
23
import * as React from 'react'
34
import { Linking } from 'react-native'
@@ -21,6 +22,7 @@ import {
2122
fiatProviderDeeplinkHandler
2223
} from '../plugins/gui/fiatPlugin'
2324
import { rampDeeplinkManager } from '../plugins/ramps/rampDeeplinkHandler'
25+
import { getExchangeDenom } from '../selectors/DenominationSelectors'
2426
import { config } from '../theme/appConfig'
2527
import type { DeepLink } from '../types/DeepLinkTypes'
2628
import type { Dispatch, RootState, ThunkAction } from '../types/reduxTypes'
@@ -233,6 +235,67 @@ async function handleLink(
233235
})
234236
break
235237

238+
case 'paymentRedirect': {
239+
// A provider sell-completion redirect (e.g. MoonPay "Send with Edge").
240+
// Resolve the provider's base currency code to candidate assets, then
241+
// open the Send scene pre-filled with the deposit address, amount, and
242+
// destination tag / memo so the user can finish the sell order.
243+
const { currencyCode, depositAddress, amount, addressTag } = link
244+
245+
// Collect every native AND token asset that shares the symbol, and let
246+
// the user disambiguate via the wallet picker. A provider sell can be a
247+
// token whose ticker collides with another chain's native asset (the
248+
// provider disambiguates by network metadata we do not get here), so we
249+
// must not exclude token matches when a native one also matches.
250+
const symbol = currencyCode.split('_')[0].toUpperCase()
251+
const assets: EdgeAsset[] = []
252+
for (const pluginId of Object.keys(account.currencyConfig)) {
253+
const currencyConfig = account.currencyConfig[pluginId]
254+
if (currencyConfig.currencyInfo.currencyCode.toUpperCase() === symbol) {
255+
assets.push({ pluginId, tokenId: null })
256+
}
257+
const { builtinTokens } = currencyConfig
258+
for (const tokenId of Object.keys(builtinTokens)) {
259+
if (builtinTokens[tokenId].currencyCode.toUpperCase() === symbol) {
260+
assets.push({ pluginId, tokenId })
261+
}
262+
}
263+
}
264+
265+
if (assets.length === 0) {
266+
showToast(lstrings.alert_deep_link_no_wallet_for_uri)
267+
break
268+
}
269+
270+
const result = await pickWallet({
271+
account,
272+
assets,
273+
navigation,
274+
showCreateWallet: true
275+
})
276+
if (result?.type !== 'wallet') break
277+
const { walletId, tokenId } = result
278+
const wallet = account.currencyWallets[walletId]
279+
if (wallet == null) break
280+
281+
const nativeAmount =
282+
amount != null
283+
? mul(
284+
amount,
285+
getExchangeDenom(wallet.currencyConfig, tokenId).multiplier
286+
)
287+
: undefined
288+
289+
const parsedUri: EdgeParsedUri = {
290+
publicAddress: depositAddress,
291+
nativeAmount,
292+
uniqueIdentifier: addressTag,
293+
tokenId
294+
}
295+
await dispatch(handleWalletUris(navigation, wallet, parsedUri))
296+
break
297+
}
298+
236299
case 'price-change': {
237300
const { pluginId, body } = link
238301
const currencyCode =

src/plugins/gui/providers/common.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,20 @@ import {
77
type FiatProviderSupportedRegions
88
} from '../fiatProviderTypes'
99

10-
export const RETURN_URL_SUCCESS = 'https://edge.app/redirect/success/'
11-
export const RETURN_URL_FAIL = 'https://edge.app/redirect/fail/'
12-
export const RETURN_URL_CANCEL = 'https://edge.app/redirect/cancel/'
13-
export const RETURN_URL_PAYMENT = 'https://edge.app/redirect/payment/'
10+
// These URLs are handed to the ramp providers as sell redirect / callback URLs
11+
// and can be reflected back to the user outside the app (provider email or
12+
// order-history "complete payment" button), so they must live on a host the app
13+
// actually claims as a universal link. `deep.edge.app` is claimed on iOS and
14+
// Android (the apex `edge.app` is not), matching the buy redirects, so an
15+
// external click opens the app and the deep-link parser routes it (PAYMENT to
16+
// the pre-filled Send scene; the terminal states to a harmless no-op). In-app
17+
// interception is host-agnostic: Paybis/MoonPay use `uri.startsWith(...)` and
18+
// Banxa an exact `===` against the same constant, so swapping the host is
19+
// symmetric and does not change the in-webview behavior.
20+
export const RETURN_URL_SUCCESS = 'https://deep.edge.app/redirect/success/'
21+
export const RETURN_URL_FAIL = 'https://deep.edge.app/redirect/fail/'
22+
export const RETURN_URL_CANCEL = 'https://deep.edge.app/redirect/cancel/'
23+
export const RETURN_URL_PAYMENT = 'https://deep.edge.app/redirect/payment/'
1424

1525
export const NOT_SUCCESS_TOAST_HIDE_MS = 5000
1626

src/types/DeepLinkTypes.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,25 @@ export interface PaymentProtoLink {
5656
uri: string
5757
}
5858

59+
/**
60+
* A provider sell-completion redirect (e.g. MoonPay's "Send with Edge" button).
61+
* Carries everything needed to open the Send scene so the user can finish
62+
* depositing crypto for a pending sell order:
63+
*
64+
* https://edge.app/redirect/payment/?baseCurrencyCode=btc&baseCurrencyAmount=0.001&depositWalletAddress=...&depositWalletAddressTag=...
65+
*
66+
* `currencyCode` is the provider's base currency code (resolved to a wallet at
67+
* handle time), `addressTag` is the destination tag / memo (required for chains
68+
* like XRP), and `amount` is in whole units of the base currency.
69+
*/
70+
export interface PaymentRedirectLink {
71+
type: 'paymentRedirect'
72+
currencyCode: string
73+
depositAddress: string
74+
amount?: string
75+
addressTag?: string
76+
}
77+
5978
export interface EdgeLoginLink {
6079
type: 'edgeLogin'
6180
lobbyId: string
@@ -169,6 +188,7 @@ export type DeepLink =
169188
| NoopLink
170189
| PasswordRecoveryLink
171190
| PaymentProtoLink
191+
| PaymentRedirectLink
172192
| PluginLink
173193
| PriceChangeLink
174194
| PromotionLink

src/util/DeepLinkParser.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type PromotionLink
1515
} from '../types/DeepLinkTypes'
1616
import type { AppParamList } from '../types/routerTypes'
17+
import type { UriQueryMap } from '../types/WebTypes'
1718
import { parseQuery, stringifyQuery } from './WebUtils'
1819

1920
/**
@@ -244,6 +245,24 @@ function parseEdgeProtocol(url: URL<string>): DeepLink {
244245
}
245246
}
246247

248+
case 'redirect': {
249+
// Provider ramp redirect (e.g. MoonPay "Send with Edge"). All ramp
250+
// redirect URLs now live on the claimed `deep.edge.app` host, which
251+
// normalizes to this `edge://` path. A `/redirect/payment` resolves to a
252+
// pre-filled Send scene; the terminal states (`success`/`fail`/`cancel`)
253+
// carry no actionable payload, so an externally-tapped link just opens
254+
// the app via a no-op. A malformed or param-less payment URL degrades to
255+
// a no-op too, rather than surfacing an "Unknown deep link format" error.
256+
const [section] = pathParts
257+
if (section === 'payment') {
258+
return parsePaymentRedirect(parseQuery(url.query)) ?? { type: 'noop' }
259+
}
260+
if (section === 'success' || section === 'fail' || section === 'cancel') {
261+
return { type: 'noop' }
262+
}
263+
break
264+
}
265+
247266
case 'recovery': {
248267
// The new & improved format stores the token as a fragment:
249268
if (url.hash != null && url.hash !== '') {
@@ -361,6 +380,24 @@ function parseEdgeAppLink(url: URL<string>): DeepLink {
361380
}
362381
}
363382

383+
// Handle provider ramp redirects (e.g. MoonPay "Send with Edge"), which
384+
// legacy orders may still point at the apex https://edge.app/redirect/...
385+
// A `/redirect/payment` carries the deposit details for a pending sell order;
386+
// the terminal states (`success`/`fail`/`cancel`) resolve to a no-op.
387+
if (firstPath === 'redirect') {
388+
const section = pathParts[1]
389+
if (section === 'payment') {
390+
const link = parsePaymentRedirect(query)
391+
if (link != null) return link
392+
} else if (
393+
section === 'success' ||
394+
section === 'fail' ||
395+
section === 'cancel'
396+
) {
397+
return { type: 'noop' }
398+
}
399+
}
400+
364401
// No special handling supported. Open in browser.
365402
return {
366403
type: 'other',
@@ -369,6 +406,27 @@ function parseEdgeAppLink(url: URL<string>): DeepLink {
369406
}
370407
}
371408

409+
/**
410+
* Parse a provider sell-completion redirect such as MoonPay's "Send with Edge"
411+
* button, which sends the user to a `/redirect/payment/` URL carrying the
412+
* deposit details for a pending sell order. Returns null when the required
413+
* deposit parameters are missing so the caller can fall back to its default
414+
* handling (e.g. opening the link in a browser).
415+
*/
416+
function parsePaymentRedirect(query: UriQueryMap): DeepLink | null {
417+
const baseCurrencyCode = query.baseCurrencyCode ?? undefined
418+
const depositWalletAddress = query.depositWalletAddress ?? undefined
419+
if (baseCurrencyCode == null || depositWalletAddress == null) return null
420+
421+
return {
422+
type: 'paymentRedirect',
423+
currencyCode: baseCurrencyCode,
424+
depositAddress: depositWalletAddress,
425+
amount: query.baseCurrencyAmount ?? undefined,
426+
addressTag: query.depositWalletAddressTag ?? undefined
427+
}
428+
}
429+
372430
/**
373431
* Parse a request for address link.
374432
*/

0 commit comments

Comments
 (0)