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
}