14 changed files with 749 additions and 323 deletions
@ -1,9 +1,9 @@ |
|||||||
# Payto logos |
# Payto logos |
||||||
|
|
||||||
Icons for payment types (crypto, etc.) used by payto links. |
Icons for payment types used by payto links in the app. |
||||||
|
|
||||||
**Supported formats:** SVG, GIF, JPG/JPEG, PNG, WebP, etc. — any format the browser can display in `<img>` is fine. SVG scales best at different sizes. |
**Supported formats:** SVG, GIF, JPG/JPEG, PNG, WebP, etc. |
||||||
|
|
||||||
Filenames are mapped in `src/lib/payto.ts` → `PAYTO_LOGO_FILES`. Use whatever extension the asset has (.svg, .gif, .jpg, .png, …). |
**Catalog:** Each type’s `logoAssetPath` in [`src/data/payto-types.json`](../../data/payto-types.json) points at a file here (e.g. `src/assets/payto_logos/ethereum-eth-logo.svg`). Add or change logos by editing that JSON and placing the image in this folder. |
||||||
|
|
||||||
Bundled via Vite (`import.meta.glob`) so URLs live under `/assets/` and deploy with the app build. |
Bundled via Vite `import.meta.glob` in [`src/lib/payto-logos.ts`](../../lib/payto-logos.ts); runtime URLs are under `/assets/…`. |
||||||
|
|||||||
@ -0,0 +1,78 @@ |
|||||||
|
import { Button } from '@/components/ui/button' |
||||||
|
import { Input } from '@/components/ui/input' |
||||||
|
import { |
||||||
|
Select, |
||||||
|
SelectContent, |
||||||
|
SelectItem, |
||||||
|
SelectTrigger, |
||||||
|
SelectValue |
||||||
|
} from '@/components/ui/select' |
||||||
|
import { |
||||||
|
getCanonicalPaytoType, |
||||||
|
getPaytoAuthorityFieldHelp, |
||||||
|
getPaytoEditorTypeLabel, |
||||||
|
paytoEditorSelectTypes |
||||||
|
} from '@/lib/payto' |
||||||
|
import { Trash2 } from 'lucide-react' |
||||||
|
import { useTranslation } from 'react-i18next' |
||||||
|
|
||||||
|
export type PaymentMethodRowValue = { type: string; authority: string } |
||||||
|
|
||||||
|
type PaymentMethodRowProps = { |
||||||
|
row: PaymentMethodRowValue |
||||||
|
onChange: (row: PaymentMethodRowValue) => void |
||||||
|
onRemove: () => void |
||||||
|
} |
||||||
|
|
||||||
|
export default function PaymentMethodRow({ row, onChange, onRemove }: PaymentMethodRowProps) { |
||||||
|
const { t } = useTranslation() |
||||||
|
const selectTypes = paytoEditorSelectTypes(row.type) |
||||||
|
const canonicalType = getCanonicalPaytoType(row.type || 'lightning') |
||||||
|
const fieldHelp = getPaytoAuthorityFieldHelp(canonicalType) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="flex gap-2 items-start"> |
||||||
|
<Select |
||||||
|
value={canonicalType} |
||||||
|
onValueChange={(type) => onChange({ ...row, type })} |
||||||
|
> |
||||||
|
<SelectTrigger className="w-[11.5rem] shrink-0 font-medium text-sm"> |
||||||
|
<SelectValue placeholder={t('Payment type')} /> |
||||||
|
</SelectTrigger> |
||||||
|
<SelectContent className="max-h-[min(20rem,70vh)]"> |
||||||
|
{selectTypes.map((type) => ( |
||||||
|
<SelectItem key={type} value={type}> |
||||||
|
{getPaytoEditorTypeLabel(type)} |
||||||
|
</SelectItem> |
||||||
|
))} |
||||||
|
</SelectContent> |
||||||
|
</Select> |
||||||
|
|
||||||
|
<div className="flex-1 min-w-0 space-y-1"> |
||||||
|
<Input |
||||||
|
value={row.authority} |
||||||
|
onChange={(e) => onChange({ ...row, authority: e.target.value })} |
||||||
|
placeholder={t(`paytoEditor.placeholder.${canonicalType}`, { |
||||||
|
defaultValue: fieldHelp.placeholder |
||||||
|
})} |
||||||
|
className="font-mono text-sm" |
||||||
|
aria-describedby={`payto-hint-${canonicalType}`} |
||||||
|
/> |
||||||
|
<p id={`payto-hint-${canonicalType}`} className="text-xs text-muted-foreground leading-snug"> |
||||||
|
{t(`paytoEditor.hint.${canonicalType}`, { defaultValue: fieldHelp.hint })} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<Button |
||||||
|
type="button" |
||||||
|
variant="ghost" |
||||||
|
size="icon" |
||||||
|
className="shrink-0 text-muted-foreground hover:text-destructive mt-0.5" |
||||||
|
onClick={onRemove} |
||||||
|
aria-label={t('Remove')} |
||||||
|
> |
||||||
|
<Trash2 className="h-4 w-4" /> |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,376 @@ |
|||||||
|
{ |
||||||
|
"_meta": { |
||||||
|
"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)." |
||||||
|
}, |
||||||
|
"editorOrder": [ |
||||||
|
"lightning", |
||||||
|
"bitcoin", |
||||||
|
"liquid", |
||||||
|
"lbtc", |
||||||
|
"sats", |
||||||
|
"ethereum", |
||||||
|
"monero", |
||||||
|
"litecoin", |
||||||
|
"dogecoin", |
||||||
|
"bitcoin-cash", |
||||||
|
"solana", |
||||||
|
"nano", |
||||||
|
"usdt", |
||||||
|
"usdc", |
||||||
|
"dai", |
||||||
|
"euroc", |
||||||
|
"paypal", |
||||||
|
"cashme", |
||||||
|
"venmo", |
||||||
|
"revolut", |
||||||
|
"buymeacoffee", |
||||||
|
"ko-fi", |
||||||
|
"patreon", |
||||||
|
"github", |
||||||
|
"geyser", |
||||||
|
"gofundme", |
||||||
|
"kickstarter", |
||||||
|
"apple-pay", |
||||||
|
"google-pay" |
||||||
|
], |
||||||
|
"genericAuthorityHelp": { |
||||||
|
"placeholder": "payment target", |
||||||
|
"hint": "Authority segment of payto://<type>/<this value> (address, username, or ID)" |
||||||
|
}, |
||||||
|
"aliases": { |
||||||
|
"btc": "bitcoin", |
||||||
|
"doge": "dogecoin", |
||||||
|
"eth": "ethereum", |
||||||
|
"xmr": "monero", |
||||||
|
"ltc": "litecoin", |
||||||
|
"xno": "nano", |
||||||
|
"sol": "solana", |
||||||
|
"bch": "bitcoin-cash" |
||||||
|
}, |
||||||
|
"types": { |
||||||
|
"bitcoin": { |
||||||
|
"label": "Bitcoin", |
||||||
|
"symbol": "₿", |
||||||
|
"category": "bitcoin", |
||||||
|
"authority": { |
||||||
|
"placeholder": "bc1q…", |
||||||
|
"hint": "On-chain Bitcoin address (Bech32 bc1… preferred)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"liquid": { |
||||||
|
"label": "Liquid", |
||||||
|
"symbol": "⛓", |
||||||
|
"category": "bitcoin-layer", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/LBTC.svg", |
||||||
|
"authority": { |
||||||
|
"placeholder": "VJL… or bc1q… on Liquid", |
||||||
|
"hint": "Liquid network address (confidential or explicit)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"lbtc": { |
||||||
|
"label": "Liquid Bitcoin", |
||||||
|
"symbol": "₿", |
||||||
|
"category": "bitcoin-layer", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/LBTC.svg", |
||||||
|
"authority": { |
||||||
|
"placeholder": "VJL…", |
||||||
|
"hint": "Liquid Bitcoin (L-BTC) receiving address" |
||||||
|
} |
||||||
|
}, |
||||||
|
"sats": { |
||||||
|
"label": "Satoshis", |
||||||
|
"symbol": "丰", |
||||||
|
"category": "bitcoin", |
||||||
|
"authority": { |
||||||
|
"placeholder": "bc1q… or lightning address", |
||||||
|
"hint": "Satoshis payment target (same formats as Bitcoin / Lightning)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"lightning": { |
||||||
|
"label": "Lightning Network", |
||||||
|
"symbol": "⚡", |
||||||
|
"category": "bitcoin-layer", |
||||||
|
"authority": { |
||||||
|
"placeholder": "user@getalby.com", |
||||||
|
"hint": "Lightning address (LUD-16): name@domain — not a BOLT11 invoice" |
||||||
|
} |
||||||
|
}, |
||||||
|
"ethereum": { |
||||||
|
"label": "Ethereum", |
||||||
|
"symbol": "Ξ", |
||||||
|
"category": "crypto", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/ethereum-eth-logo.svg", |
||||||
|
"authority": { |
||||||
|
"placeholder": "0x…", |
||||||
|
"hint": "Ethereum address (0x + 40 hex characters)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"monero": { |
||||||
|
"label": "Monero", |
||||||
|
"symbol": "ɱ", |
||||||
|
"category": "crypto", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/Monero.png", |
||||||
|
"authority": { |
||||||
|
"placeholder": "4… or 8…", |
||||||
|
"hint": "Monero primary address (starts with 4 or 8)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"nano": { |
||||||
|
"label": "Nano", |
||||||
|
"symbol": "Ӿ", |
||||||
|
"category": "crypto", |
||||||
|
"authority": { |
||||||
|
"placeholder": "nano_…", |
||||||
|
"hint": "Nano account address (nano_ prefix)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"cashme": { |
||||||
|
"label": "Cash App", |
||||||
|
"symbol": "$", |
||||||
|
"category": "fiat", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/cashapp.webp", |
||||||
|
"profileUrlTemplate": "https://cash.app/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "$cashtag", |
||||||
|
"hint": "Cash App $cashtag or phone-linked ID" |
||||||
|
} |
||||||
|
}, |
||||||
|
"revolut": { |
||||||
|
"label": "Revolut", |
||||||
|
"symbol": "💳", |
||||||
|
"category": "fiat", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/revolut.webp", |
||||||
|
"profileUrlTemplate": "https://revolut.me/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "username", |
||||||
|
"hint": "Revolut.me username" |
||||||
|
} |
||||||
|
}, |
||||||
|
"venmo": { |
||||||
|
"label": "Venmo", |
||||||
|
"symbol": "$", |
||||||
|
"category": "fiat", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/venmo.png", |
||||||
|
"profileUrlTemplate": "https://venmo.com/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "@username", |
||||||
|
"hint": "Venmo username (with or without @)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"bitcoin-cash": { |
||||||
|
"label": "Bitcoin Cash", |
||||||
|
"symbol": "₿", |
||||||
|
"category": "crypto", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/bitcoin-cash-bch-logo.svg", |
||||||
|
"authority": { |
||||||
|
"placeholder": "bitcoincash:… or q…", |
||||||
|
"hint": "Bitcoin Cash address (CashAddr or legacy)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"dogecoin": { |
||||||
|
"label": "Dogecoin", |
||||||
|
"symbol": "Ð", |
||||||
|
"category": "crypto", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/dogecoin-doge-logo.svg", |
||||||
|
"authority": { |
||||||
|
"placeholder": "D…", |
||||||
|
"hint": "Dogecoin address (usually starts with D)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"litecoin": { |
||||||
|
"label": "Litecoin", |
||||||
|
"symbol": "Ł", |
||||||
|
"category": "crypto", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/Litecoin.png", |
||||||
|
"authority": { |
||||||
|
"placeholder": "ltc1q… or L… or M…", |
||||||
|
"hint": "Litecoin address" |
||||||
|
} |
||||||
|
}, |
||||||
|
"usdt": { |
||||||
|
"label": "Tether", |
||||||
|
"symbol": "₮", |
||||||
|
"category": "stablecoin", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/tether-usdt-logo.svg", |
||||||
|
"authority": { |
||||||
|
"placeholder": "0x… or T…", |
||||||
|
"hint": "USDT address — include network if needed (ERC-20, TRC-20, etc.)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"usdc": { |
||||||
|
"label": "USD Coin", |
||||||
|
"symbol": "◎", |
||||||
|
"category": "stablecoin", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/usd-coin-usdc-logo.svg", |
||||||
|
"authority": { |
||||||
|
"placeholder": "0x…", |
||||||
|
"hint": "USDC address — specify chain in notes if not obvious" |
||||||
|
} |
||||||
|
}, |
||||||
|
"dai": { |
||||||
|
"label": "Dai", |
||||||
|
"symbol": "◈", |
||||||
|
"category": "crypto", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/multi-collateral-dai-dai-logo.svg", |
||||||
|
"authority": { |
||||||
|
"placeholder": "0x…", |
||||||
|
"hint": "Dai (ERC-20) wallet address" |
||||||
|
} |
||||||
|
}, |
||||||
|
"euroc": { |
||||||
|
"label": "Euro Coin", |
||||||
|
"symbol": "€", |
||||||
|
"category": "stablecoin", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/EurC.png", |
||||||
|
"authority": { |
||||||
|
"placeholder": "0x…", |
||||||
|
"hint": "Euro Coin (EURC) wallet address" |
||||||
|
} |
||||||
|
}, |
||||||
|
"solana": { |
||||||
|
"label": "Solana", |
||||||
|
"symbol": "◎", |
||||||
|
"category": "crypto", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/solana.png", |
||||||
|
"authority": { |
||||||
|
"placeholder": "Base58 pubkey…", |
||||||
|
"hint": "Solana wallet address (base58, 32–44 characters)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"paypal": { |
||||||
|
"label": "PayPal", |
||||||
|
"symbol": "💙", |
||||||
|
"category": "fiat", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/paypal.webp", |
||||||
|
"profileUrlTemplate": "https://paypal.me/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "username", |
||||||
|
"hint": "PayPal.me username (without paypal.me/)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"buymeacoffee": { |
||||||
|
"label": "Buy Me a Coffee", |
||||||
|
"symbol": "☕", |
||||||
|
"category": "tip", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/buymeacoffee.png", |
||||||
|
"profileUrlTemplate": "https://buymeacoffee.com/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "username", |
||||||
|
"hint": "Buy Me a Coffee page slug" |
||||||
|
} |
||||||
|
}, |
||||||
|
"ko-fi": { |
||||||
|
"label": "Ko-fi", |
||||||
|
"symbol": "☕", |
||||||
|
"category": "tip", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/ko-fi.png", |
||||||
|
"profileUrlTemplate": "https://ko-fi.com/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "username", |
||||||
|
"hint": "Ko-fi profile name" |
||||||
|
} |
||||||
|
}, |
||||||
|
"kofi": { |
||||||
|
"label": "Ko-fi", |
||||||
|
"symbol": "☕", |
||||||
|
"category": "tip", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/ko-fi.png", |
||||||
|
"profileUrlTemplate": "https://ko-fi.com/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "username", |
||||||
|
"hint": "Ko-fi profile name" |
||||||
|
} |
||||||
|
}, |
||||||
|
"patreon": { |
||||||
|
"label": "Patreon", |
||||||
|
"symbol": "🎭", |
||||||
|
"category": "tip", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/patreon.png", |
||||||
|
"profileUrlTemplate": "https://patreon.com/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "creator", |
||||||
|
"hint": "Patreon creator / page slug" |
||||||
|
} |
||||||
|
}, |
||||||
|
"github": { |
||||||
|
"label": "GitHub Sponsors", |
||||||
|
"symbol": "🐙", |
||||||
|
"category": "tip", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/github_sponsors.png", |
||||||
|
"profileUrlTemplate": "https://github.com/sponsors/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "username", |
||||||
|
"hint": "GitHub Sponsors username" |
||||||
|
} |
||||||
|
}, |
||||||
|
"apple-pay": { |
||||||
|
"label": "Apple Pay", |
||||||
|
"symbol": "🍎", |
||||||
|
"category": "fiat", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/apple_pay.svg", |
||||||
|
"authority": { |
||||||
|
"placeholder": "phone or email", |
||||||
|
"hint": "Apple Pay contact (phone number or email)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"google-pay": { |
||||||
|
"label": "Google Pay", |
||||||
|
"symbol": "G", |
||||||
|
"category": "fiat", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/google_pay.jpeg", |
||||||
|
"authority": { |
||||||
|
"placeholder": "phone or email", |
||||||
|
"hint": "Google Pay contact (phone number or email)" |
||||||
|
} |
||||||
|
}, |
||||||
|
"geyser": { |
||||||
|
"label": "Geyser Fund", |
||||||
|
"symbol": "⛲", |
||||||
|
"category": "tip", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/geyser_fund.webp", |
||||||
|
"profileUrlTemplate": "https://geyser.fund/project/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "project-slug", |
||||||
|
"hint": "Geyser Fund project identifier" |
||||||
|
} |
||||||
|
}, |
||||||
|
"gofundme": { |
||||||
|
"label": "GoFundMe", |
||||||
|
"symbol": "🎯", |
||||||
|
"category": "tip", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/gofundme.jpeg", |
||||||
|
"profileUrlTemplate": "https://www.gofundme.com/f/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "campaign-slug", |
||||||
|
"hint": "GoFundMe campaign path segment" |
||||||
|
} |
||||||
|
}, |
||||||
|
"kickstarter": { |
||||||
|
"label": "Kickstarter", |
||||||
|
"symbol": "🚀", |
||||||
|
"category": "tip", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/kickstarter.webp", |
||||||
|
"profileUrlTemplate": "https://www.kickstarter.com/projects/{authority}", |
||||||
|
"authority": { |
||||||
|
"placeholder": "project-slug", |
||||||
|
"hint": "Kickstarter project slug" |
||||||
|
} |
||||||
|
}, |
||||||
|
"bnb": { |
||||||
|
"label": "BNB", |
||||||
|
"category": "crypto", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/BNB.png" |
||||||
|
}, |
||||||
|
"tron": { |
||||||
|
"label": "Tron", |
||||||
|
"category": "crypto", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/Tron.png" |
||||||
|
}, |
||||||
|
"xrp": { |
||||||
|
"label": "XRP", |
||||||
|
"category": "crypto", |
||||||
|
"logoAssetPath": "src/assets/payto_logos/XRP.gif" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
import { describe, expect, it } from 'vitest' |
||||||
|
import { getPaytoAuthorityFieldHelp, getPaytoLogoPath, paytoEditorSelectTypes } from './payto-registry' |
||||||
|
|
||||||
|
describe('getPaytoAuthorityFieldHelp', () => { |
||||||
|
it('returns lightning-specific hint', () => { |
||||||
|
const help = getPaytoAuthorityFieldHelp('lightning') |
||||||
|
expect(help.placeholder).toContain('@') |
||||||
|
expect(help.hint.toLowerCase()).toContain('lud') |
||||||
|
}) |
||||||
|
|
||||||
|
it('falls back for unknown types', () => { |
||||||
|
const help = getPaytoAuthorityFieldHelp('custom-coin') |
||||||
|
expect(help.hint).toContain('payto://') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('paytoEditorSelectTypes', () => { |
||||||
|
it('appends custom type not in curated list', () => { |
||||||
|
const types = paytoEditorSelectTypes('custom-coin') |
||||||
|
expect(types[0]).toBe('lightning') |
||||||
|
expect(types).toContain('custom-coin') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('getPaytoLogoPath', () => { |
||||||
|
it('resolves ethereum logo from catalog asset path', () => { |
||||||
|
const url = getPaytoLogoPath('ethereum') |
||||||
|
expect(url).toBeTruthy() |
||||||
|
expect(url!.length).toBeGreaterThan(10) |
||||||
|
}) |
||||||
|
}) |
||||||
@ -1,61 +1,35 @@ |
|||||||
/** |
/** |
||||||
* Explicit Vite `?url` imports so every payto logo is emitted under `/assets/` in production. |
* Resolves payto logo paths from {@link ../data/payto-types.json} `logoAssetPath` values. |
||||||
* Keep files in `src/assets/payto_logos/` (not `public/`). |
* All files under `src/assets/payto_logos/` are bundled via Vite `import.meta.glob`. |
||||||
*/ |
*/ |
||||||
import applePaySvg from '../assets/payto_logos/apple_pay.svg?url' |
|
||||||
import bitcoinCashLogo from '../assets/payto_logos/bitcoin-cash-bch-logo.svg?url' |
|
||||||
import bnbPng from '../assets/payto_logos/BNB.png?url' |
|
||||||
import buyMeACoffeePng from '../assets/payto_logos/buymeacoffee.png?url' |
|
||||||
import cashappWebp from '../assets/payto_logos/cashapp.webp?url' |
|
||||||
import daiLogo from '../assets/payto_logos/multi-collateral-dai-dai-logo.svg?url' |
|
||||||
import dogecoinLogo from '../assets/payto_logos/dogecoin-doge-logo.svg?url' |
|
||||||
import ethLogo from '../assets/payto_logos/ethereum-eth-logo.svg?url' |
|
||||||
import eurocPng from '../assets/payto_logos/EurC.png?url' |
|
||||||
import geyserWebp from '../assets/payto_logos/geyser_fund.webp?url' |
|
||||||
import githubSponsorsPng from '../assets/payto_logos/github_sponsors.png?url' |
|
||||||
import gofundmeJpeg from '../assets/payto_logos/gofundme.jpeg?url' |
|
||||||
import googlePayJpeg from '../assets/payto_logos/google_pay.jpeg?url' |
|
||||||
import kickstarterWebp from '../assets/payto_logos/kickstarter.webp?url' |
|
||||||
import kofiPng from '../assets/payto_logos/ko-fi.png?url' |
|
||||||
import lbtcSvg from '../assets/payto_logos/LBTC.svg?url' |
|
||||||
import litecoinPng from '../assets/payto_logos/Litecoin.png?url' |
|
||||||
import moneroPng from '../assets/payto_logos/Monero.png?url' |
|
||||||
import patreonPng from '../assets/payto_logos/patreon.png?url' |
|
||||||
import paypalWebp from '../assets/payto_logos/paypal.webp?url' |
|
||||||
import revolutWebp from '../assets/payto_logos/revolut.webp?url' |
|
||||||
import solanaPng from '../assets/payto_logos/solana.png?url' |
|
||||||
import tetherLogo from '../assets/payto_logos/tether-usdt-logo.svg?url' |
|
||||||
import tronPng from '../assets/payto_logos/Tron.png?url' |
|
||||||
import usdcLogo from '../assets/payto_logos/usd-coin-usdc-logo.svg?url' |
|
||||||
import venmoPng from '../assets/payto_logos/venmo.png?url' |
|
||||||
import xrpGif from '../assets/payto_logos/XRP.gif?url' |
|
||||||
|
|
||||||
export const PAYTO_LOGO_URL_BY_FILENAME: Record<string, string> = { |
const logoModules = import.meta.glob<string>('../assets/payto_logos/*', { |
||||||
'apple_pay.svg': applePaySvg, |
eager: true, |
||||||
'bitcoin-cash-bch-logo.svg': bitcoinCashLogo, |
query: '?url', |
||||||
'BNB.png': bnbPng, |
import: 'default' |
||||||
'buymeacoffee.png': buyMeACoffeePng, |
}) |
||||||
'cashapp.webp': cashappWebp, |
|
||||||
'multi-collateral-dai-dai-logo.svg': daiLogo, |
/** Repo-relative path or basename → bundled URL (e.g. `/assets/…`). */ |
||||||
'dogecoin-doge-logo.svg': dogecoinLogo, |
const URL_BY_ASSET_PATH = new Map<string, string>() |
||||||
'ethereum-eth-logo.svg': ethLogo, |
|
||||||
'EurC.png': eurocPng, |
for (const [modulePath, url] of Object.entries(logoModules)) { |
||||||
'geyser_fund.webp': geyserWebp, |
const filename = modulePath.split('/payto_logos/')[1] |
||||||
'github_sponsors.png': githubSponsorsPng, |
if (!filename || !url) continue |
||||||
'gofundme.jpeg': gofundmeJpeg, |
const assetPath = `src/assets/payto_logos/${filename}` |
||||||
'google_pay.jpeg': googlePayJpeg, |
URL_BY_ASSET_PATH.set(assetPath, url) |
||||||
'kickstarter.webp': kickstarterWebp, |
URL_BY_ASSET_PATH.set(filename, url) |
||||||
'ko-fi.png': kofiPng, |
|
||||||
'LBTC.svg': lbtcSvg, |
|
||||||
'Litecoin.png': litecoinPng, |
|
||||||
'Monero.png': moneroPng, |
|
||||||
'patreon.png': patreonPng, |
|
||||||
'paypal.webp': paypalWebp, |
|
||||||
'revolut.webp': revolutWebp, |
|
||||||
'solana.png': solanaPng, |
|
||||||
'tether-usdt-logo.svg': tetherLogo, |
|
||||||
'Tron.png': tronPng, |
|
||||||
'usd-coin-usdc-logo.svg': usdcLogo, |
|
||||||
'venmo.png': venmoPng, |
|
||||||
'XRP.gif': xrpGif |
|
||||||
} |
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolve a catalog `logoAssetPath` (or legacy basename) to the app asset URL. |
||||||
|
*/ |
||||||
|
export function resolvePaytoLogoAssetPath(assetPathOrFilename: string | undefined): string | null { |
||||||
|
if (!assetPathOrFilename?.trim()) return null |
||||||
|
const key = assetPathOrFilename.trim() |
||||||
|
return URL_BY_ASSET_PATH.get(key) ?? URL_BY_ASSET_PATH.get(key.split('/').pop() ?? '') ?? null |
||||||
|
} |
||||||
|
|
||||||
|
/** @deprecated Use {@link resolvePaytoLogoAssetPath} with catalog `logoAssetPath`. */ |
||||||
|
export const PAYTO_LOGO_URL_BY_FILENAME: Record<string, string> = Object.fromEntries( |
||||||
|
[...URL_BY_ASSET_PATH.entries()].filter(([k]) => !k.startsWith('src/')) |
||||||
|
) |
||||||
|
|||||||
@ -0,0 +1,111 @@ |
|||||||
|
/** |
||||||
|
* Loads payto type metadata from {@link ../data/payto-types.json}. |
||||||
|
* Edit that JSON to add types, editor order, hints, logos, and profile URL templates. |
||||||
|
*/ |
||||||
|
|
||||||
|
import paytoTypesCatalog from '@/data/payto-types.json' |
||||||
|
import { resolvePaytoLogoAssetPath } from '@/lib/payto-logos' |
||||||
|
|
||||||
|
export type PaytoCategory = 'bitcoin' | 'bitcoin-layer' | 'crypto' | 'stablecoin' | 'fiat' | 'tip' |
||||||
|
|
||||||
|
export type PaytoAuthorityHelp = { |
||||||
|
placeholder: string |
||||||
|
hint: string |
||||||
|
} |
||||||
|
|
||||||
|
export type PaytoTypeRecord = { |
||||||
|
label: string |
||||||
|
symbol?: string |
||||||
|
category: PaytoCategory |
||||||
|
/** Repo-relative path, e.g. `src/assets/payto_logos/ethereum-eth-logo.svg`. */ |
||||||
|
logoAssetPath?: string |
||||||
|
profileUrlTemplate?: string |
||||||
|
authority?: PaytoAuthorityHelp |
||||||
|
} |
||||||
|
|
||||||
|
type PaytoTypesCatalogJson = { |
||||||
|
editorOrder: string[] |
||||||
|
genericAuthorityHelp: PaytoAuthorityHelp |
||||||
|
aliases: Record<string, string> |
||||||
|
types: Record<string, PaytoTypeRecord> |
||||||
|
} |
||||||
|
|
||||||
|
const catalog = paytoTypesCatalog as PaytoTypesCatalogJson |
||||||
|
|
||||||
|
export const PAYTO_EDITOR_TYPE_ORDER: readonly string[] = catalog.editorOrder |
||||||
|
|
||||||
|
const GENERIC_AUTHORITY_HELP: PaytoAuthorityHelp = catalog.genericAuthorityHelp |
||||||
|
|
||||||
|
const PAYTO_TYPE_ALIASES: Record<string, string> = catalog.aliases |
||||||
|
|
||||||
|
const PAYTO_TYPES: Record<string, PaytoTypeRecord> = catalog.types |
||||||
|
|
||||||
|
/** UI summary per canonical type (label, symbol, category). */ |
||||||
|
export const PAYTO_KNOWN_TYPES: Record< |
||||||
|
string, |
||||||
|
{ label: string; symbol?: string; category: PaytoCategory } |
||||||
|
> = Object.fromEntries( |
||||||
|
Object.entries(PAYTO_TYPES).map(([id, row]) => [ |
||||||
|
id, |
||||||
|
{ label: row.label, symbol: row.symbol, category: row.category } |
||||||
|
]) |
||||||
|
) |
||||||
|
|
||||||
|
export function getCanonicalPaytoType(type: string): string { |
||||||
|
const key = type.toLowerCase().trim() |
||||||
|
return PAYTO_TYPE_ALIASES[key] ?? key |
||||||
|
} |
||||||
|
|
||||||
|
export function getPaytoTypeRecord(type: string): PaytoTypeRecord | undefined { |
||||||
|
return PAYTO_TYPES[getCanonicalPaytoType(type)] |
||||||
|
} |
||||||
|
|
||||||
|
export function getPaytoTypeInfo(type: string): (typeof PAYTO_KNOWN_TYPES)[string] | undefined { |
||||||
|
return PAYTO_KNOWN_TYPES[getCanonicalPaytoType(type)] |
||||||
|
} |
||||||
|
|
||||||
|
export function isKnownPaytoType(type: string): boolean { |
||||||
|
return getCanonicalPaytoType(type) in PAYTO_KNOWN_TYPES |
||||||
|
} |
||||||
|
|
||||||
|
export function getPaytoAuthorityFieldHelp(type: string): PaytoAuthorityHelp { |
||||||
|
const row = getPaytoTypeRecord(type) |
||||||
|
return row?.authority ?? GENERIC_AUTHORITY_HELP |
||||||
|
} |
||||||
|
|
||||||
|
export function getPaytoEditorTypeLabel(type: string): string { |
||||||
|
return getPaytoTypeInfo(type)?.label ?? getCanonicalPaytoType(type) |
||||||
|
} |
||||||
|
|
||||||
|
/** Dropdown options: catalog order plus the row's type when not listed. */ |
||||||
|
export function paytoEditorSelectTypes(currentType: string): string[] { |
||||||
|
const key = getCanonicalPaytoType(currentType) |
||||||
|
const ordered = new Set(PAYTO_EDITOR_TYPE_ORDER) |
||||||
|
const out = [...PAYTO_EDITOR_TYPE_ORDER] |
||||||
|
if (key && !ordered.has(key)) out.push(key) |
||||||
|
return out |
||||||
|
} |
||||||
|
|
||||||
|
/** Bundled asset URL for `<img src>` (resolved from catalog `logoAssetPath`). */ |
||||||
|
export function getPaytoLogoPath(type: string): string | null { |
||||||
|
return resolvePaytoLogoAssetPath(getPaytoTypeRecord(type)?.logoAssetPath) |
||||||
|
} |
||||||
|
|
||||||
|
/** Same as {@link getPaytoLogoPath}; alias for callers that expect a URL field name. */ |
||||||
|
export function getPaytoLogoUrl(type: string): string | null { |
||||||
|
return getPaytoLogoPath(type) |
||||||
|
} |
||||||
|
|
||||||
|
export function getPaytoProfileUrl(type: string, authority: string): string | null { |
||||||
|
const template = getPaytoTypeRecord(type)?.profileUrlTemplate |
||||||
|
if (!template || !authority.trim()) return null |
||||||
|
return template.replace('{authority}', encodeURIComponent(authority.trim())) |
||||||
|
} |
||||||
|
|
||||||
|
export function getPaytoIconChar(type: string): string | null { |
||||||
|
return getPaytoTypeRecord(type)?.symbol ?? null |
||||||
|
} |
||||||
|
|
||||||
|
export function isLightningPaytoType(type: string): boolean { |
||||||
|
return getCanonicalPaytoType(type) === 'lightning' |
||||||
|
} |
||||||
Loading…
Reference in new issue