Browse Source

bug-fix

imwald
Silberengel 4 weeks ago
parent
commit
239ba9a3f9
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 43
      src/components/PaytoDialog/index.tsx
  4. 5
      src/data/payto-types.json
  5. 1
      src/i18n/locales/en.ts
  6. 5
      src/lib/payto-registry.ts
  7. 8
      src/lib/payto-targets.test.ts
  8. 20
      src/lib/payto-targets.ts
  9. 1
      src/lib/payto.ts

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.13.3", "version": "23.13.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imwald", "name": "imwald",
"version": "23.13.3", "version": "23.13.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "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", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

43
src/components/PaytoDialog/index.tsx

@ -14,7 +14,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from '@/components/ui/select' } 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 { useTranslation } from 'react-i18next'
import { closeModal } from '@getalby/bitcoin-connect-react' import { closeModal } from '@getalby/bitcoin-connect-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@ -23,8 +23,12 @@ import { releaseBodyScrollLocks } from '@/lib/react-remove-scroll-body-cleanup'
import { import {
filterPaytoPaymentOpenHandlersForDevice, filterPaytoPaymentOpenHandlersForDevice,
getPaytoTypeInfo, getPaytoTypeInfo,
isLikelyMobileWalletUserAgent,
isPaytoHttpOpenUrl,
openPaytoPaymentTarget, openPaytoPaymentTarget,
resolvePaytoPaymentOpenHandlers openPaytoResolvedUrl,
resolvePaytoPaymentOpenHandlers,
resolvePaytoProfileUrl
} from '@/lib/payto' } from '@/lib/payto'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
@ -99,6 +103,19 @@ export default function PaytoDialog({
[openHandlers, selectedOpenHandlerId] [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) => { const handleCopy = (text: string, copyLabel?: string) => {
navigator.clipboard.writeText(text) navigator.clipboard.writeText(text)
toast.success(copyLabel ? t('Copied {{label}} address', { label: copyLabel }) : t('Copied to clipboard')) toast.success(copyLabel ? t('Copied {{label}} address', { label: copyLabel }) : t('Copied to clipboard'))
@ -145,6 +162,8 @@ export default function PaytoDialog({
<DialogDescription className="text-left text-sm leading-relaxed sm:text-base"> <DialogDescription className="text-left text-sm leading-relaxed sm:text-base">
{isLightning {isLightning
? t('Create a BOLT11 invoice from this Lightning address, then pay in your connected wallet or another app.') ? t('Create a BOLT11 invoice from this Lightning address, then pay in your connected wallet or another app.')
: showPrimaryOpen
? t('Open in your wallet app or copy the address below.')
: t('Payment address – copy to use in your wallet or app')} : t('Payment address – copy to use in your wallet or app')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -166,17 +185,32 @@ export default function PaytoDialog({
</p> </p>
<p className="break-all font-mono text-base leading-relaxed select-text sm:text-lg">{authority}</p> <p className="break-all font-mono text-base leading-relaxed select-text sm:text-lg">{authority}</p>
</div> </div>
<div className="grid min-w-0 grid-cols-1 gap-2 sm:grid-cols-2"> <div className="flex min-w-0 flex-col gap-2">
{showPrimaryOpen && walletOpenUri ? (
<Button <Button
variant="default" variant="default"
className="h-11 w-full min-w-0 gap-2 text-base" className="h-11 w-full min-w-0 gap-2 text-base"
onClick={handleOpenWallet}
>
<Wallet className="size-5 shrink-0" />
<span className="truncate">
{isPaytoHttpOpenUrl(walletOpenUri)
? t('Open on website')
: t('Open in wallet')}
</span>
</Button>
) : null}
<div className="grid min-w-0 grid-cols-1 gap-2 sm:grid-cols-2">
<Button
variant={showPrimaryOpen ? 'outline' : 'default'}
className="h-11 w-full min-w-0 gap-2 text-base"
onClick={() => handleCopy(authority, label)} onClick={() => handleCopy(authority, label)}
> >
<Copy className="size-5 shrink-0" /> <Copy className="size-5 shrink-0" />
<span className="truncate">{t('Copy address')}</span> <span className="truncate">{t('Copy address')}</span>
</Button> </Button>
<Button <Button
variant="secondary" variant={showPrimaryOpen ? 'outline' : 'secondary'}
className="h-11 w-full min-w-0 gap-2 text-base" className="h-11 w-full min-w-0 gap-2 text-base"
onClick={() => handleCopy(paytoUri)} onClick={() => handleCopy(paytoUri)}
> >
@ -184,6 +218,7 @@ export default function PaytoDialog({
<span className="truncate">{t('Copy payto URI')}</span> <span className="truncate">{t('Copy payto URI')}</span>
</Button> </Button>
</div> </div>
</div>
</> </>
)} )}

5
src/data/payto-types.json

@ -340,7 +340,10 @@
"logoAssetPath": "src/assets/payto_logos/bitcoin-cash-bch-logo.svg", "logoAssetPath": "src/assets/payto_logos/bitcoin-cash-bch-logo.svg",
"walletOpen": { "walletOpen": {
"scheme": "bitcoincash", "scheme": "bitcoincash",
"walletApps": ["cakewallet", "ledger"] "walletApps": ["cakewallet", "ledger"],
"walletAppUriTemplates": {
"cakewallet": "bitcoincash:{authority}"
}
}, },
"authority": { "authority": {
"placeholder": "bitcoincash:… or q…", "placeholder": "bitcoincash:… or q…",

1
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", "Lightning payment address – copy to pay via your wallet": "Lightning payment address – copy to pay via your wallet",
"Payment address": "Payment address", "Payment address": "Payment address",
"Payment address – copy to use in your wallet or app": "Payment address – copy to use in your wallet or app", "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 open payment options": "Click to open payment options",
"Click to copy address": "Click to copy address", "Click to copy address": "Click to copy address",
"Open on website": "Open on website", "Open on website": "Open on website",

5
src/lib/payto-registry.ts

@ -33,6 +33,11 @@ export type PaytoWalletOpenRow = {
requireAtSign?: boolean requireAtSign?: boolean
requirePrefix?: string requirePrefix?: string
walletApps?: 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<string, string>
/** When true, {@link walletApps} are hidden until PaytoDialog supplies a BOLT11 (see `_openWith.bolt11Invoice`). */ /** When true, {@link walletApps} are hidden until PaytoDialog supplies a BOLT11 (see `_openWith.bolt11Invoice`). */
deferWalletAppsUntilBolt11?: boolean deferWalletAppsUntilBolt11?: boolean
} }

8
src/lib/payto-targets.test.ts

@ -247,6 +247,14 @@ describe('resolvePaytoPaymentOpenHandlers', () => {
).toEqual(['PayPal']) ).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', () => { it('never mixes fiat web links or lightning wallets across payto types', () => {
const monero = resolvePaytoPaymentOpenHandlers( const monero = resolvePaytoPaymentOpenHandlers(
'monero', 'monero',

20
src/lib/payto-targets.ts

@ -155,12 +155,13 @@ export function resolveWalletAppHref(
const build = WALLET_APP_BUILDERS[app.builder] const build = WALLET_APP_BUILDERS[app.builder]
if (build) return build(coinScheme, auth) if (build) return build(coinScheme, auth)
} }
if (!app.uriTemplate) return null const template = walletOpen?.walletAppUriTemplates?.[appId] ?? app.uriTemplate
if (!template) return null
const payload = const payload =
coinScheme === 'lightning' || coinScheme === 'bolt12' coinScheme === 'lightning' || coinScheme === 'bolt12'
? auth.replace(/^lightning:/i, '') ? auth.replace(/^lightning:/i, '')
: auth : 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. */ /** Primary open URL: native wallet URI, then profile web template. */
@ -314,11 +315,16 @@ export function filterPaytoPaymentOpenHandlersForDevice(
return handlers.filter((h) => !h.mobileOnly) return handlers.filter((h) => !h.mobileOnly)
} }
/** Open a resolved handler (new tab for https, assign for wallet schemes). */ /** Open a resolved payto / wallet URL (https in a new tab, coin schemes via assign). */
export function openPaytoPaymentTarget(handler: PaytoPaymentOpenHandler): void { export function openPaytoResolvedUrl(url: string): void {
if (handler.isHttp) { if (isPaytoHttpOpenUrl(url)) {
window.open(handler.href, '_blank', 'noopener,noreferrer') window.open(url, '_blank', 'noopener,noreferrer')
return 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)
} }

1
src/lib/payto.ts

@ -29,6 +29,7 @@ export {
export { export {
filterPaytoPaymentOpenHandlersForDevice, filterPaytoPaymentOpenHandlersForDevice,
openPaytoPaymentTarget, openPaytoPaymentTarget,
openPaytoResolvedUrl,
resolvePaytoPaymentOpenHandlers, resolvePaytoPaymentOpenHandlers,
resolvePaytoProfileUrl, resolvePaytoProfileUrl,
isPaytoHttpOpenUrl, isPaytoHttpOpenUrl,

Loading…
Cancel
Save