Skip to content

Commit 676c146

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 (needed for chains like XRP). Both the deep.edge.app and legacy edge.app hosts are parsed.
1 parent 1811696 commit 676c146

6 files changed

Lines changed: 168 additions & 1 deletion

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. The sell deposit redirect points at the claimed deep.edge.app universal-link host (was the unclaimed edge.app apex), and the deep-link parser routes it to Send with the deposit address, amount, and destination tag.
67

78
## 4.49.0 (staging)
89

src/__tests__/DeepLink.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,44 @@ 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 falls back to opening in 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+
})
228+
})
229+
192230
describe('plugin', function () {
193231
makeLinkTests({
194232
'edge://plugin/simplex/rabbit/hole?param=alice': {

src/actions/DeepLinkingActions.tsx

Lines changed: 62 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,66 @@ 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+
// Prefer a native (mainnet) match; otherwise fall back to tokens that
246+
// share the symbol and let the user disambiguate via the wallet picker.
247+
const symbol = currencyCode.split('_')[0].toUpperCase()
248+
const nativeAssets: EdgeAsset[] = []
249+
const tokenAssets: EdgeAsset[] = []
250+
for (const pluginId of Object.keys(account.currencyConfig)) {
251+
const currencyConfig = account.currencyConfig[pluginId]
252+
if (currencyConfig.currencyInfo.currencyCode.toUpperCase() === symbol) {
253+
nativeAssets.push({ pluginId, tokenId: null })
254+
}
255+
const { builtinTokens } = currencyConfig
256+
for (const tokenId of Object.keys(builtinTokens)) {
257+
if (builtinTokens[tokenId].currencyCode.toUpperCase() === symbol) {
258+
tokenAssets.push({ pluginId, tokenId })
259+
}
260+
}
261+
}
262+
const assets = nativeAssets.length > 0 ? nativeAssets : tokenAssets
263+
264+
if (assets.length === 0) {
265+
showToast(lstrings.alert_deep_link_no_wallet_for_uri)
266+
break
267+
}
268+
269+
const result = await pickWallet({
270+
account,
271+
assets,
272+
navigation,
273+
showCreateWallet: true
274+
})
275+
if (result?.type !== 'wallet') break
276+
const { walletId, tokenId } = result
277+
const wallet = account.currencyWallets[walletId]
278+
if (wallet == null) break
279+
280+
const nativeAmount =
281+
amount != null
282+
? mul(
283+
amount,
284+
getExchangeDenom(wallet.currencyConfig, tokenId).multiplier
285+
)
286+
: undefined
287+
288+
const parsedUri: EdgeParsedUri = {
289+
publicAddress: depositAddress,
290+
nativeAmount,
291+
uniqueIdentifier: addressTag,
292+
tokenId
293+
}
294+
await dispatch(handleWalletUris(navigation, wallet, parsedUri))
295+
break
296+
}
297+
236298
case 'price-change': {
237299
const { pluginId, body } = link
238300
const currencyCode =

src/plugins/gui/providers/common.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ import {
1010
export const RETURN_URL_SUCCESS = 'https://edge.app/redirect/success/'
1111
export const RETURN_URL_FAIL = 'https://edge.app/redirect/fail/'
1212
export const RETURN_URL_CANCEL = 'https://edge.app/redirect/cancel/'
13-
export const RETURN_URL_PAYMENT = 'https://edge.app/redirect/payment/'
13+
// This URL is handed to the provider as the sell deposit redirect and is
14+
// reflected back to the user outside the app (provider email / order-history
15+
// "complete payment" button), so it must be a host the app actually claims as a
16+
// universal link. `deep.edge.app` is claimed on iOS and Android (the apex
17+
// `edge.app` is not), matching the buy redirects, so an external click opens the
18+
// app and the deep-link parser routes it to the Send scene. In-app interception
19+
// is host-agnostic (`uri.startsWith(RETURN_URL_PAYMENT)`), so it is unaffected.
20+
export const RETURN_URL_PAYMENT = 'https://deep.edge.app/redirect/payment/'
1421

1522
export const NOT_SUCCESS_TOAST_HIDE_MS = 5000
1623

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: 39 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,16 @@ function parseEdgeProtocol(url: URL<string>): DeepLink {
244245
}
245246
}
246247

248+
case 'redirect': {
249+
// Provider sell-completion redirect (e.g. MoonPay "Send with Edge"):
250+
const [section] = pathParts
251+
if (section === 'payment') {
252+
const link = parsePaymentRedirect(parseQuery(url.query))
253+
if (link != null) return link
254+
}
255+
break
256+
}
257+
247258
case 'recovery': {
248259
// The new & improved format stores the token as a fragment:
249260
if (url.hash != null && url.hash !== '') {
@@ -361,6 +372,13 @@ function parseEdgeAppLink(url: URL<string>): DeepLink {
361372
}
362373
}
363374

375+
// Handle provider sell-completion redirects (e.g. MoonPay "Send with Edge"),
376+
// which point at https://edge.app/redirect/payment/?baseCurrencyCode=...
377+
if (firstPath === 'redirect' && pathParts[1] === 'payment') {
378+
const link = parsePaymentRedirect(query)
379+
if (link != null) return link
380+
}
381+
364382
// No special handling supported. Open in browser.
365383
return {
366384
type: 'other',
@@ -369,6 +387,27 @@ function parseEdgeAppLink(url: URL<string>): DeepLink {
369387
}
370388
}
371389

390+
/**
391+
* Parse a provider sell-completion redirect such as MoonPay's "Send with Edge"
392+
* button, which sends the user to a `/redirect/payment/` URL carrying the
393+
* deposit details for a pending sell order. Returns null when the required
394+
* deposit parameters are missing so the caller can fall back to its default
395+
* handling (e.g. opening the link in a browser).
396+
*/
397+
function parsePaymentRedirect(query: UriQueryMap): DeepLink | null {
398+
const baseCurrencyCode = query.baseCurrencyCode ?? undefined
399+
const depositWalletAddress = query.depositWalletAddress ?? undefined
400+
if (baseCurrencyCode == null || depositWalletAddress == null) return null
401+
402+
return {
403+
type: 'paymentRedirect',
404+
currencyCode: baseCurrencyCode,
405+
depositAddress: depositWalletAddress,
406+
amount: query.baseCurrencyAmount ?? undefined,
407+
addressTag: query.depositWalletAddressTag ?? undefined
408+
}
409+
}
410+
372411
/**
373412
* Parse a request for address link.
374413
*/

0 commit comments

Comments
 (0)