` → `/qr?code=`,
- * `/add-money//bank` → `/add-money?country=&view=bank`, etc.
- * Static routes (e.g. `/card`, `/pay-request`) pass through unchanged.
- * Returns null for an unparseable URL.
+ * Maps an incoming deep link — an App-Links URL (https://peanut.me/) or a
+ * bare path from a push payload — to the path the native static export can
+ * actually render. Dynamic web routes are funnelled through the same query-param
+ * helpers used to build outbound links, so `/send/` → `/send?recipient=`,
+ * `/qr/` → `/qr?code=`, `/add-money//bank` →
+ * `/add-money?country=&view=bank`, etc. Static routes (e.g. `/card`,
+ * `/pay-request`) pass through unchanged.
+ *
+ * Returns null for an unparseable link or one pointing at another host — an
+ * off-domain URL must stay off-domain rather than be rewritten into a bogus
+ * in-app path.
*/
export function deepLinkToNativePath(url: string): string | null {
- let parsed: URL
try {
- parsed = new URL(url)
+ return mapDeepLink(url)
} catch {
+ // The whole body is guarded, not just the URL parse: decodeURIComponent
+ // throws URIError on a stray `%`, and this runs during render in the
+ // notifications list — one malformed ctaDeeplink would blank the page.
return null
}
+}
+
+function mapDeepLink(url: string): string | null {
+ // Relative base: push payloads carry the canonical path (`/receipt/`),
+ // App Links carry the full URL. Both must resolve.
+ const parsed = new URL(url, 'https://peanut.me')
+ if (!APP_HOSTS.test(parsed.hostname)) return null
+
const path = parsed.pathname
const extraParams = parsed.search.replace(/^\?/, '')
const segments = path.split('/').filter(Boolean)
@@ -96,6 +117,21 @@ export function deepLinkToNativePath(url: string): string | null {
if (segments[0] === 'add-money' || segments[0] === 'withdraw') {
return rewriteMethodPath(path, extraParams || undefined)
}
+ // `/receipt/?kind=X` — the web receipt page is a server component and is
+ // stripped from the static export (scripts/native-build.js), so native routes
+ // to the client variant. `kind` rides along in extraParams.
+ if (segments[0] === 'receipt' && segments[1]) {
+ const id = decodeURIComponent(segments[1])
+ return appendParams(isCapacitor() ? `/receipt?id=${encodeURIComponent(id)}` : path, extraParams)
+ }
+ // `/?chargeId=` — the catch-all profile route is disabled in
+ // native builds; /pay-request is its stand-in (see (mobile-ui)/pay-request).
+ // Gated by the same reserved-route/recipient rules the web catch-all uses, so
+ // `/rewards?chargeId=x` stays on /rewards instead of landing on /pay-request.
+ if (isCapacitor() && segments.length === 1 && !isReservedRoute(path) && couldBeRecipient(segments[0])) {
+ const chargeId = parsed.searchParams.get('chargeId')
+ if (chargeId) return chargePayUrl(chargeId)
+ }
return appendParams(path, extraParams)
}