diff --git a/package-lock.json b/package-lock.json index 369d0ad4..e5f3caad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.13.3", + "version": "23.13.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.13.3", + "version": "23.13.5", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index eb69512e..03e10443 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.13.4", + "version": "23.13.5", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "private": true, "type": "module", diff --git a/src/components/PaytoDialog/index.tsx b/src/components/PaytoDialog/index.tsx index a2d9982e..375d154c 100644 --- a/src/components/PaytoDialog/index.tsx +++ b/src/components/PaytoDialog/index.tsx @@ -14,7 +14,7 @@ import { SelectTrigger, SelectValue } from '@/components/ui/select' -import { ArrowRight, Copy, Zap } from 'lucide-react' +import { ArrowRight, Copy, Wallet, Zap } from 'lucide-react' import { useTranslation } from 'react-i18next' import { closeModal } from '@getalby/bitcoin-connect-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -23,8 +23,12 @@ import { releaseBodyScrollLocks } from '@/lib/react-remove-scroll-body-cleanup' import { filterPaytoPaymentOpenHandlersForDevice, getPaytoTypeInfo, + isLikelyMobileWalletUserAgent, + isPaytoHttpOpenUrl, openPaytoPaymentTarget, - resolvePaytoPaymentOpenHandlers + openPaytoResolvedUrl, + resolvePaytoPaymentOpenHandlers, + resolvePaytoProfileUrl } from '@/lib/payto' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' @@ -99,6 +103,19 @@ export default function PaytoDialog({ [openHandlers, selectedOpenHandlerId] ) + const walletOpenUri = useMemo( + () => (isLightning ? null : resolvePaytoProfileUrl(type, authority)), + [isLightning, type, authority] + ) + + const showPrimaryOpen = isLikelyMobileWalletUserAgent() && !!walletOpenUri + + const handleOpenWallet = () => { + if (!walletOpenUri) return + openPaytoResolvedUrl(walletOpenUri) + closeForWalletFlow() + } + const handleCopy = (text: string, copyLabel?: string) => { navigator.clipboard.writeText(text) toast.success(copyLabel ? t('Copied {{label}} address', { label: copyLabel }) : t('Copied to clipboard')) @@ -145,7 +162,9 @@ export default function PaytoDialog({ {isLightning ? t('Create a BOLT11 invoice from this Lightning address, then pay in your connected wallet or another app.') - : t('Payment address – copy to use in your wallet or app')} + : showPrimaryOpen + ? t('Open in your wallet app or copy the address below.') + : t('Payment address – copy to use in your wallet or app')} @@ -166,23 +185,39 @@ export default function PaytoDialog({

{authority}

-
- - +
+ {showPrimaryOpen && walletOpenUri ? ( + + ) : null} +
+ + +
)} diff --git a/src/data/payto-types.json b/src/data/payto-types.json index 289889e6..bbe5c198 100644 --- a/src/data/payto-types.json +++ b/src/data/payto-types.json @@ -340,7 +340,10 @@ "logoAssetPath": "src/assets/payto_logos/bitcoin-cash-bch-logo.svg", "walletOpen": { "scheme": "bitcoincash", - "walletApps": ["cakewallet", "ledger"] + "walletApps": ["cakewallet", "ledger"], + "walletAppUriTemplates": { + "cakewallet": "bitcoincash:{authority}" + } }, "authority": { "placeholder": "bitcoincash:… or q…", diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 2037bd04..dbc63f53 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -126,6 +126,7 @@ export default { "Lightning payment address – copy to pay via your wallet": "Lightning payment address – copy to pay via your wallet", "Payment address": "Payment address", "Payment address – copy to use in your wallet or app": "Payment address – copy to use in your wallet or app", + "Open in your wallet app or copy the address below.": "Open in your wallet app or copy the address below.", "Click to open payment options": "Click to open payment options", "Click to copy address": "Click to copy address", "Open on website": "Open on website", diff --git a/src/lib/payto-registry.ts b/src/lib/payto-registry.ts index 1ae25e4b..a480d807 100644 --- a/src/lib/payto-registry.ts +++ b/src/lib/payto-registry.ts @@ -33,6 +33,11 @@ export type PaytoWalletOpenRow = { requireAtSign?: boolean requirePrefix?: string walletApps?: string[] + /** + * Per-app URI template overrides (e.g. Cake Wallet needs native `bitcoincash:` for BCH + * because its wallet label is "bitcoin cash", not `bitcoincash`). + */ + walletAppUriTemplates?: Record /** When true, {@link walletApps} are hidden until PaytoDialog supplies a BOLT11 (see `_openWith.bolt11Invoice`). */ deferWalletAppsUntilBolt11?: boolean } diff --git a/src/lib/payto-targets.test.ts b/src/lib/payto-targets.test.ts index 66289a27..6dedd9f4 100644 --- a/src/lib/payto-targets.test.ts +++ b/src/lib/payto-targets.test.ts @@ -247,6 +247,14 @@ describe('resolvePaytoPaymentOpenHandlers', () => { ).toEqual(['PayPal']) }) + it('uses native bitcoincash: URI for Cake Wallet on BCH (not cakewallet:bitcoincash)', () => { + const addr = 'qzrvw7kxr6a2vwm6hjcpaym8znf8t4nlyd8y4f2f8k' + const handlers = resolvePaytoPaymentOpenHandlers('bitcoin-cash', addr) + const cake = handlers.find((h) => h.openTargetName === 'Cake Wallet') + expect(cake?.href).toBe(`bitcoincash:${addr}`) + expect(cake?.href).not.toMatch(/^cakewallet:/) + }) + it('never mixes fiat web links or lightning wallets across payto types', () => { const monero = resolvePaytoPaymentOpenHandlers( 'monero', diff --git a/src/lib/payto-targets.ts b/src/lib/payto-targets.ts index 1ba44b0e..926785c6 100644 --- a/src/lib/payto-targets.ts +++ b/src/lib/payto-targets.ts @@ -155,12 +155,13 @@ export function resolveWalletAppHref( const build = WALLET_APP_BUILDERS[app.builder] if (build) return build(coinScheme, auth) } - if (!app.uriTemplate) return null + const template = walletOpen?.walletAppUriTemplates?.[appId] ?? app.uriTemplate + if (!template) return null const payload = coinScheme === 'lightning' || coinScheme === 'bolt12' ? auth.replace(/^lightning:/i, '') : auth - return substituteAuthority(app.uriTemplate.replace(/\{coinScheme\}/g, coinScheme), payload) + return substituteAuthority(template.replace(/\{coinScheme\}/g, coinScheme), payload) } /** Primary open URL: native wallet URI, then profile web template. */ @@ -314,11 +315,16 @@ export function filterPaytoPaymentOpenHandlersForDevice( return handlers.filter((h) => !h.mobileOnly) } -/** Open a resolved handler (new tab for https, assign for wallet schemes). */ -export function openPaytoPaymentTarget(handler: PaytoPaymentOpenHandler): void { - if (handler.isHttp) { - window.open(handler.href, '_blank', 'noopener,noreferrer') +/** Open a resolved payto / wallet URL (https in a new tab, coin schemes via assign). */ +export function openPaytoResolvedUrl(url: string): void { + if (isPaytoHttpOpenUrl(url)) { + window.open(url, '_blank', 'noopener,noreferrer') return } - window.location.assign(handler.href) + window.location.assign(url) +} + +/** Open a resolved handler (new tab for https, assign for wallet schemes). */ +export function openPaytoPaymentTarget(handler: PaytoPaymentOpenHandler): void { + openPaytoResolvedUrl(handler.href) } diff --git a/src/lib/payto.ts b/src/lib/payto.ts index 9ba86f5e..83699dfa 100644 --- a/src/lib/payto.ts +++ b/src/lib/payto.ts @@ -29,6 +29,7 @@ export { export { filterPaytoPaymentOpenHandlersForDevice, openPaytoPaymentTarget, + openPaytoResolvedUrl, resolvePaytoPaymentOpenHandlers, resolvePaytoProfileUrl, isPaytoHttpOpenUrl,