Browse Source

expand payto:// handling

imwald
Silberengel 2 months ago
parent
commit
ae39425055
  1. 2
      docker-entrypoint.sh
  2. 3
      public/payto_logos/apple_pay.svg
  3. BIN
      public/payto_logos/google_pay.jpeg
  4. 12
      src/components/PaytoLink/index.tsx
  5. 92
      src/lib/payto.ts

2
docker-entrypoint.sh

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
# Runtime config for the SPA. NIP-66 monitor runs in a separate cron container; nsec is never sent to the client.
# Optional: NIP66_MONITOR_NPUB (npub of the monitor) can be exposed so the relay info page shows who runs the monitor.
if [ -n "$NIP66_MONITOR_NPUB" ]; then
echo "$NIP66_MONITOR_NPUB" | jq -n -R '{NIP66_MONITOR_NPUB: .}' > /usr/share/nginx/html/config.json
jq -n --arg npub "$NIP66_MONITOR_NPUB" '{NIP66_MONITOR_NPUB: $npub}' > /usr/share/nginx/html/config.json
else
echo '{}' > /usr/share/nginx/html/config.json
fi

3
public/payto_logos/apple_pay.svg

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="814" height="1000">
<path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 660 B

BIN
public/payto_logos/google_pay.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

12
src/components/PaytoLink/index.tsx

@ -4,6 +4,7 @@ import { toast } from 'sonner' @@ -4,6 +4,7 @@ import { toast } from 'sonner'
import {
parsePaytoUri,
buildPaytoUri,
getCanonicalPaytoType,
getPaytoTypeInfo,
getPaytoIconChar,
getPaytoLogoPath,
@ -39,7 +40,11 @@ export default function PaytoLink({ @@ -39,7 +40,11 @@ export default function PaytoLink({
const parsed = paytoUri
? parsePaytoUri(paytoUri)
: typeProp && authorityProp
? { type: typeProp.toLowerCase(), authority: authorityProp, raw: buildPaytoUri(typeProp, authorityProp) }
? {
type: getCanonicalPaytoType(typeProp),
authority: authorityProp,
raw: buildPaytoUri(typeProp, authorityProp)
}
: null
if (!parsed) {
@ -68,6 +73,7 @@ export default function PaytoLink({ @@ -68,6 +73,7 @@ export default function PaytoLink({
}
const displayLabel = info?.label ?? type
const categoryLabel = info?.category ? info.category.charAt(0).toUpperCase() + info.category.slice(1) : ''
const logoPath = getPaytoLogoPath(type)
const iconChar = getPaytoIconChar(type)
const profileUrl = getPaytoProfileUrl(type, authority)
@ -100,7 +106,7 @@ export default function PaytoLink({ @@ -100,7 +106,7 @@ export default function PaytoLink({
'text-primary hover:underline cursor-pointer text-left break-words inline-flex items-center gap-1.5',
className
)}
title={`${displayLabel}: ${t('Open on website')}`}
title={categoryLabel ? `${displayLabel} (${categoryLabel}): ${t('Open on website')}` : `${displayLabel}: ${t('Open on website')}`}
onClick={(e) => e.stopPropagation()}
>
{iconEl}
@ -118,7 +124,7 @@ export default function PaytoLink({ @@ -118,7 +124,7 @@ export default function PaytoLink({
'text-primary hover:underline cursor-pointer text-left break-words inline-flex items-center gap-1.5',
className
)}
title={known ? `${displayLabel}: ${t('Click to open payment options')}` : t('Click to copy address')}
title={known && categoryLabel ? `${displayLabel} (${categoryLabel}): ${t('Click to open payment options')}` : known ? `${displayLabel}: ${t('Click to open payment options')}` : t('Click to copy address')}
>
{iconEl}
{content}

92
src/lib/payto.ts

@ -18,9 +18,10 @@ export function parsePaytoUri(uri: string): ParsedPayto | null { @@ -18,9 +18,10 @@ export function parsePaytoUri(uri: string): ParsedPayto | null {
const trimmed = uri.trim()
const m = /^payto:\/\/([a-z0-9-]+)\/(.+)$/i.exec(trimmed)
if (!m) return null
const type = m[1].toLowerCase()
const typeRaw = m[1].toLowerCase()
const authority = decodeURIComponent(m[2].replace(/\+/g, ' '))
if (!type || !authority) return null
if (!typeRaw || !authority) return null
const type = getCanonicalPaytoType(typeRaw)
return { type, authority, raw: trimmed }
}
@ -36,42 +37,65 @@ export function buildPaytoUri(type: string, authority: string): string { @@ -36,42 +37,65 @@ export function buildPaytoUri(type: string, authority: string): string {
/** Known payment types: NIP-A3 recommended + common extras (crypto, fiat, tipping) */
export const PAYTO_KNOWN_TYPES: Record<
string,
{ label: string; shortLabel?: string; symbol?: string; category: 'crypto' | 'fiat' | 'lightning' | 'tip' }
{ label: string; symbol?: string; category: 'bitcoin' | 'crypto' | 'stablecoin' | 'fiat' | 'lightning' | 'tip' }
> = {
bitcoin: { label: 'Bitcoin', shortLabel: 'BTC', symbol: '₿', category: 'crypto' },
lightning: { label: 'Lightning Network', shortLabel: 'LBTC', symbol: '⚡', category: 'lightning' },
ethereum: { label: 'Ethereum', shortLabel: 'ETH', symbol: 'Ξ', category: 'crypto' },
monero: { label: 'Monero', shortLabel: 'XMR', symbol: 'ɱ', category: 'crypto' },
nano: { label: 'Nano', shortLabel: 'XNO', symbol: 'Ӿ', category: 'crypto' },
cashme: { label: 'Cash App', shortLabel: 'Cash App', symbol: '$', category: 'fiat' },
revolut: { label: 'Revolut', shortLabel: 'Revolut', symbol: '💳', category: 'fiat' },
venmo: { label: 'Venmo', shortLabel: 'Venmo', symbol: '$', category: 'fiat' },
bitcoin: { label: 'Bitcoin', symbol: '₿', category: 'bitcoin' },
sats: { label: 'Satoshis', symbol: '丰', category: 'bitcoin' },
lightning: { label: 'Lightning Network', symbol: '⚡', category: 'lightning' },
ethereum: { label: 'Ethereum', symbol: 'Ξ', category: 'crypto' },
monero: { label: 'Monero', symbol: 'ɱ', category: 'crypto' },
nano: { label: 'Nano', symbol: 'Ӿ', category: 'crypto' },
cashme: { label: 'Cash App', symbol: '$', category: 'fiat' },
revolut: { label: 'Revolut', symbol: '💳', category: 'fiat' },
venmo: { label: 'Venmo', symbol: '$', category: 'fiat' },
// Common crypto
dogecoin: { label: 'Dogecoin', shortLabel: 'DOGE', symbol: 'Ð', category: 'crypto' },
litecoin: { label: 'Litecoin', shortLabel: 'LTC', symbol: 'Ł', category: 'crypto' },
usdt: { label: 'Tether', shortLabel: 'USDT', symbol: '₮', category: 'crypto' },
usdc: { label: 'USD Coin', shortLabel: 'USDC', symbol: '◎', category: 'crypto' },
dai: { label: 'Dai', shortLabel: 'DAI', symbol: '◈', category: 'crypto' },
euroc: { label: 'Euro Coin', shortLabel: 'EUROC', symbol: '€', category: 'crypto' },
solana: { label: 'Solana', shortLabel: 'SOL', symbol: '◎', category: 'crypto' },
'bitcoin-cash': { label: 'Bitcoin Cash', symbol: '₿', category: 'crypto' },
dogecoin: { label: 'Dogecoin', symbol: 'Ð', category: 'crypto' },
litecoin: { label: 'Litecoin', symbol: 'Ł', category: 'crypto' },
usdt: { label: 'Tether', symbol: '₮', category: 'stablecoin' },
usdc: { label: 'USD Coin', symbol: '◎', category: 'stablecoin' },
dai: { label: 'Dai', symbol: '◈', category: 'crypto' },
euroc: { label: 'Euro Coin', symbol: '€', category: 'stablecoin' },
solana: { label: 'Solana', symbol: '◎', category: 'crypto' },
// Tipping / donation
paypal: { label: 'PayPal', shortLabel: 'PayPal', symbol: '💙', category: 'fiat' },
buymeacoffee: { label: 'Buy Me a Coffee', shortLabel: 'Buy Me a Coffee', symbol: '☕', category: 'tip' },
'ko-fi': { label: 'Ko-fi', shortLabel: 'Ko-fi', symbol: '☕', category: 'tip' },
kofi: { label: 'Ko-fi', shortLabel: 'Ko-fi', symbol: '☕', category: 'tip' },
patreon: { label: 'Patreon', shortLabel: 'Patreon', symbol: '🎭', category: 'tip' },
github: { label: 'GitHub Sponsors', shortLabel: 'GitHub', symbol: '🐙', category: 'tip' },
paypal: { label: 'PayPal', symbol: '💙', category: 'fiat' },
buymeacoffee: { label: 'Buy Me a Coffee', symbol: '☕', category: 'tip' },
'ko-fi': { label: 'Ko-fi', symbol: '☕', category: 'tip' },
kofi: { label: 'Ko-fi', symbol: '☕', category: 'tip' },
patreon: { label: 'Patreon', symbol: '🎭', category: 'tip' },
github: { label: 'GitHub Sponsors', symbol: '🐙', category: 'tip' },
// Fiat / wallets
'apple-pay': { label: 'Apple Pay', shortLabel: 'Apple Pay', symbol: '🍎', category: 'fiat' },
'google-pay': { label: 'Google Pay', shortLabel: 'Google Pay', symbol: 'G', category: 'fiat' },
'apple-pay': { label: 'Apple Pay', symbol: '🍎', category: 'fiat' },
'google-pay': { label: 'Google Pay', symbol: 'G', category: 'fiat' },
// Crowdfunding / fundraising
geyser: { label: 'Geyser Fund', shortLabel: 'Geyser', symbol: '⛲', category: 'tip' },
gofundme: { label: 'GoFundMe', shortLabel: 'GoFundMe', symbol: '🎯', category: 'tip' },
kickstarter: { label: 'Kickstarter', shortLabel: 'Kickstarter', symbol: '🚀', category: 'tip' }
geyser: { label: 'Geyser Fund', symbol: '⛲', category: 'tip' },
gofundme: { label: 'GoFundMe', symbol: '🎯', category: 'tip' },
kickstarter: { label: 'Kickstarter', symbol: '🚀', category: 'tip' }
}
/**
* Short labels accepted after payto:// that map to a canonical type.
* e.g. payto://BTC/..., payto://LBTC/..., payto://DOGE/... are recognized as bitcoin, lightning, dogecoin.
*/
export const PAYTO_TYPE_ALIASES: Record<string, string> = {
btc: 'bitcoin',
lbtc: 'lightning',
doge: 'dogecoin',
eth: 'ethereum',
xmr: 'monero',
ltc: 'litecoin',
xno: 'nano',
sol: 'solana',
bch: 'bitcoin-cash'
}
export function getCanonicalPaytoType(type: string): string {
const key = type.toLowerCase().trim()
return PAYTO_TYPE_ALIASES[key] ?? key
}
/** Icon character/symbol for known types; null for unknown (render HelpCircle or ?) */
@ -104,8 +128,8 @@ export const PAYTO_LOGO_FILES: Record<string, string> = { @@ -104,8 +128,8 @@ export const PAYTO_LOGO_FILES: Record<string, string> = {
kofi: 'ko-fi.png',
patreon: 'patreon.png',
github: 'github_sponsors.png',
'apple-pay': 'apple_pay.webp',
'google-pay': 'google_pay.png',
'apple-pay': 'apple_pay.svg',
'google-pay': 'google_pay.jpeg',
geyser: 'geyser_fund.webp',
gofundme: 'gofundme.jpeg',
kickstarter: 'kickstarter.webp'
@ -142,14 +166,14 @@ export function getPaytoLogoPath(type: string): string | null { @@ -142,14 +166,14 @@ export function getPaytoLogoPath(type: string): string | null {
}
export function getPaytoTypeInfo(type: string): (typeof PAYTO_KNOWN_TYPES)[string] | undefined {
return PAYTO_KNOWN_TYPES[type.toLowerCase()]
return PAYTO_KNOWN_TYPES[getCanonicalPaytoType(type)]
}
export function isKnownPaytoType(type: string): boolean {
return type.toLowerCase() in PAYTO_KNOWN_TYPES
return getCanonicalPaytoType(type) in PAYTO_KNOWN_TYPES
}
/** Check if type is lightning (opens Zap flow when pubkey available) */
export function isLightningPaytoType(type: string): boolean {
return type.toLowerCase() === 'lightning'
return getCanonicalPaytoType(type) === 'lightning'
}

Loading…
Cancel
Save