Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased (develop)

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

## 4.49.0 (staging)

Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/DeepLink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,48 @@ describe('parseDeepLink', function () {
})
})

describe('paymentRedirect', () => {
makeLinkTests({
// Real-world MoonPay "Send with Edge" sell link (extra params ignored):
'https://edge.app/redirect/payment/?transactionId=6ae325aa-d930-47cd-9ef0-d26e03b68f3c&baseCurrencyCode=btc&baseCurrencyAmount=0.00212&depositWalletAddress=bc1qqp44yqt9nzrca7cw4hrl2hu5nmpw5fg0r32z62&paymentMethod=moonpay_balance':
{
type: 'paymentRedirect',
currencyCode: 'btc',
depositAddress: 'bc1qqp44yqt9nzrca7cw4hrl2hu5nmpw5fg0r32z62',
amount: '0.00212',
addressTag: undefined
},
// XRP needs a destination tag, carried as depositWalletAddressTag:
'edge://redirect/payment/?baseCurrencyCode=xrp&baseCurrencyAmount=10&depositWalletAddress=rEXAMPLExrpADDRESS&depositWalletAddressTag=123456':
{
type: 'paymentRedirect',
currencyCode: 'xrp',
depositAddress: 'rEXAMPLExrpADDRESS',
amount: '10',
addressTag: '123456'
},
// deep.edge.app is an already-claimed universal-link host:
'https://deep.edge.app/redirect/payment/?baseCurrencyCode=ltc&baseCurrencyAmount=1.5&depositWalletAddress=ltc1qexample':
{
type: 'paymentRedirect',
currencyCode: 'ltc',
depositAddress: 'ltc1qexample',
amount: '1.5',
addressTag: undefined
},
// Missing the deposit address on the edge.app host falls back to a browser:
'https://edge.app/redirect/payment/?baseCurrencyCode=btc': {
type: 'other',
protocol: 'https',
uri: 'https://edge.app/redirect/payment/?baseCurrencyCode=btc'
},
// Missing params on the edge:// (deep.edge.app) path degrade to a no-op
// instead of an "Unknown deep link format" error:
'edge://redirect/payment/?baseCurrencyCode=btc': { type: 'noop' },
'https://deep.edge.app/redirect/payment/': { type: 'noop' }
})
})

describe('plugin', function () {
makeLinkTests({
'edge://plugin/simplex/rabbit/hole?param=alice': {
Expand Down
63 changes: 63 additions & 0 deletions src/actions/DeepLinkingActions.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { mul } from 'biggystring'
import type { EdgeParsedUri, EdgeTokenId } from 'edge-core-js'
import * as React from 'react'
import { Linking } from 'react-native'
Expand All @@ -21,6 +22,7 @@ import {
fiatProviderDeeplinkHandler
} from '../plugins/gui/fiatPlugin'
import { rampDeeplinkManager } from '../plugins/ramps/rampDeeplinkHandler'
import { getExchangeDenom } from '../selectors/DenominationSelectors'
import { config } from '../theme/appConfig'
import type { DeepLink } from '../types/DeepLinkTypes'
import type { Dispatch, RootState, ThunkAction } from '../types/reduxTypes'
Expand Down Expand Up @@ -233,6 +235,67 @@ async function handleLink(
})
break

case 'paymentRedirect': {
// A provider sell-completion redirect (e.g. MoonPay "Send with Edge").
// Resolve the provider's base currency code to candidate assets, then
// open the Send scene pre-filled with the deposit address, amount, and
// destination tag / memo so the user can finish the sell order.
const { currencyCode, depositAddress, amount, addressTag } = link

// Collect every native AND token asset that shares the symbol, and let
// the user disambiguate via the wallet picker. A provider sell can be a
// token whose ticker collides with another chain's native asset (the
// provider disambiguates by network metadata we do not get here), so we
// must not exclude token matches when a native one also matches.
const symbol = currencyCode.split('_')[0].toUpperCase()
const assets: EdgeAsset[] = []
for (const pluginId of Object.keys(account.currencyConfig)) {
const currencyConfig = account.currencyConfig[pluginId]
if (currencyConfig.currencyInfo.currencyCode.toUpperCase() === symbol) {
assets.push({ pluginId, tokenId: null })
}
const { builtinTokens } = currencyConfig
for (const tokenId of Object.keys(builtinTokens)) {
if (builtinTokens[tokenId].currencyCode.toUpperCase() === symbol) {
assets.push({ pluginId, tokenId })
}
}
}

if (assets.length === 0) {
showToast(lstrings.alert_deep_link_no_wallet_for_uri)
break
}

const result = await pickWallet({
account,
assets,
navigation,
showCreateWallet: true
})
if (result?.type !== 'wallet') break
const { walletId, tokenId } = result
const wallet = account.currencyWallets[walletId]
if (wallet == null) break

const nativeAmount =
amount != null
? mul(
amount,
getExchangeDenom(wallet.currencyConfig, tokenId).multiplier
)
: undefined

const parsedUri: EdgeParsedUri = {
publicAddress: depositAddress,
nativeAmount,
uniqueIdentifier: addressTag,
tokenId
}
await dispatch(handleWalletUris(navigation, wallet, parsedUri))
break
}

case 'price-change': {
const { pluginId, body } = link
const currencyCode =
Expand Down
9 changes: 8 additions & 1 deletion src/plugins/gui/providers/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import {
export const RETURN_URL_SUCCESS = 'https://edge.app/redirect/success/'
export const RETURN_URL_FAIL = 'https://edge.app/redirect/fail/'
export const RETURN_URL_CANCEL = 'https://edge.app/redirect/cancel/'
export const RETURN_URL_PAYMENT = 'https://edge.app/redirect/payment/'
// This URL is handed to the provider as the sell deposit redirect and is
// reflected back to the user outside the app (provider email / order-history
// "complete payment" button), so it must be a host the app actually claims as a
// universal link. `deep.edge.app` is claimed on iOS and Android (the apex
// `edge.app` is not), matching the buy redirects, so an external click opens the
// app and the deep-link parser routes it to the Send scene. In-app interception
// is host-agnostic (`uri.startsWith(RETURN_URL_PAYMENT)`), so it is unaffected.
export const RETURN_URL_PAYMENT = 'https://deep.edge.app/redirect/payment/'

export const NOT_SUCCESS_TOAST_HIDE_MS = 5000

Expand Down
20 changes: 20 additions & 0 deletions src/types/DeepLinkTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ export interface PaymentProtoLink {
uri: string
}

/**
* A provider sell-completion redirect (e.g. MoonPay's "Send with Edge" button).
* Carries everything needed to open the Send scene so the user can finish
* depositing crypto for a pending sell order:
*
* https://edge.app/redirect/payment/?baseCurrencyCode=btc&baseCurrencyAmount=0.001&depositWalletAddress=...&depositWalletAddressTag=...
*
* `currencyCode` is the provider's base currency code (resolved to a wallet at
* handle time), `addressTag` is the destination tag / memo (required for chains
* like XRP), and `amount` is in whole units of the base currency.
*/
export interface PaymentRedirectLink {
type: 'paymentRedirect'
currencyCode: string
depositAddress: string
amount?: string
addressTag?: string
}

export interface EdgeLoginLink {
type: 'edgeLogin'
lobbyId: string
Expand Down Expand Up @@ -169,6 +188,7 @@ export type DeepLink =
| NoopLink
| PasswordRecoveryLink
| PaymentProtoLink
| PaymentRedirectLink
| PluginLink
| PriceChangeLink
| PromotionLink
Expand Down
43 changes: 43 additions & 0 deletions src/util/DeepLinkParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type PromotionLink
} from '../types/DeepLinkTypes'
import type { AppParamList } from '../types/routerTypes'
import type { UriQueryMap } from '../types/WebTypes'
import { parseQuery, stringifyQuery } from './WebUtils'

/**
Expand Down Expand Up @@ -244,6 +245,20 @@ function parseEdgeProtocol(url: URL<string>): DeepLink {
}
}

case 'redirect': {
// Provider sell-completion redirect (e.g. MoonPay "Send with Edge").
// Always resolve a `/redirect/payment` path: a malformed or
// provider-specific (param-less, e.g. Paybis) URL degrades to a no-op
// rather than surfacing an "Unknown deep link format" error, since new
// sell redirects use `deep.edge.app` which normalizes to this `edge://`
// path.
const [section] = pathParts
if (section === 'payment') {
return parsePaymentRedirect(parseQuery(url.query)) ?? { type: 'noop' }
}
break
}
Comment thread
cursor[bot] marked this conversation as resolved.

case 'recovery': {
// The new & improved format stores the token as a fragment:
if (url.hash != null && url.hash !== '') {
Expand Down Expand Up @@ -361,6 +376,13 @@ function parseEdgeAppLink(url: URL<string>): DeepLink {
}
}

// Handle provider sell-completion redirects (e.g. MoonPay "Send with Edge"),
// which point at https://edge.app/redirect/payment/?baseCurrencyCode=...
if (firstPath === 'redirect' && pathParts[1] === 'payment') {
const link = parsePaymentRedirect(query)
if (link != null) return link
}

// No special handling supported. Open in browser.
return {
type: 'other',
Expand All @@ -369,6 +391,27 @@ function parseEdgeAppLink(url: URL<string>): DeepLink {
}
}

/**
* Parse a provider sell-completion redirect such as MoonPay's "Send with Edge"
* button, which sends the user to a `/redirect/payment/` URL carrying the
* deposit details for a pending sell order. Returns null when the required
* deposit parameters are missing so the caller can fall back to its default
* handling (e.g. opening the link in a browser).
*/
function parsePaymentRedirect(query: UriQueryMap): DeepLink | null {
const baseCurrencyCode = query.baseCurrencyCode ?? undefined
const depositWalletAddress = query.depositWalletAddress ?? undefined
if (baseCurrencyCode == null || depositWalletAddress == null) return null

return {
type: 'paymentRedirect',
currencyCode: baseCurrencyCode,
depositAddress: depositWalletAddress,
amount: query.baseCurrencyAmount ?? undefined,
addressTag: query.depositWalletAddressTag ?? undefined
}
}

/**
* Parse a request for address link.
*/
Expand Down
Loading