Skip to content
Merged
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 @@ -4,6 +4,7 @@

## 4.49.0 (staging)

- added: Honor `af` affiliate parameter on `deep.edge.app` deep links, activating the promotion alongside any inner payload (e.g. private-key import).
- added: Show swap KYC/terms modal for NExchange
- added: Nym mixnet warning in Stake, Unstake, and Claim Rewards scenes
- changed: Migrate Thorchain Savers and Thorchain Yield endpoints off NineRealms to gateway.liquify.com.
Expand Down
43 changes: 43 additions & 0 deletions src/__tests__/DeepLink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,53 @@ describe('parseDeepLink', function () {
'https://dl.edge.app': {
type: 'promotion',
installerId: ''
},
'https://deep.edge.app/?af=bob': {
type: 'promotion',
installerId: 'bob'
},
'https://deep.edge.app/promotion/bob?af=bob': {
type: 'promotion',
installerId: 'bob'
}
})
})

describe('affiliate', function () {
makeLinkTests({
'https://deep.edge.app/pay/bitcoincash/abc123?af=zano-telegram': {
type: 'affiliate',
installerId: 'zano-telegram',
link: {
type: 'other',
protocol: 'bitcoincash',
uri: 'bitcoincash:abc123'
}
},
'https://deep.edge.app/plugin/simplex/rabbit/hole?af=bob&param=alice': {
type: 'affiliate',
installerId: 'bob',
link: {
type: 'plugin',
pluginId: 'simplex',
path: '/rabbit/hole',
query: { param: 'alice' }
}
}
})

// Lookalike hosts must NOT be treated as deep.edge.app:
it('https://deep.edge.appsomething.com/?af=evil', () => {
expect(
parseDeepLink('https://deep.edge.appsomething.com/?af=evil')
).toEqual({
type: 'other',
protocol: 'https',
uri: 'https://deep.edge.appsomething.com/?af=evil'
})
})
})

describe('swap', () => {
makeLinkTests({
'edge://swap': {
Expand Down
5 changes: 5 additions & 0 deletions src/actions/DeepLinkingActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ async function handleLink(
await dispatch(activatePromotion(link.installerId ?? ''))
break

case 'affiliate':
await dispatch(activatePromotion(link.installerId))
await handleLink(navigation, dispatch, state, link.link)
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Promotion failure blocks inner link processing in affiliate handler

Medium Severity

If activatePromotion throws (e.g. asServerTweaks rejects malformed server JSON, or saveAccountReferral hits a disk I/O error), the error propagates up and the second await handleLink(…, link.link) never executes. For the BCHx gift-card scenario, this means a transient promotion-server hiccup silently prevents the private-key import — the more important payload — from ever being processed.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b724830. Configure here.


case 'requestAddress':
await doRequestAddress(navigation, state.core.account, dispatch, link)
break
Expand Down
12 changes: 12 additions & 0 deletions src/types/DeepLinkTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
* - https://dl.edge.app/... = edge://promotion/...
* - https://dl.edge.app/?af=... = edge://promotion/...
*
* `deep.edge.app` URLs may also carry an `?af=<installerId>` query parameter.
* When present alongside another payload (e.g. a `pay` private-key URI), the
* deep link resolves to an `affiliate` wrapper that activates the promotion
* and then delegates to the inner link.
*
* We also support some legacy prefixes (but don't use these):
*
* - edge-ret://plugins/simplex/... = edge://plugin/simplex/...
Expand Down Expand Up @@ -147,7 +152,14 @@ export interface ModalLink {
modalName: ModalNames
}

export interface AffiliateLink {
type: 'affiliate'
installerId: string
link: DeepLink
}
Comment thread
j0ntz marked this conversation as resolved.

export type DeepLink =
| AffiliateLink
| AztecoLink
| SceneLink
| EdgeLoginLink
Expand Down
41 changes: 41 additions & 0 deletions src/util/DeepLinkParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ export function parseDeepLink(
): DeepLink {
const { aztecoApiKey = ENV.AZTECO_API_KEY } = opts

// Extract an `af` affiliate installer id from `deep.edge.app` URLs before
// the prefix normalization below strips the host. Matches the `dl.edge.app`
// behavior and adds a wrapper when there is also an inner payload:
const affiliateSplit = splitAffiliateLink(uri)
if (affiliateSplit != null) {
const { installerId, remainingUri } = affiliateSplit
const inner = parseDeepLink(remainingUri, opts)
if (inner.type === 'promotion' || inner.type === 'noop') {
return { type: 'promotion', installerId }
}
return { type: 'affiliate', installerId, link: inner }
}

// Normalize some legacy cases:
for (const prefix of prefixes) {
const [from, to] = prefix
Expand Down Expand Up @@ -351,3 +364,31 @@ const prefixes: Array<[string, string]> = [
['airbitz://', 'edge://'],
['reqaddr://', 'edge://reqaddr']
]

/**
* Detect an `af` affiliate installer id on a `deep.edge.app` URL and return
* the extracted id plus the original URL with `af` stripped from the query.
* Returns `null` for every other input.
*/
function splitAffiliateLink(
uri: string
): { installerId: string; remainingUri: string } | null {
if (!uri.startsWith('https://')) return null

const url = new URL(uri)
if (url.host !== 'deep.edge.app') return null
const query = parseQuery(url.query)
const { af } = query
if (af == null || af === '') return null

const remainingQuery: typeof query = {}
for (const key of Object.keys(query)) {
if (key !== 'af') remainingQuery[key] = query[key]
}
const queryString =
Object.keys(remainingQuery).length === 0
? ''
: stringifyQuery(remainingQuery)
const remainingUri = `${url.protocol}//${url.host}${url.pathname}${queryString}${url.hash}`
return { installerId: af, remainingUri }
}
Loading