14 changed files with 749 additions and 323 deletions
@ -1,9 +1,9 @@
@@ -1,9 +1,9 @@
|
||||
# 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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -1,61 +1,35 @@
|
||||
/** |
||||
* Explicit Vite `?url` imports so every payto logo is emitted under `/assets/` in production. |
||||
* Keep files in `src/assets/payto_logos/` (not `public/`). |
||||
* Resolves payto logo paths from {@link ../data/payto-types.json} `logoAssetPath` values. |
||||
* 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> = { |
||||
'apple_pay.svg': applePaySvg, |
||||
'bitcoin-cash-bch-logo.svg': bitcoinCashLogo, |
||||
'BNB.png': bnbPng, |
||||
'buymeacoffee.png': buyMeACoffeePng, |
||||
'cashapp.webp': cashappWebp, |
||||
'multi-collateral-dai-dai-logo.svg': daiLogo, |
||||
'dogecoin-doge-logo.svg': dogecoinLogo, |
||||
'ethereum-eth-logo.svg': ethLogo, |
||||
'EurC.png': eurocPng, |
||||
'geyser_fund.webp': geyserWebp, |
||||
'github_sponsors.png': githubSponsorsPng, |
||||
'gofundme.jpeg': gofundmeJpeg, |
||||
'google_pay.jpeg': googlePayJpeg, |
||||
'kickstarter.webp': kickstarterWebp, |
||||
'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 |
||||
const logoModules = import.meta.glob<string>('../assets/payto_logos/*', { |
||||
eager: true, |
||||
query: '?url', |
||||
import: 'default' |
||||
}) |
||||
|
||||
/** Repo-relative path or basename → bundled URL (e.g. `/assets/…`). */ |
||||
const URL_BY_ASSET_PATH = new Map<string, string>() |
||||
|
||||
for (const [modulePath, url] of Object.entries(logoModules)) { |
||||
const filename = modulePath.split('/payto_logos/')[1] |
||||
if (!filename || !url) continue |
||||
const assetPath = `src/assets/payto_logos/${filename}` |
||||
URL_BY_ASSET_PATH.set(assetPath, url) |
||||
URL_BY_ASSET_PATH.set(filename, url) |
||||
} |
||||
|
||||
/** |
||||
* 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 @@
@@ -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