diff --git a/src/components/PaymentMethodsSection/index.tsx b/src/components/PaymentMethodsSection/index.tsx index 2244cf66..c05ceac6 100644 --- a/src/components/PaymentMethodsSection/index.tsx +++ b/src/components/PaymentMethodsSection/index.tsx @@ -1,14 +1,95 @@ import PaytoLink from '@/components/PaytoLink' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from '@/components/ui/collapsible' +import { usePreferredPaytoCategory } from '@/hooks/usePreferredPaytoCategory' import type { PaymentMethodGroup } from '@/lib/merge-payment-methods' +import { partitionPaymentGroupsByPreferredCategory } from '@/lib/payto-category-display' import { PRIMARY_LINK_HOVER_CLASS } from '@/lib/link-styles' import { cn } from '@/lib/utils' -import { Copy } from 'lucide-react' +import { ChevronDown, Copy } from 'lucide-react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { NostrEvent } from 'nostr-tools' import type { PostPaymentContext } from '@/lib/post-payment-context' +function PaymentMethodGroupsList({ + groups, + recipientPubkey, + referencedEvent, + offerTipNoticeOnClose, + onPostPaymentRequest +}: { + groups: PaymentMethodGroup[] + recipientPubkey?: string + referencedEvent?: NostrEvent + offerTipNoticeOnClose: boolean + onPostPaymentRequest?: (context: PostPaymentContext) => void +}) { + const { t } = useTranslation() + + return ( + <> + {groups.map((group, groupIdx) => ( +
+
{group.displayType}
+
+ {group.methods.map((method, idx) => ( +
+ {method.authority && ( +
+ + {method.authority} + + +
+ )} + {(method.currency || + (method.minAmount !== undefined && method.maxAmount !== undefined)) && ( +
+ {method.currency && ({method.currency})} + {method.minAmount !== undefined && method.maxAmount !== undefined && ( + + {method.minAmount}-{method.maxAmount} + + )} +
+ )} +
+ ))} +
+
+ ))} + + ) +} + export default function PaymentMethodsSection({ groups, recipientPubkey, @@ -29,67 +110,47 @@ export default function PaymentMethodsSection({ className?: string }) { const { t } = useTranslation() + const { preferredPaytoCategory } = usePreferredPaytoCategory() + const [otherCategoriesOpen, setOtherCategoriesOpen] = useState(false) + + const { preferredGroups, otherGroups } = useMemo( + () => partitionPaymentGroupsByPreferredCategory(groups, preferredPaytoCategory), + [groups, preferredPaytoCategory] + ) if (groups.length === 0) return null + const listProps = { + recipientPubkey, + referencedEvent, + offerTipNoticeOnClose, + onPostPaymentRequest + } + return (
{title ?? t('Payment Methods')}
- {groups.map((group, groupIdx) => ( -
-
{group.displayType}
-
- {group.methods.map((method, idx) => ( -
- {method.authority && ( -
- - {method.authority} - - -
- )} - {(method.currency || - (method.minAmount !== undefined && method.maxAmount !== undefined)) && ( -
- {method.currency && ({method.currency})} - {method.minAmount !== undefined && method.maxAmount !== undefined && ( - - {method.minAmount}-{method.maxAmount} - - )} -
- )} -
- ))} -
-
- ))} + + {otherGroups.length > 0 && ( + + + + + {t('Other payment categories ({{count}})', { count: otherGroups.length })} + + + + + + + )}
) diff --git a/src/constants.ts b/src/constants.ts index de9803a5..b1f61d83 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -334,6 +334,8 @@ export const StorageKey = { NOTIFICATION_TYPE: 'notificationType', DEFAULT_ZAP_SATS: 'defaultZapSats', DEFAULT_ZAP_COMMENT: 'defaultZapComment', + /** Expanded payto category on payment method lists; empty = show all. */ + PREFERRED_PAYTO_CATEGORY: 'preferredPaytoCategory', QUICK_ZAP: 'quickZap', INCLUDE_PUBLIC_ZAP_RECEIPT: 'includePublicZapReceipt', /** Per-pubkey ms timestamps: last full network hydrate (see ACCOUNT_SESSION_NETWORK_HYDRATE_MIN_INTERVAL_MS). */ diff --git a/src/hooks/usePreferredPaytoCategory.ts b/src/hooks/usePreferredPaytoCategory.ts new file mode 100644 index 00000000..edd6cdf2 --- /dev/null +++ b/src/hooks/usePreferredPaytoCategory.ts @@ -0,0 +1,26 @@ +import { useCallback, useEffect, useState } from 'react' + +import type { PaytoCategory } from '@/lib/payto-registry' +import storage from '@/services/local-storage.service' + +export const PREFERRED_PAYTO_CATEGORY_CHANGED_EVENT = 'preferredPaytoCategoryChanged' + +export function usePreferredPaytoCategory() { + const [preferredPaytoCategory, setPreferredPaytoCategoryState] = useState( + () => storage.getPreferredPaytoCategory() + ) + + useEffect(() => { + const sync = () => setPreferredPaytoCategoryState(storage.getPreferredPaytoCategory()) + window.addEventListener(PREFERRED_PAYTO_CATEGORY_CHANGED_EVENT, sync) + return () => window.removeEventListener(PREFERRED_PAYTO_CATEGORY_CHANGED_EVENT, sync) + }, []) + + const setPreferredPaytoCategory = useCallback((category: PaytoCategory | null) => { + storage.setPreferredPaytoCategory(category) + setPreferredPaytoCategoryState(category) + window.dispatchEvent(new Event(PREFERRED_PAYTO_CATEGORY_CHANGED_EVENT)) + }, []) + + return { preferredPaytoCategory, setPreferredPaytoCategory } +} diff --git a/src/i18n/locales/cs.ts b/src/i18n/locales/cs.ts index 7aad6371..0bc1801e 100644 --- a/src/i18n/locales/cs.ts +++ b/src/i18n/locales/cs.ts @@ -669,6 +669,18 @@ export default { 'Default zap comment': 'Default zap comment', 'Lightning Address (or LNURL)': 'Lightning Address (or LNURL)', 'Quick zap': 'Quick zap', + 'Preferred payto category': 'Preferred payto category', + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.': + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.', + 'Show all categories': 'Show all categories', + 'Other payment categories ({{count}})': 'Other payment categories ({{count}})', + 'paytoCategory.bitcoin': 'Bitcoin', + 'paytoCategory.bitcoin-layer': 'Bitcoin layer', + 'paytoCategory.monero': 'Monero', + 'paytoCategory.crypto': 'Crypto', + 'paytoCategory.stablecoin': 'Stablecoin', + 'paytoCategory.fiat': 'Fiat', + 'paytoCategory.tip': 'Tip', 'If enabled, you can zap with a single click. Click and hold for custom amounts': 'If enabled, you can zap with a single click. Click and hold for custom amounts', 'Include public zap receipt': 'Include public zap receipt', diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index d25afd31..0bf0174e 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -677,6 +677,18 @@ export default { 'Default zap comment': 'Standard-Zap-Kommentar', 'Lightning Address (or LNURL)': 'Lightning-Adresse (oder LNURL)', 'Quick zap': 'Schneller Zap', + 'Preferred payto category': 'Bevorzugte Payto-Kategorie', + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.': + 'Diese Kategorie in Zahlungsmethodenlisten ausgeklappt anzeigen; andere Kategorien hinter einem Akkordeon einklappen.', + 'Show all categories': 'Alle Kategorien anzeigen', + 'Other payment categories ({{count}})': 'Weitere Zahlungskategorien ({{count}})', + 'paytoCategory.bitcoin': 'Bitcoin', + 'paytoCategory.bitcoin-layer': 'Bitcoin-Layer', + 'paytoCategory.monero': 'Monero', + 'paytoCategory.crypto': 'Krypto', + 'paytoCategory.stablecoin': 'Stablecoin', + 'paytoCategory.fiat': 'Fiat', + 'paytoCategory.tip': 'Trinkgeld', 'If enabled, you can zap with a single click. Click and hold for custom amounts': 'Wenn aktiviert, kannst du mit einem Klick zapen. Klicke und halte für individuelle Beträge', 'Include public zap receipt': 'Include public zap receipt', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 6f986b99..5e437fb1 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -667,6 +667,18 @@ export default { 'Default zap comment': 'Default zap comment', 'Lightning Address (or LNURL)': 'Lightning Address (or LNURL)', 'Quick zap': 'Quick zap', + 'Preferred payto category': 'Preferred payto category', + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.': + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.', + 'Show all categories': 'Show all categories', + 'Other payment categories ({{count}})': 'Other payment categories ({{count}})', + 'paytoCategory.bitcoin': 'Bitcoin', + 'paytoCategory.bitcoin-layer': 'Bitcoin layer', + 'paytoCategory.monero': 'Monero', + 'paytoCategory.crypto': 'Crypto', + 'paytoCategory.stablecoin': 'Stablecoin', + 'paytoCategory.fiat': 'Fiat', + 'paytoCategory.tip': 'Tip', 'If enabled, you can zap with a single click. Click and hold for custom amounts': 'If enabled, you can zap with a single click. Click and hold for custom amounts', 'Include public zap receipt': 'Include public zap receipt', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index df257ef2..c707f4be 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -671,6 +671,18 @@ export default { 'Default zap comment': 'Comentario predeterminado de zap', 'Lightning Address (or LNURL)': 'Dirección Lightning (o LNURL)', 'Quick zap': 'Zap rápido', + 'Preferred payto category': 'Categoría payto preferida', + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.': + 'Muestra esta categoría expandida en las listas de métodos de pago; las demás se pliegan en un acordeón.', + 'Show all categories': 'Mostrar todas las categorías', + 'Other payment categories ({{count}})': 'Otras categorías de pago ({{count}})', + 'paytoCategory.bitcoin': 'Bitcoin', + 'paytoCategory.bitcoin-layer': 'Capa Bitcoin', + 'paytoCategory.monero': 'Monero', + 'paytoCategory.crypto': 'Cripto', + 'paytoCategory.stablecoin': 'Stablecoin', + 'paytoCategory.fiat': 'Fiat', + 'paytoCategory.tip': 'Propina', 'If enabled, you can zap with a single click. Click and hold for custom amounts': 'Si está habilitado, puedes enviar un zap con un solo clic. Haz clic y mantén pulsado para cantidades personalizadas', 'Include public zap receipt': 'Include public zap receipt', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 2d3486e6..3a9e69d6 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -670,6 +670,18 @@ export default { 'Default zap comment': 'Commentaire de zap par défaut', 'Lightning Address (or LNURL)': 'Adresse Lightning (ou LNURL)', 'Quick zap': 'Zap rapide', + 'Preferred payto category': 'Catégorie payto préférée', + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.': + 'Afficher cette catégorie dépliée dans les listes de paiement ; les autres sont repliées dans un accordéon.', + 'Show all categories': 'Afficher toutes les catégories', + 'Other payment categories ({{count}})': 'Autres catégories de paiement ({{count}})', + 'paytoCategory.bitcoin': 'Bitcoin', + 'paytoCategory.bitcoin-layer': 'Couche Bitcoin', + 'paytoCategory.monero': 'Monero', + 'paytoCategory.crypto': 'Crypto', + 'paytoCategory.stablecoin': 'Stablecoin', + 'paytoCategory.fiat': 'Fiat', + 'paytoCategory.tip': 'Pourboire', 'If enabled, you can zap with a single click. Click and hold for custom amounts': 'Si activé, vous pouvez zap avec un seul clic. Cliquez et maintenez pour des montants personnalisés', 'Include public zap receipt': 'Include public zap receipt', diff --git a/src/i18n/locales/nl.ts b/src/i18n/locales/nl.ts index d25610ab..a515615b 100644 --- a/src/i18n/locales/nl.ts +++ b/src/i18n/locales/nl.ts @@ -669,6 +669,18 @@ export default { 'Default zap comment': 'Default zap comment', 'Lightning Address (or LNURL)': 'Lightning Address (or LNURL)', 'Quick zap': 'Quick zap', + 'Preferred payto category': 'Preferred payto category', + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.': + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.', + 'Show all categories': 'Show all categories', + 'Other payment categories ({{count}})': 'Other payment categories ({{count}})', + 'paytoCategory.bitcoin': 'Bitcoin', + 'paytoCategory.bitcoin-layer': 'Bitcoin layer', + 'paytoCategory.monero': 'Monero', + 'paytoCategory.crypto': 'Crypto', + 'paytoCategory.stablecoin': 'Stablecoin', + 'paytoCategory.fiat': 'Fiat', + 'paytoCategory.tip': 'Tip', 'If enabled, you can zap with a single click. Click and hold for custom amounts': 'If enabled, you can zap with a single click. Click and hold for custom amounts', 'Include public zap receipt': 'Include public zap receipt', diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 1f20c43a..5dde00da 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -668,6 +668,18 @@ export default { 'Default zap comment': 'Standardowy komentarz do zapa', 'Lightning Address (or LNURL)': 'Lightning Adres (lub LNURL)', 'Quick zap': 'Szybki zap', + 'Preferred payto category': 'Preferowana kategoria payto', + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.': + 'Pokazuj tę kategorię rozwiniętą na listach metod płatności; pozostałe zwijaj w akordeonie.', + 'Show all categories': 'Pokaż wszystkie kategorie', + 'Other payment categories ({{count}})': 'Inne kategorie płatności ({{count}})', + 'paytoCategory.bitcoin': 'Bitcoin', + 'paytoCategory.bitcoin-layer': 'Warstwa Bitcoin', + 'paytoCategory.monero': 'Monero', + 'paytoCategory.crypto': 'Krypto', + 'paytoCategory.stablecoin': 'Stablecoin', + 'paytoCategory.fiat': 'Fiat', + 'paytoCategory.tip': 'Napiwek', 'If enabled, you can zap with a single click. Click and hold for custom amounts': 'Jeśli włączysz tę opcję, będzie można wysyłać zapa jednym kliknięciem. Naciśnij i przytrzymaj, aby wprowadzić inną kwotę', 'Include public zap receipt': 'Include public zap receipt', diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 0a637c33..cadb8e11 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -671,6 +671,18 @@ export default { 'Default zap comment': 'Стандартный комментарий для зап', 'Lightning Address (or LNURL)': 'Lightning-адрес (или LNURL)', 'Quick zap': 'Быстрый зап', + 'Preferred payto category': 'Предпочитаемая категория payto', + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.': + 'Показывать эту категорию развёрнутой в списках способов оплаты; остальные сворачиваются в аккордеон.', + 'Show all categories': 'Показать все категории', + 'Other payment categories ({{count}})': 'Другие категории оплаты ({{count}})', + 'paytoCategory.bitcoin': 'Bitcoin', + 'paytoCategory.bitcoin-layer': 'Слой Bitcoin', + 'paytoCategory.monero': 'Monero', + 'paytoCategory.crypto': 'Крипто', + 'paytoCategory.stablecoin': 'Стейблкоин', + 'paytoCategory.fiat': 'Фиат', + 'paytoCategory.tip': 'Чаевые', 'If enabled, you can zap with a single click. Click and hold for custom amounts': 'Если включено, вы можете отправить зап одним нажатием. Нажмите и удерживайте для выбора суммы', 'Include public zap receipt': 'Include public zap receipt', diff --git a/src/i18n/locales/tr.ts b/src/i18n/locales/tr.ts index e7fac2b8..36036d7c 100644 --- a/src/i18n/locales/tr.ts +++ b/src/i18n/locales/tr.ts @@ -669,6 +669,18 @@ export default { 'Default zap comment': 'Default zap comment', 'Lightning Address (or LNURL)': 'Lightning Address (or LNURL)', 'Quick zap': 'Quick zap', + 'Preferred payto category': 'Preferred payto category', + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.': + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.', + 'Show all categories': 'Show all categories', + 'Other payment categories ({{count}})': 'Other payment categories ({{count}})', + 'paytoCategory.bitcoin': 'Bitcoin', + 'paytoCategory.bitcoin-layer': 'Bitcoin layer', + 'paytoCategory.monero': 'Monero', + 'paytoCategory.crypto': 'Crypto', + 'paytoCategory.stablecoin': 'Stablecoin', + 'paytoCategory.fiat': 'Fiat', + 'paytoCategory.tip': 'Tip', 'If enabled, you can zap with a single click. Click and hold for custom amounts': 'If enabled, you can zap with a single click. Click and hold for custom amounts', 'Include public zap receipt': 'Include public zap receipt', diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 4d74b4e9..edf7a528 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -667,6 +667,18 @@ export default { 'Default zap comment': '默认打闪附言', 'Lightning Address (or LNURL)': '闪电地址 (或 LNURL)', 'Quick zap': '快速打闪', + 'Preferred payto category': '首选 payto 类别', + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.': + '在支付方式列表中展开此类别;其他类别折叠在手风琴中。', + 'Show all categories': '显示所有类别', + 'Other payment categories ({{count}})': '其他支付类别 ({{count}})', + 'paytoCategory.bitcoin': '比特币', + 'paytoCategory.bitcoin-layer': '比特币二层', + 'paytoCategory.monero': '门罗币', + 'paytoCategory.crypto': '加密货币', + 'paytoCategory.stablecoin': '稳定币', + 'paytoCategory.fiat': '法币', + 'paytoCategory.tip': '打赏', 'If enabled, you can zap with a single click. Click and hold for custom amounts': '如果启用,您单击即可打闪。长按以设置自定义金额', 'Include public zap receipt': 'Include public zap receipt', diff --git a/src/lib/payto-category-display.test.ts b/src/lib/payto-category-display.test.ts new file mode 100644 index 00000000..f7cb4ccd --- /dev/null +++ b/src/lib/payto-category-display.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' + +import type { PaymentMethodGroup } from '@/lib/merge-payment-methods' +import { + getPaymentMethodGroupCategory, + partitionPaymentGroupsByPreferredCategory +} from '@/lib/payto-category-display' + +function group(type: string, displayType: string): PaymentMethodGroup { + return { + displayType, + methods: [{ type, authority: 'addr', payto: `payto://${type}/addr`, displayType }] + } +} + +describe('payto-category-display', () => { + it('reads category from first method type', () => { + expect(getPaymentMethodGroupCategory(group('lightning', 'Lightning'))).toBe('bitcoin-layer') + expect(getPaymentMethodGroupCategory(group('monero', 'Monero'))).toBe('monero') + expect(getPaymentMethodGroupCategory(group('xmr', 'Monero'))).toBe('monero') + }) + + it('treats monero as part of crypto preference', () => { + const groups = [group('monero', 'Monero'), group('ethereum', 'Ethereum'), group('bitcoin', 'Bitcoin')] + const { preferredGroups, otherGroups } = partitionPaymentGroupsByPreferredCategory(groups, 'crypto') + expect(preferredGroups.map((g) => g.displayType).sort()).toEqual(['Ethereum', 'Monero']) + expect(otherGroups).toHaveLength(1) + }) + + it('partitions monero-only when monero preference is set', () => { + const groups = [group('monero', 'Monero'), group('ethereum', 'Ethereum')] + const { preferredGroups, otherGroups } = partitionPaymentGroupsByPreferredCategory(groups, 'monero') + expect(preferredGroups).toHaveLength(1) + expect(preferredGroups[0].displayType).toBe('Monero') + expect(otherGroups).toHaveLength(1) + }) + + it('partitions groups by preferred category', () => { + const groups = [ + group('lightning', 'Lightning'), + group('monero', 'Monero'), + group('bitcoin', 'Bitcoin') + ] + const { preferredGroups, otherGroups } = partitionPaymentGroupsByPreferredCategory( + groups, + 'bitcoin-layer' + ) + expect(preferredGroups).toHaveLength(1) + expect(preferredGroups[0].displayType).toBe('Lightning') + expect(otherGroups).toHaveLength(2) + }) + + it('returns all groups as preferred when preference is unset', () => { + const groups = [group('lightning', 'Lightning'), group('monero', 'Monero')] + const { preferredGroups, otherGroups } = partitionPaymentGroupsByPreferredCategory(groups, null) + expect(preferredGroups).toEqual(groups) + expect(otherGroups).toHaveLength(0) + }) + + it('shows all groups expanded when preferred category has no matches', () => { + const groups = [group('monero', 'Monero'), group('bitcoin', 'Bitcoin')] + const { preferredGroups, otherGroups } = partitionPaymentGroupsByPreferredCategory( + groups, + 'fiat' + ) + expect(preferredGroups).toEqual(groups) + expect(otherGroups).toHaveLength(0) + }) +}) diff --git a/src/lib/payto-category-display.ts b/src/lib/payto-category-display.ts new file mode 100644 index 00000000..4e64557a --- /dev/null +++ b/src/lib/payto-category-display.ts @@ -0,0 +1,63 @@ +import type { PaymentMethodGroup } from '@/lib/merge-payment-methods' +import { getCanonicalPaytoType, getPaytoTypeInfo, type PaytoCategory } from '@/lib/payto-registry' + +export const PAYTO_CATEGORIES: readonly PaytoCategory[] = [ + 'bitcoin', + 'bitcoin-layer', + 'monero', + 'crypto', + 'stablecoin', + 'fiat', + 'tip' +] as const + +export function isPaytoCategory(value: string): value is PaytoCategory { + return (PAYTO_CATEGORIES as readonly string[]).includes(value) +} + +/** i18n key under `paytoCategory.*` */ +export function paytoCategoryTranslationKey(category: PaytoCategory): string { + return `paytoCategory.${category}` +} + +export function getPaymentMethodGroupCategory(group: PaymentMethodGroup): PaytoCategory | null { + const type = group.methods.find((m) => m.type)?.type + if (!type) return null + if (getCanonicalPaytoType(type) === 'monero') return 'monero' + return getPaytoTypeInfo(type)?.category ?? null +} + +/** Monero stays in the crypto bucket in the catalog but has its own preference category on Nostr. */ +export function groupMatchesPreferredPaytoCategory( + group: PaymentMethodGroup, + preferred: PaytoCategory +): boolean { + const category = getPaymentMethodGroupCategory(group) + if (!category) return false + if (category === preferred) return true + if (preferred === 'crypto' && category === 'monero') return true + return false +} + +export function partitionPaymentGroupsByPreferredCategory( + groups: PaymentMethodGroup[], + preferred: PaytoCategory | null +): { preferredGroups: PaymentMethodGroup[]; otherGroups: PaymentMethodGroup[] } { + if (!preferred) { + return { preferredGroups: groups, otherGroups: [] } + } + const preferredGroups: PaymentMethodGroup[] = [] + const otherGroups: PaymentMethodGroup[] = [] + for (const group of groups) { + if (groupMatchesPreferredPaytoCategory(group, preferred)) { + preferredGroups.push(group) + } else { + otherGroups.push(group) + } + } + // No matches on this profile: show everything expanded instead of an empty list + accordion. + if (preferredGroups.length === 0) { + return { preferredGroups: groups, otherGroups: [] } + } + return { preferredGroups, otherGroups } +} diff --git a/src/lib/payto-registry.ts b/src/lib/payto-registry.ts index 50d7e639..d2b716f8 100644 --- a/src/lib/payto-registry.ts +++ b/src/lib/payto-registry.ts @@ -7,7 +7,14 @@ import paytoTypesCatalog from '@/data/payto-types.json' import { resolvePaytoLogoAssetPath } from '@/lib/payto-logos' import { resolvePaypalPaymentUrl } from '@/lib/payto-paypal-url' -export type PaytoCategory = 'bitcoin' | 'bitcoin-layer' | 'crypto' | 'stablecoin' | 'fiat' | 'tip' +export type PaytoCategory = + | 'bitcoin' + | 'bitcoin-layer' + | 'monero' + | 'crypto' + | 'stablecoin' + | 'fiat' + | 'tip' export type PaytoAuthorityHelp = { placeholder: string diff --git a/src/pages/secondary/WalletPage/PreferredPaytoCategorySelect.tsx b/src/pages/secondary/WalletPage/PreferredPaytoCategorySelect.tsx new file mode 100644 index 00000000..04b7888a --- /dev/null +++ b/src/pages/secondary/WalletPage/PreferredPaytoCategorySelect.tsx @@ -0,0 +1,52 @@ +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select' +import { usePreferredPaytoCategory } from '@/hooks/usePreferredPaytoCategory' +import { + isPaytoCategory, + PAYTO_CATEGORIES, + paytoCategoryTranslationKey +} from '@/lib/payto-category-display' +import { SelectValue } from '@radix-ui/react-select' +import { useTranslation } from 'react-i18next' + +const SHOW_ALL_VALUE = '__all__' + +export default function PreferredPaytoCategorySelect() { + const { t } = useTranslation() + const { preferredPaytoCategory, setPreferredPaytoCategory } = usePreferredPaytoCategory() + + return ( +
+ +

+ {t( + 'Show this category expanded on payment method lists; other categories collapse behind an accordion.' + )} +

+ +
+ ) +} diff --git a/src/pages/secondary/WalletPage/index.tsx b/src/pages/secondary/WalletPage/index.tsx index 07c971b1..8910cb51 100644 --- a/src/pages/secondary/WalletPage/index.tsx +++ b/src/pages/secondary/WalletPage/index.tsx @@ -5,6 +5,7 @@ import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' import { forwardRef, useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import LightningAddressInput from './LightningAddressInput' +import PreferredPaytoCategorySelect from './PreferredPaytoCategorySelect' import WalletZapSendingSettings from './WalletZapSendingSettings' const WalletPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => { @@ -32,6 +33,7 @@ const WalletPage = forwardRef(({ index, hideTitlebar = false }: { index?: number
{LIGHTNING_WALLET_PAY_ENABLED ? : null} +
) diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index cbe461af..51bab52b 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -9,6 +9,8 @@ import { import { kinds } from 'nostr-tools' import { isSameAccount } from '@/lib/account' import { DEFAULT_ZAP_SATS } from '@/lib/lightning' +import { isPaytoCategory } from '@/lib/payto-category-display' +import type { PaytoCategory } from '@/lib/payto-registry' import { setRestrictConnectionsToMetadataRelaysOnly } from '@/lib/read-only-relay-personal' import { randomString } from '@/lib/random' import { @@ -53,6 +55,7 @@ const SETTINGS_KEYS = [ StorageKey.CURRENT_ACCOUNT, StorageKey.DEFAULT_ZAP_SATS, StorageKey.DEFAULT_ZAP_COMMENT, + StorageKey.PREFERRED_PAYTO_CATEGORY, StorageKey.QUICK_ZAP, StorageKey.INCLUDE_PUBLIC_ZAP_RECEIPT, StorageKey.AUTOPLAY, @@ -100,6 +103,7 @@ class LocalStorageService { private noteListMode: TNoteListMode = 'posts' private defaultZapSats: number = DEFAULT_ZAP_SATS private defaultZapComment: string = 'Zap!' + private preferredPaytoCategory: PaytoCategory | null = null private quickZap: boolean = false private includePublicZapReceipt: boolean = true private mediaUploadService: string = DEFAULT_NIP_96_SERVICE @@ -198,6 +202,11 @@ class LocalStorageService { } } this.defaultZapComment = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_COMMENT) ?? 'Zap!' + const preferredPaytoCategoryStr = window.localStorage.getItem(StorageKey.PREFERRED_PAYTO_CATEGORY) + this.preferredPaytoCategory = + preferredPaytoCategoryStr && isPaytoCategory(preferredPaytoCategoryStr) + ? preferredPaytoCategoryStr + : null this.quickZap = window.localStorage.getItem(StorageKey.QUICK_ZAP) === 'true' const includeReceiptStr = window.localStorage.getItem(StorageKey.INCLUDE_PUBLIC_ZAP_RECEIPT) if (includeReceiptStr != null) { @@ -596,6 +605,13 @@ class LocalStorageService { } const defaultZapCommentStr = get(StorageKey.DEFAULT_ZAP_COMMENT) if (defaultZapCommentStr != null) this.defaultZapComment = defaultZapCommentStr + const preferredPaytoCategoryStr = get(StorageKey.PREFERRED_PAYTO_CATEGORY) + if (preferredPaytoCategoryStr != null) { + this.preferredPaytoCategory = + preferredPaytoCategoryStr && isPaytoCategory(preferredPaytoCategoryStr) + ? preferredPaytoCategoryStr + : null + } const quickZapStr = get(StorageKey.QUICK_ZAP) if (quickZapStr != null) this.quickZap = quickZapStr === 'true' const includeReceiptStr = get(StorageKey.INCLUDE_PUBLIC_ZAP_RECEIPT) @@ -803,6 +819,20 @@ class LocalStorageService { this.persistSetting(StorageKey.DEFAULT_ZAP_COMMENT, comment) } + getPreferredPaytoCategory(): PaytoCategory | null { + return this.preferredPaytoCategory + } + + setPreferredPaytoCategory(category: PaytoCategory | null) { + this.preferredPaytoCategory = category + if (category) { + this.persistSetting(StorageKey.PREFERRED_PAYTO_CATEGORY, category) + } else { + window.localStorage.removeItem(StorageKey.PREFERRED_PAYTO_CATEGORY) + void this.persistSettingToIndexedDb(StorageKey.PREFERRED_PAYTO_CATEGORY, '') + } + } + getQuickZap() { return this.quickZap }