Browse Source

wallet handlers

imwald
Silberengel 4 weeks ago
parent
commit
fddb722fa0
  1. 121
      src/components/PaytoDialog/index.tsx
  2. 24
      src/data/payto-types.json
  3. 4
      src/lib/payto-registry.ts
  4. 79
      src/lib/payto-wallet-open.test.ts
  5. 232
      src/lib/payto-wallet-open.ts
  6. 6
      src/lib/payto.ts

121
src/components/PaytoDialog/index.tsx

@ -7,7 +7,15 @@ import { @@ -7,7 +7,15 @@ import {
DialogTitle
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Copy, ExternalLink, Wallet, Zap } from 'lucide-react'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import type { PaytoPaymentOpenHandler } from '@/lib/payto'
import { ArrowRight, Copy, Zap } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { closeModal } from '@getalby/bitcoin-connect-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@ -16,7 +24,6 @@ import { releaseBodyScrollLocks } from '@/lib/react-remove-scroll-body-cleanup' @@ -16,7 +24,6 @@ import { releaseBodyScrollLocks } from '@/lib/react-remove-scroll-body-cleanup'
import {
filterPaytoPaymentOpenHandlersForDevice,
getPaytoPaymentOpenHandlers,
getPhoenixPaymentOpenHandler,
getPaytoTypeInfo
} from '@/lib/payto'
import { cn } from '@/lib/utils'
@ -50,10 +57,12 @@ export default function PaytoDialog({ @@ -50,10 +57,12 @@ export default function PaytoDialog({
const label = info?.label ?? type
const isLightning = type.toLowerCase() === 'lightning'
const [bolt11Invoice, setBolt11Invoice] = useState<string | null>(null)
const [selectedOpenHandlerId, setSelectedOpenHandlerId] = useState('')
useEffect(() => {
if (!open) {
setBolt11Invoice(null)
setSelectedOpenHandlerId('')
closeModal()
releaseBodyScrollLocks()
}
@ -64,14 +73,42 @@ export default function PaytoDialog({ @@ -64,14 +73,42 @@ export default function PaytoDialog({
onOpenChange(false)
}, [onOpenChange])
const openHandlers = useMemo(() => {
const handlers = getPaytoPaymentOpenHandlers(type, authority)
if (isLightning && bolt11Invoice) {
const phoenix = getPhoenixPaymentOpenHandler('lightning', bolt11Invoice)
if (phoenix) handlers.push(phoenix)
const openHandlers = useMemo(
() =>
filterPaytoPaymentOpenHandlersForDevice(
getPaytoPaymentOpenHandlers(type, authority, { bolt11Invoice })
),
[type, authority, bolt11Invoice]
)
useEffect(() => {
if (openHandlers.length === 0) {
setSelectedOpenHandlerId('')
return
}
return filterPaytoPaymentOpenHandlersForDevice(handlers)
}, [type, authority, isLightning, bolt11Invoice])
setSelectedOpenHandlerId((prev) =>
openHandlers.some((h) => h.id === prev) ? prev : openHandlers[0].id
)
}, [openHandlers])
const selectedOpenHandler = useMemo(
() =>
openHandlers.find((h) => h.id === selectedOpenHandlerId) ??
openHandlers[0] ??
null,
[openHandlers, selectedOpenHandlerId]
)
const openSelectedHandler = useCallback(
(handler: PaytoPaymentOpenHandler) => {
if (handler.isHttp) {
window.open(handler.href, '_blank', 'noopener,noreferrer')
return
}
window.location.assign(handler.href)
},
[]
)
const handleCopy = (text: string, copyLabel?: string) => {
navigator.clipboard.writeText(text)
@ -166,31 +203,49 @@ export default function PaytoDialog({ @@ -166,31 +203,49 @@ export default function PaytoDialog({
<p className="text-sm font-semibold uppercase tracking-wider text-muted-foreground sm:text-base">
{t('Open with')}
</p>
<div className="grid min-w-0 grid-cols-1 gap-2 sm:grid-cols-2">
{openHandlers.map((handler) => (
<Button
key={handler.id}
variant="outline"
className="h-11 w-full min-w-0 justify-start gap-2 px-3 text-base"
asChild
<div className="flex min-w-0 items-stretch gap-2">
<Select
value={selectedOpenHandlerId}
onValueChange={setSelectedOpenHandlerId}
>
<SelectTrigger
className="h-11 min-w-0 flex-1 text-base"
aria-label={t('Open with')}
>
<a
href={handler.href}
className="flex min-w-0 items-center"
{...(handler.isHttp
? { target: '_blank', rel: 'noopener noreferrer' }
: {})}
onClick={(e) => e.stopPropagation()}
>
{handler.isHttp ? (
<ExternalLink className="size-5 shrink-0" />
) : (
<Wallet className="size-5 shrink-0" />
)}
<span className="truncate">{t('Open in {{name}}', { name: handler.openTargetName })}</span>
</a>
</Button>
))}
<SelectValue
placeholder={t('Choose app', { defaultValue: 'Choose app' })}
/>
</SelectTrigger>
<SelectContent>
{openHandlers.map((handler) => (
<SelectItem key={handler.id} value={handler.id} className="text-base">
{handler.openTargetName}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="outline"
size="icon"
className="h-11 w-11 shrink-0"
disabled={!selectedOpenHandler}
title={
selectedOpenHandler
? t('Open in {{name}}', { name: selectedOpenHandler.openTargetName })
: undefined
}
aria-label={
selectedOpenHandler
? t('Open in {{name}}', { name: selectedOpenHandler.openTargetName })
: t('Open', { defaultValue: 'Open' })
}
onClick={() => {
if (selectedOpenHandler) openSelectedHandler(selectedOpenHandler)
}}
>
<ArrowRight className="size-5" aria-hidden />
</Button>
</div>
</div>
)}

24
src/data/payto-types.json

@ -3,6 +3,13 @@ @@ -3,6 +3,13 @@
"logoAssetsDir": "src/assets/payto_logos",
"logoAssetPathNote": "Repo-relative path to the logo file. The app bundles these via Vite and exposes them under /assets/ at runtime (see getPaytoLogoPath)."
},
"_openWith": {
"bolt11Invoice": {
"paytoType": "lightning",
"coinScheme": "lightning",
"walletApps": ["phoenix", "zeus", "bluewallet"]
}
},
"walletApps": {
"cakewallet": {
"label": "Cake Wallet",
@ -13,6 +20,16 @@ @@ -13,6 +20,16 @@
"label": "Phoenix",
"mobileOnly": true,
"uriTemplate": "phoenix:{coinScheme}:{authority}"
},
"zeus": {
"label": "Zeus",
"mobileOnly": true,
"uriTemplate": "zeusln:lightning:{authority}"
},
"bluewallet": {
"label": "BlueWallet",
"mobileOnly": true,
"uriTemplate": "bluewallet:lightning:{authority}"
}
},
"kind0CryptocurrencyAddresses": {
@ -116,7 +133,7 @@ @@ -116,7 +133,7 @@
"walletOpen": {
"scheme": "bolt12",
"requirePrefix": "lno1",
"walletApps": ["phoenix"]
"walletApps": ["phoenix", "zeus"]
},
"authority": {
"placeholder": "lno1…",
@ -130,7 +147,7 @@ @@ -130,7 +147,7 @@
"walletOpen": {
"scheme": "lightning",
"requireAtSign": true,
"walletApps": ["phoenix"]
"walletApps": ["phoenix", "zeus"]
},
"authority": {
"placeholder": "user@example.com",
@ -179,7 +196,7 @@ @@ -179,7 +196,7 @@
"walletOpen": {
"scheme": "lightning",
"requireAtSign": true,
"walletApps": ["phoenix"]
"deferWalletAppsUntilBolt11": true
},
"authority": {
"placeholder": "user@getalby.com",
@ -361,6 +378,7 @@ @@ -361,6 +378,7 @@
"symbol": "💙",
"category": "fiat",
"logoAssetPath": "src/assets/payto_logos/paypal.webp",
"paymentOpen": "paypal",
"profileUrlTemplate": "https://paypal.me/{authority}",
"authority": {
"placeholder": "username (not email)",

4
src/lib/payto-registry.ts

@ -23,6 +23,8 @@ export type PaytoWalletOpenRow = { @@ -23,6 +23,8 @@ export type PaytoWalletOpenRow = {
requireAtSign?: boolean
requirePrefix?: string
walletApps?: string[]
/** When true, {@link walletApps} are hidden until PaytoDialog has a BOLT11 (see catalog `_openWith.bolt11Invoice`). */
deferWalletAppsUntilBolt11?: boolean
}
export type PaytoTypeRecord = {
@ -32,6 +34,8 @@ export type PaytoTypeRecord = { @@ -32,6 +34,8 @@ export type PaytoTypeRecord = {
/** Repo-relative path, e.g. `src/assets/payto_logos/ethereum-eth-logo.svg`. */
logoAssetPath?: string
profileUrlTemplate?: string
/** PaytoDialog “Open with” mode; `paypal` uses the PayPal URL resolver only. */
paymentOpen?: 'paypal'
/** Native wallet URI / app deep link (see {@link getPaytoPrimaryOpenUrl}). */
walletOpen?: PaytoWalletOpenRow
authority?: PaytoAuthorityHelp

79
src/lib/payto-wallet-open.test.ts

@ -1,9 +1,12 @@ @@ -1,9 +1,12 @@
import { describe, expect, it } from 'vitest'
import { getPaytoProfileUrl } from '@/lib/payto-registry'
import {
buildBlueWalletWalletHref,
buildPhoenixWalletHref,
buildZeusWalletHref,
filterPaytoPaymentOpenHandlersForDevice,
filterWalletOpenActionsForDevice,
getLightningInvoiceWalletPaymentHandlers,
getPaytoPaymentOpenHandlers,
getPaytoPrimaryOpenUrl,
getPaytoWalletOpenActions,
@ -86,12 +89,13 @@ describe('getPaytoPaymentOpenHandlers', () => { @@ -86,12 +89,13 @@ describe('getPaytoPaymentOpenHandlers', () => {
expect(cash[0].href).toBe('https://cash.app/%24cashtag')
})
it('builds Phoenix bolt12 deep link from offer string', () => {
it('builds Phoenix and Zeus bolt12 deep links from offer string', () => {
const offer = 'lno1pg257enxv4ezqcneype82um50ynhxgrwdajx283qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36ryg488qwlrnzyjczs'
const actions = getPaytoWalletOpenActions('bolt12', offer)
expect(actions).toHaveLength(1)
expect(actions[0].href).toBe(`phoenix:bolt12:${offer}`)
expect(actions[0].mobileOnly).toBe(true)
expect(actions).toHaveLength(2)
expect(actions.map((a) => a.label).sort()).toEqual(['Phoenix', 'Zeus'])
expect(actions.find((a) => a.label === 'Phoenix')?.href).toBe(`phoenix:bolt12:${offer}`)
expect(actions.find((a) => a.label === 'Zeus')?.href).toBe(`zeusln:${offer}`)
})
it('builds Phoenix lightning deep link without pay?uri query', () => {
@ -104,16 +108,21 @@ describe('getPaytoPaymentOpenHandlers', () => { @@ -104,16 +108,21 @@ describe('getPaytoPaymentOpenHandlers', () => {
)
})
it('omits Phoenix for lightning address until BOLT11 is supplied separately', () => {
it('omits mobile lightning wallets for lightning address until BOLT11 is supplied separately', () => {
const handlers = getPaytoPaymentOpenHandlers('lightning', 'user@example.com')
expect(handlers.some((h) => h.openTargetName === 'Phoenix')).toBe(false)
expect(handlers.some((h) => h.openTargetName === 'Zeus')).toBe(false)
expect(handlers.some((h) => h.openTargetName === 'BlueWallet')).toBe(false)
})
it('includes Phoenix on mobile only for bip353', () => {
it('includes Phoenix and Zeus on mobile only for bip353', () => {
const handlers = getPaytoPaymentOpenHandlers('bip353', 'user@example.com')
const phoenix = handlers.find((h) => h.openTargetName === 'Phoenix')
const zeus = handlers.find((h) => h.openTargetName === 'Zeus')
expect(phoenix?.href).toBe('phoenix:lightning:user@example.com')
expect(zeus?.href).toBe('zeusln:lightning:user@example.com')
expect(phoenix?.mobileOnly).toBe(true)
expect(zeus?.mobileOnly).toBe(true)
const prev = navigator.userAgent
Object.defineProperty(navigator, 'userAgent', {
@ -128,6 +137,64 @@ describe('getPaytoPaymentOpenHandlers', () => { @@ -128,6 +137,64 @@ describe('getPaytoPaymentOpenHandlers', () => {
Object.defineProperty(navigator, 'userAgent', { value: prev, configurable: true })
}
})
it('builds Zeus and BlueWallet BOLT11 deep links', () => {
expect(buildZeusWalletHref('lightning', 'lnbc1p0example')).toBe('zeusln:lightning:lnbc1p0example')
expect(buildZeusWalletHref('lightning', 'lightning:lnbc1p0example')).toBe(
'zeusln:lightning:lnbc1p0example'
)
expect(buildBlueWalletWalletHref('lnbc1p0example')).toBe('bluewallet:lightning:lnbc1p0example')
expect(buildBlueWalletWalletHref('lightning:lnbc1p0example')).toBe(
'bluewallet:lightning:lnbc1p0example'
)
})
it('lists Phoenix, Zeus, and BlueWallet for a resolved BOLT11 invoice', () => {
const invoice = 'lnbc1p0example'
const handlers = getLightningInvoiceWalletPaymentHandlers(invoice)
expect(handlers.map((h) => h.openTargetName).sort()).toEqual(['BlueWallet', 'Phoenix', 'Zeus'])
expect(handlers.find((h) => h.openTargetName === 'Phoenix')?.href).toBe(
`phoenix:lightning:${invoice}`
)
expect(handlers.find((h) => h.openTargetName === 'Zeus')?.href).toBe(`zeusln:lightning:${invoice}`)
expect(handlers.find((h) => h.openTargetName === 'BlueWallet')?.href).toBe(
`bluewallet:lightning:${invoice}`
)
})
it('scopes lightning BOLT11 handlers to lightning payto only', () => {
const invoice = 'lnbc1p0example'
const lightning = getPaytoPaymentOpenHandlers('lightning', 'user@example.com', {
bolt11Invoice: invoice
})
expect(lightning.map((h) => h.openTargetName).sort()).toEqual(['BlueWallet', 'Phoenix', 'Zeus'])
expect(lightning.some((h) => h.openTargetName === 'PayPal')).toBe(false)
const moneroAddr =
'4AdUndXHHZ6cfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgvYatVKtQNjUoNcknXV85jSp3wjUGpHbWfnqPm4WjwFGtW'
const moneroWithBolt11 = getPaytoPaymentOpenHandlers('monero', moneroAddr, { bolt11Invoice: invoice })
expect(moneroWithBolt11.some((h) => h.openTargetName === 'Phoenix')).toBe(false)
expect(moneroWithBolt11.some((h) => h.openTargetName === 'Zeus')).toBe(false)
expect(moneroWithBolt11.some((h) => h.openTargetName === 'BlueWallet')).toBe(false)
expect(
getPaytoPaymentOpenHandlers('paypal', 'someuser', { bolt11Invoice: invoice }).map((h) => h.openTargetName)
).toEqual(['PayPal'])
})
it('never mixes fiat web links or lightning wallets across payto types', () => {
const monero = getPaytoPaymentOpenHandlers(
'monero',
'4AdUndXHHZ6cfufTMvppY6JwXNouMBzSkbLYfpAV5Usx3skxNgvYatVKtQNjUoNcknXV85jSp3wjUGpHbWfnqPm4WjwFGtW'
)
expect(monero.some((h) => h.openTargetName === 'PayPal')).toBe(false)
expect(monero.some((h) => h.openTargetName === 'Phoenix')).toBe(false)
expect(monero.some((h) => h.openTargetName === 'Cake Wallet')).toBe(true)
const cash = getPaytoPaymentOpenHandlers('cashme', '$cashtag')
expect(cash).toHaveLength(1)
expect(cash[0].openTargetName).toBe('Cash App')
expect(cash.some((h) => h.openTargetName === 'Phoenix')).toBe(false)
})
})
describe('isPaytoHttpOpenUrl', () => {

232
src/lib/payto-wallet-open.ts

@ -6,12 +6,23 @@ import paytoTypesCatalog from '@/data/payto-types.json' @@ -6,12 +6,23 @@ import paytoTypesCatalog from '@/data/payto-types.json'
import type { PaytoWalletOpenRow } from '@/lib/payto-registry'
import { resolvePaypalPaymentUrl } from '@/lib/payto-paypal-url'
type PaytoBolt11InvoiceOpenConfig = {
paytoType?: string
coinScheme?: string
walletApps: string[]
}
type PaytoTypeRecordWallet = {
label?: string
profileUrlTemplate?: string
paymentOpen?: 'paypal'
walletOpen?: PaytoWalletOpenRow
}
type PaytoWalletCatalogOpenWith = {
bolt11Invoice?: PaytoBolt11InvoiceOpenConfig
}
/** Labeled “open in …” action shown inside {@link PaytoDialog}. */
export type PaytoPaymentOpenHandler = {
id: string
@ -38,6 +49,7 @@ type WalletAppRowJson = { @@ -38,6 +49,7 @@ type WalletAppRowJson = {
}
type PaytoWalletCatalogJson = {
_openWith?: PaytoWalletCatalogOpenWith
aliases?: Record<string, string>
types: Record<string, PaytoTypeRecordWallet>
walletApps?: Record<string, WalletAppRowJson>
@ -110,6 +122,62 @@ export function buildPhoenixWalletHref(coinScheme: string, authority: string): s @@ -110,6 +122,62 @@ export function buildPhoenixWalletHref(coinScheme: string, authority: string): s
return `phoenix:${scheme}:${payload}`
}
/**
* Zeus {@link https://github.com/ZeusLN/zeus/blob/master/utils/AddressUtils.ts processBIP21Uri}:
* strips `zeusln:` then parses `lightning:` / bare `lno1` / `lnbc` payloads via handleAnything.
*/
export function buildZeusWalletHref(coinScheme: string, authority: string): string | null {
const auth = trimAuthority(authority)
if (!auth) return null
const scheme = coinScheme.toLowerCase().trim()
if (!scheme) return null
const payload = auth.replace(/^lightning:/i, '')
if (scheme === 'bolt12' || /^lno1/i.test(payload)) {
return `zeusln:${payload}`
}
return `zeusln:lightning:${payload}`
}
/**
* BlueWallet wiki: wrap BOLT11 / lightning targets as `bluewallet:lightning:…`.
* @see https://github.com/BlueWallet/BlueWallet/wiki/Deeplinking
*/
export function buildBlueWalletWalletHref(authority: string): string | null {
const auth = trimAuthority(authority)
if (!auth) return null
const payload = auth.replace(/^lightning:/i, '')
if (!payload) return null
return `bluewallet:lightning:${payload}`
}
const WALLET_APP_BUILDERS: Record<
string,
(coinScheme: string, authority: string) => string | null
> = {
phoenix: buildPhoenixWalletHref,
zeus: buildZeusWalletHref,
bluewallet: (_coinScheme, authority) => buildBlueWalletWalletHref(authority)
}
function walletAppMeta(appId: string): WalletAppRowJson | undefined {
return walletCatalog.walletApps?.[appId]
}
function paymentOpenHandlerFromHref(
appId: string,
coinScheme: string,
href: string
): PaytoPaymentOpenHandler {
const app = walletAppMeta(appId)
return {
id: `${appId}-${coinScheme}`,
openTargetName: app?.label ?? appId,
href,
isHttp: false,
mobileOnly: app?.mobileOnly !== false
}
}
function resolveWalletAppUri(
appId: string,
paytoType: string,
@ -121,8 +189,9 @@ function resolveWalletAppUri( @@ -121,8 +189,9 @@ function resolveWalletAppUri(
const auth = trimAuthority(authority)
if (!auth) return null
const coinScheme = (row?.scheme ?? paytoType).toLowerCase()
if (appId === 'phoenix') {
return buildPhoenixWalletHref(coinScheme, auth)
const customBuild = WALLET_APP_BUILDERS[appId]
if (customBuild) {
return customBuild(coinScheme, auth)
}
const href = substituteAuthority(
app.uriTemplate.replace(/\{coinScheme\}/g, coinScheme),
@ -138,16 +207,92 @@ export function getPhoenixPaymentOpenHandler( @@ -138,16 +207,92 @@ export function getPhoenixPaymentOpenHandler(
): PaytoPaymentOpenHandler | null {
const href = buildPhoenixWalletHref(coinScheme, authority)
if (!href) return null
return paymentOpenHandlerFromHref('phoenix', coinScheme, href)
}
export function getZeusPaymentOpenHandler(
coinScheme: string,
authority: string
): PaytoPaymentOpenHandler | null {
const href = buildZeusWalletHref(coinScheme, authority)
if (!href) return null
return paymentOpenHandlerFromHref('zeus', coinScheme, href)
}
/** BlueWallet BOLT11 handler (invoice must already be resolved). */
export function getBlueWalletPaymentOpenHandler(
coinScheme: string,
authority: string
): PaytoPaymentOpenHandler | null {
const href = buildBlueWalletWalletHref(authority)
if (!href) return null
return paymentOpenHandlerFromHref('bluewallet', coinScheme, href)
}
function getBolt11InvoiceOpenConfig(): PaytoBolt11InvoiceOpenConfig | undefined {
const cfg = walletCatalog._openWith?.bolt11Invoice
if (!cfg?.walletApps?.length) return undefined
return cfg
}
/** Build “Open with” handlers from catalog `_openWith.bolt11Invoice.walletApps`. */
function getBolt11InvoiceOpenHandlers(invoice: string): PaytoPaymentOpenHandler[] {
const cfg = getBolt11InvoiceOpenConfig()
if (!cfg) return []
const paytoType = getCanonicalPaytoType(cfg.paytoType ?? 'lightning')
const row = getPaytoTypeRecord(paytoType)?.walletOpen
const auth = trimAuthority(invoice)
if (!auth) return []
const handlers: PaytoPaymentOpenHandler[] = []
const seen = new Set<string>()
for (const appId of cfg.walletApps) {
const app = walletCatalog.walletApps?.[appId]
const href = resolveWalletAppUri(appId, paytoType, auth, row)
if (!app || !href || seen.has(href)) continue
seen.add(href)
handlers.push({
id: `${paytoType}-${appId}-bolt11`,
openTargetName: app.label,
href,
isHttp: false,
mobileOnly: app.mobileOnly !== false
})
}
return handlers
}
/** Handlers for a resolved BOLT11 shown after LNURL invoice creation (from {@link payto-types.json}). */
export function getLightningInvoiceWalletPaymentHandlers(
authority: string
): PaytoPaymentOpenHandler[] {
return getBolt11InvoiceOpenHandlers(authority)
}
export type PaytoPaymentOpenContext = {
/** LUD-16 lightning address resolved to BOLT11 in {@link PaytoDialog}. */
bolt11Invoice?: string | null
}
function walletActionToOpenHandler(action: PaytoWalletOpenAction): PaytoPaymentOpenHandler {
return {
id: `phoenix-${coinScheme}`,
openTargetName: walletCatalog.walletApps?.phoenix?.label ?? 'Phoenix',
href,
isHttp: false,
mobileOnly: walletCatalog.walletApps?.phoenix?.mobileOnly !== false
id: action.id,
openTargetName: action.label,
href: action.href,
isHttp: isPaytoHttpOpenUrl(action.href),
mobileOnly: action.mobileOnly
}
}
const PAYTO_TYPES_PHOENIX_REQUIRES_BOLT11 = new Set(['lightning'])
function dedupePaymentOpenHandlers(handlers: PaytoPaymentOpenHandler[]): PaytoPaymentOpenHandler[] {
const seen = new Set<string>()
return handlers.filter((h) => {
if (seen.has(h.href)) return false
seen.add(h.href)
return true
})
}
/**
* Primary browser/OS URL for this payto target (wallet URI or https).
@ -213,56 +358,57 @@ export function filterWalletOpenActionsForDevice( @@ -213,56 +358,57 @@ export function filterWalletOpenActionsForDevice(
}
/**
* Named app/site open targets for PaytoDialog (https + walletApps only).
* Native coin schemes (monero:, bitcoin:, ) are omitted users copy the payto URI instead.
* Open with targets for {@link PaytoDialog} scoped to the active payto type only.
* Native coin schemes (monero:, bitcoin:, ) are omitted; users copy the payto URI instead.
*/
export function getPaytoPaymentOpenHandlers(type: string, authority: string): PaytoPaymentOpenHandler[] {
export function getPaytoPaymentOpenHandlers(
type: string,
authority: string,
context?: PaytoPaymentOpenContext
): PaytoPaymentOpenHandler[] {
const canonical = getCanonicalPaytoType(type)
const record = getPaytoTypeRecord(canonical)
const auth = trimAuthority(authority)
if (!auth || !record) return []
const handlers: PaytoPaymentOpenHandler[] = []
const seen = new Set<string>()
const add = (
id: string,
openTargetName: string,
href: string | null | undefined,
mobileOnly?: boolean
) => {
if (!href || seen.has(href)) return
seen.add(href)
handlers.push({
id,
openTargetName,
href,
isHttp: isPaytoHttpOpenUrl(href),
mobileOnly
})
if (record.paymentOpen === 'paypal') {
const href = resolvePaypalPaymentUrl(auth)
if (href) {
handlers.push({
id: 'paypal',
openTargetName: record.label ?? 'PayPal',
href,
isHttp: true,
mobileOnly: false
})
}
return dedupePaymentOpenHandlers(handlers)
}
if (canonical === 'paypal') {
add('paypal', 'PayPal', resolvePaypalPaymentUrl(auth))
return handlers
const walletRow = record.walletOpen
if (walletRow?.deferWalletAppsUntilBolt11) {
if (context?.bolt11Invoice) {
handlers.push(...getBolt11InvoiceOpenHandlers(context.bolt11Invoice))
}
} else if (walletRow?.walletApps?.length) {
for (const action of getPaytoWalletOpenActions(type, auth)) {
handlers.push(walletActionToOpenHandler(action))
}
}
if (record.profileUrlTemplate) {
add(
`${canonical}-web`,
record.label ?? canonical,
substituteAuthority(record.profileUrlTemplate, encodeURIComponent(auth))
)
}
for (const app of getPaytoWalletOpenActions(type, auth)) {
if (app.label === 'Phoenix' && PAYTO_TYPES_PHOENIX_REQUIRES_BOLT11.has(canonical)) {
continue
}
add(app.id, app.label, app.href, app.mobileOnly)
handlers.push({
id: `${canonical}-web`,
openTargetName: record.label ?? canonical,
href: substituteAuthority(record.profileUrlTemplate, encodeURIComponent(auth)),
isHttp: true,
mobileOnly: false
})
}
return handlers
return dedupePaymentOpenHandlers(handlers)
}
export function filterPaytoPaymentOpenHandlersForDevice(

6
src/lib/payto.ts

@ -28,8 +28,13 @@ export { @@ -28,8 +28,13 @@ export {
} from '@/lib/payto-registry'
export {
buildBlueWalletWalletHref,
buildPhoenixWalletHref,
buildZeusWalletHref,
getBlueWalletPaymentOpenHandler,
getLightningInvoiceWalletPaymentHandlers,
getPhoenixPaymentOpenHandler,
getZeusPaymentOpenHandler,
getPaytoPrimaryOpenUrl,
getPaytoPaymentOpenHandlers,
filterPaytoPaymentOpenHandlersForDevice,
@ -37,6 +42,7 @@ export { @@ -37,6 +42,7 @@ export {
filterWalletOpenActionsForDevice,
isPaytoHttpOpenUrl,
isLikelyMobileWalletUserAgent,
type PaytoPaymentOpenContext,
type PaytoPaymentOpenHandler,
type PaytoWalletOpenAction
} from '@/lib/payto-wallet-open'

Loading…
Cancel
Save