From 9cae4da9f46c04d78d0235823eb912845525b8f7 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Wed, 12 Nov 2025 16:20:38 -0800 Subject: [PATCH] Add lightning invoice payments --- CHANGELOG.md | 5 ++ package.json | 1 + pnpm-lock.yaml | Bin 344642 -> 344955 bytes src/app/components/WalletPay.svelte | 111 ++++++++++++++++++++++++ src/app/core/commands.ts | 7 +- src/app/core/storage.ts | 4 +- src/app/util/storage.ts | 2 +- src/lib/components/Scanner.svelte | 39 ++++++++- src/lib/indexeddb.ts | 13 +-- src/routes/settings/wallet/+page.svelte | 10 +++ vite.config.ts | 9 ++ 11 files changed, 189 insertions(+), 12 deletions(-) create mode 100644 src/app/components/WalletPay.svelte diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da5ee1..42b02f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +# Current + +* Switch back to indexeddb to fix memory and performance +* Add pay invoice functionality + # 1.5.3 * Add space edit form diff --git a/package.json b/package.json index c3623d9..42bafec 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@capacitor/push-notifications": "^7.0.3", "@capawesome/capacitor-android-dark-mode-support": "^7.0.0", "@capawesome/capacitor-badge": "^7.0.1", + "@getalby/lightning-tools": "^6.0.0", "@getalby/sdk": "^5.1.2", "@poppanator/sveltekit-svg": "^4.2.1", "@sentry/browser": "^8.55.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5efe7e117ffe6fceac6157f669666c2cac18dc7..2b89cab2a2b44449ec33d3e8003d8e0c9a1751b1 100644 GIT binary patch delta 178 zcmX@qBKo^cbVCH|^w>kpf|C!kiZYw&8BA_u6JZ6?dIp=_S-s|f^b5-g3pMc!@htQY z4+=ES_Ag2@2*?Po$WC(iGj%skF*ERR&NMYP%}8>YoLD2<+_12{VId9b&O)005`zJ4FBh delta 41 wcmey}CVHqvbVCH|=GCm0bD9?|YG1U75r~<#FIvRhqtG69n0b5HVHVW}0FsdsT>t<8 diff --git a/src/app/components/WalletPay.svelte b/src/app/components/WalletPay.svelte new file mode 100644 index 0000000..5ec3b96 --- /dev/null +++ b/src/app/components/WalletPay.svelte @@ -0,0 +1,111 @@ + + +
+ + {#snippet title()} +
Pay with Lightning
+ {/snippet} + {#snippet info()} + Use your Nostr wallet to send Bitcoin payments over lightning. + {/snippet} +
+ {#if invoice} +
+ {#if $session?.wallet?.type === "webln" && invoice.satoshi === 0} +

+ Uh oh! It looks like your current wallet doesn't support invoices without an amount. See + if you can get a lightning invoice with a pre-set amount. +

+ {:else} + + {#snippet label()} + Amount (satoshis) + {/snippet} + {#snippet input()} +
+ +
+ {/snippet} +
+

+ You're about to pay a bitcoin lightning invoice with the following description: + {invoice.description || "[no description]"}" +

+ {/if} +
+ {:else} + +

+ To make a payment, scan a lightning invoice with your camera. +

+ {/if} + + + + +
diff --git a/src/app/core/commands.ts b/src/app/core/commands.ts index d82241b..5067d44 100644 --- a/src/app/core/commands.ts +++ b/src/app/core/commands.ts @@ -592,7 +592,7 @@ export const publishLeaveRequest = (params: LeaveRequestParams) => export const getWebLn = () => (window as any).webln -export const payInvoice = async (invoice: string) => { +export const payInvoice = async (invoice: string, msats?: number) => { const $session = session.get() if (!$session?.wallet) { @@ -600,8 +600,11 @@ export const payInvoice = async (invoice: string) => { } if ($session.wallet.type === "nwc") { - return new nwc.NWCClient($session.wallet.info).payInvoice({invoice}) + const params: {invoice: string; amount?: number} = {invoice} + if (msats) params.amount = msats + return new nwc.NWCClient($session.wallet.info).payInvoice(params) } else if ($session.wallet.type === "webln") { + if (msats) throw new Error("Unable to pay zero invoices with webln") return getWebLn() .enable() .then(() => getWebLn().sendPayment(invoice)) diff --git a/src/app/core/storage.ts b/src/app/core/storage.ts index 44e2241..9c99b84 100644 --- a/src/app/core/storage.ts +++ b/src/app/core/storage.ts @@ -1,6 +1,6 @@ -import {reject, call, identity} from "@welshman/lib" +import {call} from "@welshman/lib" import {Preferences} from "@capacitor/preferences" -import {Encoding, Filesystem, Directory} from "@capacitor/filesystem" +import {Filesystem, Directory} from "@capacitor/filesystem" import {IDB} from "@lib/indexeddb" export const kv = call(() => { diff --git a/src/app/util/storage.ts b/src/app/util/storage.ts index 1e9efe8..4f5bfea 100644 --- a/src/app/util/storage.ts +++ b/src/app/util/storage.ts @@ -1,4 +1,4 @@ -import {prop, call, on, throttle, fromPairs, batch} from "@welshman/lib" +import {on, throttle, fromPairs, batch} from "@welshman/lib" import {throttled, freshness} from "@welshman/store" import { ALERT_ANDROID, diff --git a/src/lib/components/Scanner.svelte b/src/lib/components/Scanner.svelte index 19d5e8b..695eb2e 100644 --- a/src/lib/components/Scanner.svelte +++ b/src/lib/components/Scanner.svelte @@ -5,11 +5,38 @@ const {onscan} = $props() + const changeCamera = async () => { + if (camera && scanner) { + loading = true + try { + await scanner.setCamera(camera) + } catch (error) { + console.error("Failed to switch camera:", error) + } finally { + loading = false + } + } + } + let video: HTMLVideoElement let scanner: QrScanner let loading = $state(true) + let cameras = $state([]) + let camera = $state("") onMount(() => { + QrScanner.listCameras(true) + .then(async () => { + cameras = await QrScanner.listCameras(true) + + if (cameras.length > 0) { + camera = cameras[0].id + } + }) + .catch(error => { + console.error("Failed to list cameras:", error) + }) + scanner = new QrScanner(video, r => onscan(r.data), { returnDetailedScanResult: true, }) @@ -22,11 +49,21 @@ }) -
+
{#if loading}

Loading your camera...

{/if} + {#if cameras.length > 1} + + {/if}
diff --git a/src/lib/indexeddb.ts b/src/lib/indexeddb.ts index ce1efd8..2c43427 100644 --- a/src/lib/indexeddb.ts +++ b/src/lib/indexeddb.ts @@ -1,10 +1,8 @@ import {openDB, deleteDB} from "idb" import type {IDBPDatabase} from "idb" -import {writable} from "svelte/store" import type {Unsubscriber} from "svelte/store" -import {call, defer} from "@welshman/lib" +import {call} from "@welshman/lib" import type {Maybe} from "@welshman/lib" -import {withGetter} from "@welshman/store" export type IDBAdapter = { name: string @@ -83,11 +81,11 @@ export class IDB { // If we're closing, ignore any lingering requests if ([IDBStatus.Closed, IDBStatus.Closing].includes(this.status)) return - return f(await this.idbp) + return f(await this.idbp!) } - getAll = async (table: string): Promise => - this._withIDBP(async idbp => { + getAll = async (table: string): Promise => { + const result = await this._withIDBP(async idbp => { const tx = idbp.transaction(table, "readwrite") const store = tx.objectStore(table) const result = await store.getAll() @@ -97,6 +95,9 @@ export class IDB { return result }) + return result || [] + } + bulkPut = async (table: string, data: Iterable) => this._withIDBP(async idbp => { const tx = idbp.transaction(table, "readwrite") diff --git a/src/routes/settings/wallet/+page.svelte b/src/routes/settings/wallet/+page.svelte index 12f05ee..4af6442 100644 --- a/src/routes/settings/wallet/+page.svelte +++ b/src/routes/settings/wallet/+page.svelte @@ -3,8 +3,10 @@ import {LOCALE} from "@welshman/lib" import {displayRelayUrl, isNWCWallet, fromMsats} from "@welshman/util" import {session, pubkey, profilesByPubkey} from "@welshman/app" + import Bolt from "@assets/icons/bolt.svg?dataurl" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" + import WalletPay from "@app/components/WalletPay.svelte" import WalletConnect from "@app/components/WalletConnect.svelte" import WalletDisconnect from "@app/components/WalletDisconnect.svelte" import WalletUpdateReceivingAddress from "@app/components/WalletUpdateReceivingAddress.svelte" @@ -27,6 +29,8 @@ const walletLud16 = $derived( $session?.wallet && isNWCWallet($session.wallet) ? $session.wallet.info.lud16 : undefined, ) + + const pay = () => pushModal(WalletPay)
@@ -118,4 +122,10 @@
{/if}
+
+ +
diff --git a/vite.config.ts b/vite.config.ts index 67ca659..a6bcb04 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,6 +10,15 @@ config({path: ".env.template"}) export default defineConfig({ server: { port: 1847, + // host: "0.0.0.0", + // strictPort: true, + // allowedHosts: ["coracle-client.ngrok.io"], + // hmr: { + // protocol: "wss", + // host: "coracle-client.ngrok.io", + // clientPort: 443, + // }, + // cors: true, }, build: { sourcemap: true,