diff options
| author | Adam Soltys <[email protected]> | 2026-02-05 09:34:30 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-02-11 15:34:32 -0800 |
| commit | ec304f8bd1ffef69a85b75da033fbccbe6bfc72f (patch) | |
| tree | 664e765f3d7e9c4f26463760ed155dabca76486b | |
| parent | 59fbd17aad12acfa19eb730c4ede67d50962ba9b (diff) | |
| download | coinos-ui-ec304f8bd1ffef69a85b75da033fbccbe6bfc72f.tar.gz coinos-ui-ec304f8bd1ffef69a85b75da033fbccbe6bfc72f.zip | |
fix ark payment notifications
| -rw-r--r-- | src/components/Account.svelte | 28 | ||||
| -rw-r--r-- | src/components/WalletPass.svelte | 33 | ||||
| -rw-r--r-- | src/lib/passwordCache.ts | 61 | ||||
| -rw-r--r-- | src/routes/(app)/+layout.svelte | 28 | ||||
| -rw-r--r-- | src/routes/(app)/account/[id]/+page.svelte | 38 | ||||
| -rw-r--r-- | src/routes/(app)/account/ark/+page.svelte | 28 | ||||
| -rw-r--r-- | src/routes/(app)/account/pass/+page.svelte | 28 | ||||
| -rw-r--r-- | src/routes/(app)/invoice/[id]/tip/+page.svelte | 8 | ||||
| -rw-r--r-- | src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte | 28 |
9 files changed, 259 insertions, 21 deletions
diff --git a/src/components/Account.svelte b/src/components/Account.svelte index 04ff97ae..aaef428b 100644 --- a/src/components/Account.svelte +++ b/src/components/Account.svelte @@ -6,6 +6,10 @@ import { goto } from "$app/navigation"; import Balance from "$comp/Balance.svelte"; import { t } from "$lib/translations"; import { arkkey } from "$lib/ark"; + import { + getRememberedWalletPassword, + forgetWalletPassword, + } from "$lib/passwordCache"; let { user, rate, account } = $props(); let { @@ -27,14 +31,32 @@ pendingUrl = undefined; }); - let setAccount = (event, url) => { + let tryUnlockArk = async () => { + const cached = getRememberedWalletPassword(); + if (!cached) return false; + try { + const { decrypt } = await import("nostr-tools/nip49"); + $arkkey = hex.encode(decrypt(seed, cached)); + return true; + } catch (e) { + forgetWalletPassword(); + return false; + } + }; + + let setAccount = async (event, url) => { event.preventDefault(); event.stopPropagation(); document.cookie = `aid=${id}; path=/; max-age=86400`; if (isArk) { pendingUrl = url; - if ($arkkey) goto(pendingUrl); - else passwordPrompt = true; + if ($arkkey) { + goto(pendingUrl); + } else if (await tryUnlockArk()) { + goto(pendingUrl); + } else { + passwordPrompt = true; + } } else { $arkkey = undefined; goto(url); diff --git a/src/components/WalletPass.svelte b/src/components/WalletPass.svelte index e3291f61..e0dcf961 100644 --- a/src/components/WalletPass.svelte +++ b/src/components/WalletPass.svelte @@ -3,8 +3,24 @@ import { focus } from "$lib/utils"; import { t } from "$lib/translations"; + import { + rememberForOptions, + defaultRememberForMs, + rememberWalletPassword, + forgetWalletPassword, + } from "$lib/passwordCache"; let { cancel, password = $bindable(), submit } = $props(); let revealPassword = $state(); + let rememberForMs = $state(defaultRememberForMs); + + let handleSubmit = async () => { + if (rememberForMs) { + rememberWalletPassword(password, rememberForMs); + } else { + forgetWalletPassword(); + } + await submit(); + }; </script> <div @@ -16,7 +32,7 @@ <h1 class="text-center text-2xl font-semibold"> {$t("payments.enterWalletPass")} </h1> - <form onsubmit={preventDefault(submit)}> + <form onsubmit={preventDefault(handleSubmit)}> <div class="relative mb-5"> {#if revealPassword} <input @@ -49,6 +65,21 @@ ></iconify-icon> </button> </div> + <div class="mb-5 space-y-2"> + <label for="rememberFor" class="text-sm text-gray-500"> + Remember for + </label> + <select + id="rememberFor" + class="w-full" + value={rememberForMs} + onchange={(e) => (rememberForMs = Number(e.target.value))} + > + {#each rememberForOptions as option} + <option value={option.ms}>{option.label}</option> + {/each} + </select> + </div> <div class="w-full flex justify-center gap-2"> <button type="button" diff --git a/src/lib/passwordCache.ts b/src/lib/passwordCache.ts new file mode 100644 index 00000000..4f764698 --- /dev/null +++ b/src/lib/passwordCache.ts @@ -0,0 +1,61 @@ +import { browser } from "$app/environment"; +import { writable } from "svelte/store"; + +const PASSWORD_KEY = "walletPassword"; +const EXPIRES_KEY = "walletPasswordExpiresAt"; + +export const rememberForOptions = [ + { label: "Don't remember", ms: 0 }, + { label: "8 hours", ms: 8 * 60 * 60 * 1000 }, + { label: "1 day", ms: 24 * 60 * 60 * 1000 }, + { label: "1 week", ms: 7 * 24 * 60 * 60 * 1000 }, + { label: "1 month", ms: 30 * 24 * 60 * 60 * 1000 }, + { label: "1 year", ms: 365 * 24 * 60 * 60 * 1000 }, +]; + +export const defaultRememberForMs = 30 * 24 * 60 * 60 * 1000; + +const clearStoredPassword = () => { + if (!browser) return; + localStorage.removeItem(PASSWORD_KEY); + localStorage.removeItem(EXPIRES_KEY); +}; + +const loadStoredPassword = () => { + if (!browser) return undefined; + const password = localStorage.getItem(PASSWORD_KEY) || undefined; + if (!password) return undefined; + const expiresAtRaw = localStorage.getItem(EXPIRES_KEY); + const expiresAt = expiresAtRaw ? Number(expiresAtRaw) : undefined; + if (expiresAt && Date.now() > expiresAt) { + clearStoredPassword(); + return undefined; + } + return password; +}; + +export const walletPassword = writable<string | undefined>(loadStoredPassword()); + +export const rememberWalletPassword = (password: string, durationMs: number) => { + if (!browser) return; + if (!durationMs) { + clearStoredPassword(); + walletPassword.set(undefined); + return; + } + const expiresAt = Date.now() + durationMs; + localStorage.setItem(PASSWORD_KEY, password); + localStorage.setItem(EXPIRES_KEY, String(expiresAt)); + walletPassword.set(password); +}; + +export const forgetWalletPassword = () => { + clearStoredPassword(); + walletPassword.set(undefined); +}; + +export const getRememberedWalletPassword = () => { + const password = loadStoredPassword(); + walletPassword.set(password); + return password; +}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 3a399c3a..c56a487a 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -46,10 +46,30 @@ } }); + // Track last known Ark balance to compute incoming amount + let getLastArkBalance = () => { + return parseInt(localStorage.getItem("lastArkBalance") || "0"); + }; + + let setLastArkBalance = (bal) => { + localStorage.setItem("lastArkBalance", bal.toString()); + }; + let handleArkPayment = async (notification) => { console.log("ARK payment notification:", notification); - let amount = notification.newVtxos.reduce((a, b) => a + b.value, 0); - if (!amount) return; + + // Calculate current total from newVtxos + let currentTotal = notification.newVtxos.reduce((a, b) => a + b.value, 0); + let lastBalance = getLastArkBalance(); + + // Compute delta (incoming amount) + let amount = currentTotal - lastBalance; + + // Update tracked balance + setLastArkBalance(currentTotal); + + // Only process positive incoming amounts + if (amount <= 0) return; if ($fiat && $rateStore && user?.currency) { success(`Received ${f(toFiat(amount, $rateStore), user.currency)}!`); @@ -58,12 +78,12 @@ } try { - let inv = await post(`/invoice`, { + let inv = await post(`/post/invoice`, { invoice: { type: "ark", amount }, user, }); - await post(`/ark/receive`, { + await post(`/post/ark/receive`, { amount, iid: inv.id, }); diff --git a/src/routes/(app)/account/[id]/+page.svelte b/src/routes/(app)/account/[id]/+page.svelte index a8970ca7..db93a04d 100644 --- a/src/routes/(app)/account/[id]/+page.svelte +++ b/src/routes/(app)/account/[id]/+page.svelte @@ -8,6 +8,10 @@ import { t } from "$lib/translations"; import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english.js"; + import { + getRememberedWalletPassword, + forgetWalletPassword, + } from "$lib/passwordCache"; let { data } = $props(); let { account, user } = data; @@ -26,12 +30,36 @@ let cancel = $state(() => (passwordPrompt = false)); let toggle = () => (passwordPrompt = !passwordPrompt); + let revealWithCachedPassword = async () => { + const cached = getRememberedWalletPassword(); + if (!cached) return false; + try { + const { decrypt } = await import("nostr-tools/nip49"); + let entropy = await decrypt(seed, cached); + mnemonic = entropyToMnemonic(entropy, wordlist); + password = cached; + return true; + } catch (e) { + forgetWalletPassword(); + return false; + } + }; - let submit = $state(async () => { + let requestMnemonic = async () => { + if (await revealWithCachedPassword()) return; toggle(); - const { decrypt } = await import("nostr-tools/nip49"); - let entropy = await decrypt(seed, password); - mnemonic = entropyToMnemonic(entropy, wordlist); + }; + + let submit = $state(async () => { + passwordPrompt = false; + try { + const { decrypt } = await import("nostr-tools/nip49"); + let entropy = await decrypt(seed, password); + mnemonic = entropyToMnemonic(entropy, wordlist); + } catch (e) { + fail("Invalid password, try again"); + passwordPrompt = true; + } }); let del = async () => { @@ -71,7 +99,7 @@ <div class="my-auto">{$t("accounts.copy")}</div></button > {:else if seed} - <button onclick={toggle} type="button" class="btn"> + <button onclick={requestMnemonic} type="button" class="btn"> <iconify-icon noobserver icon="ph:eye-bold" width="32" ></iconify-icon> <div class="my-auto">{$t("accounts.revealMnemonic")}</div></button diff --git a/src/routes/(app)/account/ark/+page.svelte b/src/routes/(app)/account/ark/+page.svelte index 99ab750b..aee80837 100644 --- a/src/routes/(app)/account/ark/+page.svelte +++ b/src/routes/(app)/account/ark/+page.svelte @@ -8,6 +8,12 @@ import { focus, fail, post, copy } from "$lib/utils"; import { goto } from "$app/navigation"; import { arkServerUrl } from "$lib/ark"; + import { + rememberForOptions, + defaultRememberForMs, + rememberWalletPassword, + forgetWalletPassword, + } from "$lib/passwordCache"; let { data } = $props(); let { user } = data; @@ -23,6 +29,7 @@ let confirm = $state(""); let revealPassword = $state(false); let revealConfirm = $state(false); + let rememberForMs = $state(defaultRememberForMs); let name = "Ark Vault"; let type = "ark"; @@ -73,6 +80,11 @@ let seed = await encrypt(hex.decode(privateKey), password); await post("/account", { name, seed, type, arkAddress }); + if (rememberForMs) { + rememberWalletPassword(password, rememberForMs); + } else { + forgetWalletPassword(); + } goto(`/${user.username}`); } catch (e) { console.log(e); @@ -205,6 +217,22 @@ </button> </label> + <div class="space-y-2"> + <label for="rememberFor" class="text-sm text-secondary"> + Remember for + </label> + <select + id="rememberFor" + class="w-full" + value={rememberForMs} + onchange={(e) => (rememberForMs = Number(e.target.value))} + > + {#each rememberForOptions as option} + <option value={option.ms}>{option.label}</option> + {/each} + </select> + </div> + <div class="flex gap-2"> <button type="button" diff --git a/src/routes/(app)/account/pass/+page.svelte b/src/routes/(app)/account/pass/+page.svelte index 7c57679a..74e45f17 100644 --- a/src/routes/(app)/account/pass/+page.svelte +++ b/src/routes/(app)/account/pass/+page.svelte @@ -14,6 +14,12 @@ import { focus, versions, fail, post } from "$lib/utils"; import { goto } from "$app/navigation"; import { HDKey } from "@scure/bip32"; + import { + rememberForOptions, + defaultRememberForMs, + rememberWalletPassword, + forgetWalletPassword, + } from "$lib/passwordCache"; let { data } = $props(); @@ -24,6 +30,7 @@ password = $state(), revealPassword = $state(), revealConfirm = $state(); + let rememberForMs = $state(defaultRememberForMs); let type = "bitcoin"; let name = $t("accounts.savings"); @@ -55,6 +62,11 @@ let pubkey = child.publicExtendedKey; let fingerprint = child.fingerprint.toString(16).padStart(8, "0"); await post("/account", { fingerprint, pubkey, name, seed, type }); + if (rememberForMs) { + rememberWalletPassword(password, rememberForMs); + } else { + forgetWalletPassword(); + } $mnemonic = ""; goto(`/${user.username}`); } catch (e) { @@ -159,6 +171,22 @@ </button> </label> + <div class="space-y-2"> + <label for="rememberFor" class="text-sm text-secondary"> + Remember for + </label> + <select + id="rememberFor" + class="w-full" + value={rememberForMs} + onchange={(e) => (rememberForMs = Number(e.target.value))} + > + {#each rememberForOptions as option} + <option value={option.ms}>{option.label}</option> + {/each} + </select> + </div> + <div class="flex gap-2"> <a href={`/account/seed`} class="contents"> <button type="button" class="btn !w-auto grow"> diff --git a/src/routes/(app)/invoice/[id]/tip/+page.svelte b/src/routes/(app)/invoice/[id]/tip/+page.svelte index 41cc4636..4be2f536 100644 --- a/src/routes/(app)/invoice/[id]/tip/+page.svelte +++ b/src/routes/(app)/invoice/[id]/tip/+page.svelte @@ -1,7 +1,4 @@ <script> - import { untrack } from "svelte"; - - import { browser } from "$app/environment"; import { fiat, invoice as inv, request } from "$lib/store"; import { loc, copy, focus, f, sat, get, s, sats } from "$lib/utils"; import Slider from "$comp/Slider.svelte"; @@ -78,7 +75,7 @@ let locale = $derived(loc(user)); let qr; - let tipPercent = $state(); + let tipPercent = $state(amount > 0 && tip > 0 ? (tip / amount) * 100 : 0); let fullscreen; @@ -86,8 +83,7 @@ let rate = invoice.rate * (data.rate / data.invoiceRate); $effect(() => { - if (typeof tipPercent === "undefined") tipPercent = (tip / amount) * 100; - tip = Math.round((amount / 100) * untrack(() => tipPercent)); + tip = Math.round((amount / 100) * tipPercent); }); let amountFiat = $derived(parseFloat(((amount * rate) / sats).toFixed(2))); diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte index 609e795b..c31ce60d 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -17,6 +17,10 @@ import { HDKey } from "@scure/bip32"; import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english.js"; + import { + getRememberedWalletPassword, + forgetWalletPassword, + } from "$lib/passwordCache"; import Amount from "$comp/Amount.svelte"; @@ -28,9 +32,29 @@ let cancel = $state(() => (passwordPrompt = false)); let signed = $state(); - let togglePassword = () => (passwordPrompt = !passwordPrompt); + let trySignWithCachedPassword = async () => { + const cached = getRememberedWalletPassword(); + if (!cached) return false; + try { + password = cached; + await signTx(); + return true; + } catch (e) { + forgetWalletPassword(); + return false; + } + }; + + let togglePassword = async () => { + if (passwordPrompt) { + passwordPrompt = false; + return; + } + if (await trySignWithCachedPassword()) return; + passwordPrompt = true; + }; let handler = ({ cancel }) => { - if (account.seed && !signed) cancel(), togglePassword(); + if (account.seed && !signed) cancel(), void togglePassword(); return async ({ result }) => { if (result.type === "redirect") { goto(result.location); |
