diff options
| author | Adam Soltys <[email protected]> | 2024-11-26 13:08:08 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-11-26 13:20:53 -0800 |
| commit | 9b3e117eafbba224b86dd7f8b3c56a0370f72d5b (patch) | |
| tree | a670c3ae954d5075c851126c2aca7a1a45d1b41d /src | |
| parent | 2761363d694d3f24600abbe656769f64d7a67605 (diff) | |
| download | coinos-ui-9b3e117eafbba224b86dd7f8b3c56a0370f72d5b.tar.gz coinos-ui-9b3e117eafbba224b86dd7f8b3c56a0370f72d5b.zip | |
bip21 and socket fixes
Diffstat (limited to 'src')
20 files changed, 249 insertions, 313 deletions
diff --git a/src/components/Account.svelte b/src/components/Account.svelte index c378b9a3..4db77066 100644 --- a/src/components/Account.svelte +++ b/src/components/Account.svelte @@ -1,20 +1,13 @@ <script> - import { run } from 'svelte/legacy'; + import { run } from "svelte/legacy"; import { goto } from "$app/navigation"; import Icon from "$comp/Icon.svelte"; import Balance from "$comp/Balance.svelte"; import { t } from "$lib/translations"; - let { - user, - rate, - account, - last - } = $props(); - let { name, seed, balance, id } = $state(account); - - let refresh = (a) => ({ seed, balance, id } = a); + let { user, rate, account, last } = $props(); + let { name, seed, balance, id } = $derived(account); let setAccount = () => (document.cookie = `aid=${id}; path=/; max-age=86400`); let go = () => { @@ -22,9 +15,6 @@ goto("/payments"); }; let type = $derived(seed ? "Savings" : "Cash"); - run(() => { - refresh(account); - }); </script> <div @@ -47,16 +37,18 @@ </div> <div class="flex justify-center w-full text-xl gap-2"> - <a href={`/invoice`} class="contents" onclick={setAccount}> + <a href={"/invoice"} class="contents" onclick={setAccount}> <button class="btn !w-auto flex-grow"> - <iconify-icon icon="ph:hand-coins-bold" width="32" flip="horizontal"></iconify-icon> + <iconify-icon icon="ph:hand-coins-bold" width="32" flip="horizontal" + ></iconify-icon> <div class="my-auto">{$t("user.dashboard.receive")}</div> </button> </a> <a href={`/send`} class="contents grow" onclick={setAccount}> <button type="button" class="btn !w-auto flex-grow"> - <iconify-icon icon="ph:paper-plane-right-bold" width="32"></iconify-icon> + <iconify-icon icon="ph:paper-plane-right-bold" width="32" + ></iconify-icon> <div class="my-auto">{$t("user.dashboard.send")}</div> </button> </a> diff --git a/src/components/InvoiceActions.svelte b/src/components/InvoiceActions.svelte index 0b5dca55..cd66fc36 100644 --- a/src/components/InvoiceActions.svelte +++ b/src/components/InvoiceActions.svelte @@ -35,7 +35,7 @@ </a> {/if} - {#if txt.startsWith("ln")} + {#if txt.length > 120} <div class="w-full flex justify-center gap-2 flex-wrap"> <button class="btn" onclick={() => (showQr = !showQr)}> {#if showQr} diff --git a/src/components/InvoiceData.svelte b/src/components/InvoiceData.svelte index c6721829..b511c599 100644 --- a/src/components/InvoiceData.svelte +++ b/src/components/InvoiceData.svelte @@ -26,7 +26,7 @@ </div> {/if} -{#if !showQr || !txt.startsWith("ln")} +{#if !showQr || txt.length <= 120} <div class="break-all text-center text-secondary text-xl"> {txt} </div> diff --git a/src/lib/bip21.ts b/src/lib/bip21.ts new file mode 100644 index 00000000..2642afab --- /dev/null +++ b/src/lib/bip21.ts @@ -0,0 +1,48 @@ +export function decode(uri, allowedSchemes = ["bitcoin", "liquidnetwork"]) { + // Determine the scheme from the URI + const split = uri.indexOf(":"); + if (split === -1) { + throw new Error("Invalid URI: Missing scheme"); + } + + const urnScheme = uri.slice(0, split).toLowerCase(); + if (!allowedSchemes.includes(urnScheme)) { + throw new Error(`Invalid scheme: ${urnScheme}`); + } + + // Extract address and query string + const queryStart = uri.indexOf("?"); + const address = uri.slice( + split + 1, + queryStart === -1 ? undefined : queryStart, + ); + const query = queryStart === -1 ? "" : uri.slice(queryStart + 1); + const options = Object.fromEntries(new URLSearchParams(query)); + + const n = Number(options?.amount); + if (!Number.isFinite(n)) throw new Error("Invalid amount"); + if (n < 0) throw new Error("Invalid amount"); + + return { scheme: urnScheme, address, options }; +} + +export function encode( + address, + options = { amount: "" }, + urnScheme = "bitcoin", +) { + if (!address || typeof address !== "string") { + throw new TypeError("Invalid address"); + } + + if (!["bitcoin", "liquidnetwork"].includes(urnScheme)) { + throw new Error(`Unsupported scheme: ${urnScheme}`); + } + + const n = Number(options?.amount); + if (!Number.isFinite(n)) throw new Error("Invalid amount"); + if (n < 0) throw new Error("Invalid amount"); + + const query = new URLSearchParams(options).toString(); + return `${urnScheme}:${address}${query ? `?${query}` : ""}`; +} diff --git a/src/lib/invoice.ts b/src/lib/invoice.ts index 9563d924..a48e02a4 100644 --- a/src/lib/invoice.ts +++ b/src/lib/invoice.ts @@ -2,7 +2,6 @@ import { auth, get, post } from "$lib/utils"; import { redirect } from "@sveltejs/kit"; export default async ({ cookies, request }) => { - console.log("LOADING"); const form = await request.formData(); const aid = form.get("aid") || cookies.get("aid"); diff --git a/src/lib/parse.ts b/src/lib/parse.ts index 6483aaa0..7ddb2104 100644 --- a/src/lib/parse.ts +++ b/src/lib/parse.ts @@ -1,7 +1,7 @@ import { PUBLIC_DOMAIN } from "$env/static/public"; +import { decode } from "$lib/bip21"; import { get, isLiquid, post, sats } from "$lib/utils"; import { redirect } from "@sveltejs/kit"; -import bip21 from "bip21"; import { validate } from "bitcoin-address-validation"; export default async (s, host) => { @@ -21,6 +21,23 @@ export default async (s, host) => { } catch (e) {} } + // bitcoin + if ( + t.toLowerCase().startsWith("bitcoin:") || + t.toLowerCase().startsWith("liquidnetwork:") + ) { + ({ + address: t, + options: { amount }, + } = decode(t)); + } + + if (validate(t) || isLiquid(t)) { + let r = `/send/bitcoin/${t}`; + if (amount) r += `/${Math.round(amount * sats)}`; + redirect(307, r); + } + // lightning if (t.toLowerCase().startsWith("lightning:")) t = t.toLowerCase().replace("lightning:", ""); @@ -47,19 +64,6 @@ export default async (s, host) => { } } - // bitcoin - if (t.toLowerCase().startsWith("bitcoin:")) - ({ - address: t, - options: { amount }, - } = bip21.decode(t)); - - if (validate(t) || isLiquid(t)) { - let r = `/send/bitcoin/${t}`; - if (amount) r += `/${Math.round(amount * sats)}`; - redirect(307, r); - } - if (t.startsWith("cashu")) { const { id } = await post("/cash", { token: t }); redirect(307, `/ecash/${id}`); diff --git a/src/lib/socket.ts b/src/lib/socket.ts index 566e989d..bd8494fa 100644 --- a/src/lib/socket.ts +++ b/src/lib/socket.ts @@ -3,9 +3,10 @@ import { navigating, page } from "$app/stores"; import { PUBLIC_SOCKET } from "$env/static/public"; import { event, invoice, last, request } from "$lib/store"; import { s, sleep, success, wait } from "$lib/utils"; +import cookies from "js-cookie"; import { get } from "svelte/store"; -let socket; +export let socket; let token; export const auth = () => token && send("login", token); @@ -13,8 +14,9 @@ export const auth = () => token && send("login", token); export const send = async (type, data) => { try { await wait(() => socket && socket.readyState === 1, 1000, 10); - } catch (e: any) { - if (e.message === "timeout") reconnectToWebsocket(); + } catch (e) { + const { message } = e as Error; + if (message === "timeout") reconnectToWebsocket(); } await wait(() => socket.readyState === 1, 1000, 10); @@ -47,8 +49,14 @@ export const messages = (data) => ({ url: { pathname }, } = get(page); + const username = cookies.get("username"); + if (amount > 0) { - if (pathname.includes("receive")) { + if ( + pathname.includes(`/${username}`) || + pathname.includes("/receive") || + pathname.includes("/invoice") + ) { await wait(() => !get(navigating)); await goto(`/invoice/${iid}`); } else success(`${confirmed ? "Received" : "Detected"} ⚡️${s(amount)}!`); @@ -63,7 +71,14 @@ const maxReconnectDelay = 16000; let currentReconnectDelay = initialReconnectDelay; +let connecting; export function connect(t) { + if (connecting) return; + connecting = true; + setTimeout(() => { + connecting = false; + }, 5000); + token = t; if (socket) return auth(); @@ -87,6 +102,7 @@ async function onWebsocketMessage(msg) { function onWebsocketOpen() { currentReconnectDelay = initialReconnectDelay; + send("heartbeat", token); } function onWebsocketClose() { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a15bb8e7..dc7182e3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -151,7 +151,7 @@ export const info = (m: string) => { export const login = async ( user: { username: string; password: string }, - cookies: any, + cookies, ip: string, ) => { const maxAge = 380 * 24 * 60 * 60; @@ -182,7 +182,7 @@ export const login = async ( cookies.set("token", token, opts); }; -export const auth = (cookies: any) => ({ +export const auth = (cookies) => ({ authorization: `Bearer ${cookies.get("token")}`, }); diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts index 3c5b3b3e..d490a684 100644 --- a/src/routes/(app)/+layout.server.ts +++ b/src/routes/(app)/+layout.server.ts @@ -3,7 +3,7 @@ import { error, redirect } from "@sveltejs/kit"; export async function load({ cookies, request, url, params }) { const { pathname } = url; - const { username } = params; + const { id, username } = params; const token = cookies.get("token"); let rate; let rates = { USD: 1 }; @@ -20,7 +20,14 @@ export async function load({ cookies, request, url, params }) { } let subject; - if (username) { + + if (url.pathname.startsWith("/invoice") && id) { + try { + ({ user: subject } = await get(`/invoice/${id}`)); + } catch (e) { + console.log("unable to fetch invoice", id, e); + } + } else if (username) { try { subject = await get(`/users/${username}`); } catch (e) { diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index aaaf543f..49e4bdb9 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -1,7 +1,7 @@ <script> import { SvelteToast } from "@zerodevx/svelte-toast"; import { onDestroy, onMount } from "svelte"; - import { close, connect, send } from "$lib/socket"; + import { close, connect, send, socket } from "$lib/socket"; import { last, invoice, @@ -16,7 +16,6 @@ import { browser } from "$app/environment"; import LoadingSplash from "$comp/LoadingSplash.svelte"; import AppHeader from "$comp/AppHeader.svelte"; - import Invoice from "$comp/Invoice.svelte"; import Password from "$comp/Password.svelte"; import { getCookie, warning, protectedRoutes } from "$lib/utils"; import { t, locale, loading } from "$lib/translations"; @@ -29,7 +28,6 @@ $themeStore = theme; $effect(() => (theme = $themeStore)); - $effect(() => browser && connect(token)); afterNavigate(() => { if (user) { @@ -64,14 +62,13 @@ } }); - let lost, - checkTimer, - expireTimer, + let checkTimer, counter = 0; let checkSocket = () => { counter++; - lost = Date.now() - $last > 30000; + let lost = socket?.readyState !== 1 || !$last || Date.now() - $last > 30000; + console.log("LOST?", lost); if (lost) connect(token); if (counter > 5) { send("heartbeat", token); @@ -85,7 +82,6 @@ if (browser) { close(); clearTimeout(checkTimer); - clearTimeout(expireTimer); } }); </script> diff --git a/src/routes/(app)/[username]/+layout.server.ts b/src/routes/(app)/[username]/+layout.server.ts index 1dd163e9..bff8848c 100644 --- a/src/routes/(app)/[username]/+layout.server.ts +++ b/src/routes/(app)/[username]/+layout.server.ts @@ -15,7 +15,7 @@ export const load = async ({ const expires = new Date(); expires.setSeconds(expires.getSeconds() + 380 * 24 * 60 * 60); - cookies.set("username", username, { path: "/", expires }); + cookies.set("username", username, { path: "/", expires, httpOnly: false }); return { subject, user }; } catch (e) { diff --git a/src/routes/(app)/[username]/receive/+page.svelte b/src/routes/(app)/[username]/receive/+page.svelte index ddb17e71..89f3a1f3 100644 --- a/src/routes/(app)/[username]/receive/+page.svelte +++ b/src/routes/(app)/[username]/receive/+page.svelte @@ -38,16 +38,18 @@ tip; let setType = async (type) => { - invoice.type = type; - ({ id } = await post(`/invoice`, { - invoice, - user: { username, currency }, - })); + $showQr = true; + if (type === types.lightning && !amount) + goto(`/${username}/receive`, { invalidateAll: true, noScroll: true }); + else { + invoice.type = type; + ({ id } = await post(`/invoice`, { + invoice, + user: { username, currency }, + })); - goto(`/invoice/${id}?options=true`, { - invalidateAll: true, - noScroll: true, - }); + goto(`/invoice/${id}?options=true`, { invalidateAll: true, noScroll: true }); + } }; let setAmount = async (e) => { diff --git a/src/routes/(app)/invoice/[id]/[email protected] b/src/routes/(app)/invoice/[id]/[email protected] deleted file mode 100644 index 13b121d8..00000000 --- a/src/routes/(app)/invoice/[id]/[email protected] +++ /dev/null @@ -1,110 +0,0 @@ -<script> - import { SvelteToast } from "@zerodevx/svelte-toast"; - import { onDestroy, onMount } from "svelte"; - import { close, connect, send } from "$lib/socket"; - import { - last, - invoice, - request, - ndef, - passwordPrompt, - password, - pin, - theme as themeStore, - } from "$lib/store"; - import { page } from "$app/stores"; - import { browser } from "$app/environment"; - import LoadingSplash from "$comp/LoadingSplash.svelte"; - import AppHeader from "$comp/AppHeader.svelte"; - import Invoice from "$comp/Invoice.svelte"; - import Password from "$comp/Password.svelte"; - import { getCookie, warning, protectedRoutes } from "$lib/utils"; - import { t, locale, loading } from "$lib/translations"; - import { goto, onNavigate } from "$app/navigation"; - - let { data, children } = $props(); - - let { user, subject, token } = $derived(data); - let { theme } = $state(data); - - $themeStore = theme; - $effect(() => (theme = $themeStore)); - $effect(() => browser && connect(token)); - - onMount(async () => { - if (browser) { - checkSocket(); - $pin = getCookie("pin"); - - if (window.NDEFReader && user.nfc) { - try { - $ndef = new NDEFReader(); - await $ndef.scan(); - - $ndef.addEventListener("readingerror", (e) => { - console.log("nfc error", e); - }); - - $ndef.addEventListener("reading", ({ message, serialNumber }) => { - goto(`/send/${message}`); - }); - } catch (error) { - console.log("Argh! " + error); - } - } - } - }); - - let lost, - checkTimer, - expireTimer, - counter = 0; - - let checkSocket = () => { - counter++; - lost = Date.now() - $last > 30000; - if (lost) connect(token); - if (counter > 5) { - send("heartbeat", token); - counter = 0; - } - - checkTimer = setTimeout(checkSocket, 1000); - }; - - onDestroy(() => { - if (browser) { - close(); - clearTimeout(checkTimer); - clearTimeout(expireTimer); - } - }); -</script> - -{#if browser && user && $passwordPrompt} - <Password {user} /> -{/if} - -<svelte:head> - {#if subject} - <title>coinos - {subject.username}</title> - <meta name="lightning" content={`lnurlp:${subject.username}@coinos.io`} /> - {:else} - <title>coinos</title> - {/if} - - {#if subject?.profile} - <meta name="og:image" content={`/api/public/${subject.profile}.webp`} /> - {:else} - <meta property="og:image" content="/images/logo.webp" /> - {/if} -</svelte:head> - -<SvelteToast options={{ reversed: true, intro: { y: 192 } }} /> - -<main class="pb-4 min-h-dvh" data-theme={theme}> - <AppHeader {user} {subject} /> - {#if !$loading} - {@render children?.()} - {/if} -</main> diff --git a/src/routes/(app)/invoice/[id]/+page.svelte b/src/routes/(app)/invoice/[id]/+page.svelte index 8c6e2b75..3ad20f68 100644 --- a/src/routes/(app)/invoice/[id]/+page.svelte +++ b/src/routes/(app)/invoice/[id]/+page.svelte @@ -56,13 +56,21 @@ }); let setType = async (type) => { - invoice.type = type; - ({ id } = await post(`/invoice`, { - invoice, - user: { username, currency }, - })); + $showQr = true; + if (type === types.lightning && !amount) + goto(`/${username}/receive`, { invalidateAll: true, noScroll: true }); + else { + invoice.type = type; + ({ id } = await post(`/invoice`, { + invoice, + user: { username, currency }, + })); - goto(`./${id}?options=true`, { invalidateAll: true, noScroll: true }); + goto(`/invoice/${id}?options=true`, { + invalidateAll: true, + noScroll: true, + }); + } }; let setAmount = async (e) => { @@ -74,7 +82,7 @@ if (typeof $amountPrompt === "undefined") $amountPrompt = true; invoice.amount = newAmount; invoice.tip = 0; - + ({ id } = await post(`/invoice`, { invoice, user: { username, currency }, diff --git a/src/routes/(app)/payments/[...page]/+page.server.ts b/src/routes/(app)/payments/[...page]/+page.server.ts index 268fd32b..05ed2d64 100644 --- a/src/routes/(app)/payments/[...page]/+page.server.ts +++ b/src/routes/(app)/payments/[...page]/+page.server.ts @@ -1,4 +1,4 @@ -import { get, auth } from "$lib/utils"; +import { auth, get } from "$lib/utils"; const limit = 25; diff --git a/src/routes/(app)/payments/[...page]/+page.svelte b/src/routes/(app)/payments/[...page]/+page.svelte index cda7c350..146a286d 100644 --- a/src/routes/(app)/payments/[...page]/+page.svelte +++ b/src/routes/(app)/payments/[...page]/+page.svelte @@ -11,8 +11,9 @@ import { goto, invalidate } from "$app/navigation"; import locales from "$lib/locales"; let { data } = $props(); + let { start, end, user, rates, payments, incoming, outgoing } = + $derived(data); - let { start, end, user, rates, payments } = $state(data); let locale = locales[user.language]; let change = ({ target: { value } }) => goto(value); @@ -55,19 +56,13 @@ ); let p = $state(), - incoming = $state(), - outgoing = $state(), pages = $state([]); - run(() => { - data && - ({ page: p, pages, start, end, incoming, outgoing, payments } = data); - }); - run(() => { - $page && ($newPayment = false); - }); - run(() => { - $newPayment && invalidate(`/users/${user.username}`); + $effect(() => { + if ($newPayment) { + $newPayment = false; + invalidate(`/users/${user.username}`); + } }); let path = $derived( @@ -204,104 +199,101 @@ <div class="text-base"> {#each payments as p, i} - <div - class="grid grid-cols-12 border-b border-base-200 hover:bg-base-200 px-1 py-2 lg:p-4 cursor-pointer" - class:border-b-0={i === payments.length - 1} - class:text-error={p.amount < 0} - onclick={() => goto(`/payment/${p.id}`)} - > - <div class="whitespace-nowrap my-auto col-span-3"> - <div - class="font-bold flex items-center" - > - <div class="flex items-center"> - <iconify-icon - icon="ph:lightning-fill" - width="24" - class="text-yellow-300" - ></iconify-icon> - <div>{s(Math.abs(p.amount))}</div> - </div> + <div + class="grid grid-cols-12 border-b border-base-200 hover:bg-base-200 px-1 py-2 lg:p-4 cursor-pointer" + class:border-b-0={i === payments.length - 1} + class:text-error={p.amount < 0} + onclick={() => goto(`/payment/${p.id}`)} + > + <div class="whitespace-nowrap my-auto col-span-3"> + <div class="font-bold flex items-center"> + <div class="flex items-center"> + <iconify-icon + icon="ph:lightning-fill" + width="24" + class="text-yellow-300" + ></iconify-icon> + <div>{s(Math.abs(p.amount))}</div> </div> + </div> - <div class="text-secondary flex items-center text-base"> - {f(Math.abs(p.amount) * (p.rate / sats), p.currency)} + <div class="text-secondary flex items-center text-base"> + {f(Math.abs(p.amount) * (p.rate / sats), p.currency)} - {#if p.tip} - <span class="text-sm text-secondary"> - +{Math.round((p.tip / Math.abs(p.amount)) * 100)}% - </span> - {/if} - </div> + {#if p.tip} + <span class="text-sm text-secondary"> + +{Math.round((p.tip / Math.abs(p.amount)) * 100)}% + </span> + {/if} </div> + </div> - <div - class="flex my-auto col-span-6 truncate text-ellipsis overflow-hidden mx-auto" - > - {#if p.type === types.pot} - <a href={`/pot/${p.memo}`}> - <div class="text-secondary flex"> - <div class="my-auto mr-1"> - <img src="/images/moneypot.png" class="w-12" alt="Pot" /> - </div> - - <div class="my-auto">Pot</div> - </div> - </a> - {:else if p.with} - <div class="flex"> - <div class="my-auto"> - <Avatar user={p.with} size={12} disabled={true} /> - </div> - <div class="my-auto ml-1 text-secondary"> - {p.with.username} + <div + class="flex my-auto col-span-6 truncate text-ellipsis overflow-hidden mx-auto" + > + {#if p.type === types.pot} + <a href={`/pot/${p.memo}`}> + <div class="text-secondary flex"> + <div class="my-auto mr-1"> + <img src="/images/moneypot.png" class="w-12" alt="Pot" /> </div> - </div> - {:else} - <div class="text-secondary flex items-center gap-1"> - {#if p.type === types.lightning} - <iconify-icon - icon="ph:lightning-fill" - class="text-yellow-300 text-3xl" - ></iconify-icon> - {:else if p.type === types.ecash} - <img src="/images/cash.png" class="w-12" /> - {:else if p.type === types.reconcile} - <iconify-icon icon="ph:scales-bold" width="32" - ></iconify-icon> - {:else if p.type === types.bitcoin} - <iconify-icon icon="logos:bitcoin" class="text-3xl" - ></iconify-icon> - {:else if p.type === types.liquid} - <div class="my-auto"> - <img - src="/images/liquid.svg" - class="w-10 border-4 border-transparent" - alt="Liquid" - /> - </div> - {/if} + <div class="my-auto">Pot</div> + </div> + </a> + {:else if p.with} + <div class="flex"> + <div class="my-auto"> + <Avatar user={p.with} size={12} disabled={true} /> + </div> + <div class="my-auto ml-1 text-secondary"> + {p.with.username} + </div> + </div> + {:else} + <div class="text-secondary flex items-center gap-1"> + {#if p.type === types.lightning} + <iconify-icon + icon="ph:lightning-fill" + class="text-yellow-300 text-3xl" + ></iconify-icon> + {:else if p.type === types.ecash} + <img src="/images/cash.png" class="w-12" /> + {:else if p.type === types.reconcile} + <iconify-icon icon="ph:scales-bold" width="32"></iconify-icon> + {:else if p.type === types.bitcoin} + <iconify-icon icon="logos:bitcoin" class="text-3xl" + ></iconify-icon> + {:else if p.type === types.liquid} <div class="my-auto"> - {p.amount > 0 - ? p.confirmed - ? "Received" - : "Pending" - : "Sent"} + <img + src="/images/liquid.svg" + class="w-10 border-4 border-transparent" + alt="Liquid" + /> </div> - </div> - {/if} - </div> + {/if} - <div class="text-secondary text-right text-sm my-auto col-span-3"> - <div> - {format(new Date(p.created), "h:mm aaa", { locale })} - </div> - <div> - {format(new Date(p.created), "MMM d, yy", { locale })} + <div class="my-auto"> + {p.amount > 0 + ? p.confirmed + ? "Received" + : "Pending" + : "Sent"} + </div> </div> + {/if} + </div> + + <div class="text-secondary text-right text-sm my-auto col-span-3"> + <div> + {format(new Date(p.created), "h:mm aaa", { locale })} + </div> + <div> + {format(new Date(p.created), "MMM d, yy", { locale })} </div> </div> + </div> {:else} <p class="text-secondary text-lg text-center">{$t("payments.empty")}</p> {/each} diff --git a/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts b/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts index 68191311..a321ec43 100644 --- a/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts +++ b/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts @@ -4,20 +4,22 @@ import { redirect } from "@sveltejs/kit"; export async function load({ cookies, params, parent }) { const { user } = await parent(); const aid = cookies.get("aid"); - const { address } = params; + const { address, amount } = params; const { balance } = await get(`/account/${aid}`, auth(cookies)); + let invoice; try { - const invoice = await get(`/invoice/${address}`); - - if (invoice) { - const recipient = await get(`/users/${invoice.uid}`); - if (recipient?.id !== user.id) - redirect(307, `/pay/${recipient.username}`); - } + invoice = await get(`/invoice/${address}`); } catch (e) { // invoice not found } + if (invoice) { + const recipient = await get(`/users/${invoice.uid}`); + let r = `/pay/${recipient.username}`; + if (amount) r += `/${amount}`; + if (recipient?.id !== user.id) redirect(307, r); + } + return { balance }; } diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts index 1122e689..9156bf7b 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts @@ -39,9 +39,10 @@ export const actions = { try { const body = await fd(request); p = await post("/bitcoin/send", body, auth(cookies)); - } catch (e: any) { + } catch (e) { console.log("problem sending bitcoin", e); - return fail(400, { address, amount, feeRate, message: e.message }); + const { message } = e as Error; + return fail(400, { address, amount, feeRate, message }); } redirect(307, `/sent/${p.id}`); 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 f37d4479..298280df 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -5,19 +5,10 @@ import { t } from "$lib/translations"; import { enhance } from "$app/forms"; import Toggle from "$comp/Toggle.svelte"; - import Icon from "$comp/Icon.svelte"; import Spinner from "$comp/Spinner.svelte"; import WalletPass from "$comp/WalletPass.svelte"; import { page } from "$app/stores"; - import { - toFiat, - f, - focus, - s, - sat, - closest, - network, - } from "$lib/utils"; + import { toFiat, f, focus, s, sat, closest, network } from "$lib/utils"; import { pin } from "$lib/store"; import { goto, invalidate } from "$app/navigation"; import { rate } from "$lib/store"; @@ -30,6 +21,8 @@ import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; + import Amount from "$comp/Amount.svelte"; + let { data, form } = $props(); let { encode, decode } = hexUtil; @@ -130,34 +123,20 @@ {:else} <div class="text-xl text-secondary break-all">{address}</div> - <div class="text-center"> - <h2 class="text-2xl md:text-3xl font-semibold"> - {f(toFiat(amount, $rate), currency)} - </h2> - <h3 class="text-secondary md:text-lg mb-6 mt-1">⚡️{s(amount)}</h3> - </div> + <Amount {amount} rate={$rate} {currency} /> <div class="text-center"> <h2 class="text-secondary text-lg">{$t("payments.networkFee")}</h2> <div class="flex flex-wrap gap-4 justify-center"> - <select - bind:value={feeRate} - onchange={setFee} - class="border text-lg bg-white p-4 rounded-2xl text-center my-auto" - > + <select bind:value={feeRate} onchange={setFee}> {#each Object.keys(feeNames) as feeName} <option value={fees[feeName]} - >{feeNames[feeName]} — ⚡️{fees[feeName]}/vb</option + >{feeNames[feeName]} — {fees[feeName]} sats/vb</option > {/each} </select> - <div class="my-auto"> - <h2 class="text-xl"> - {f(toFiat(fee, $rate), currency)} - </h2> - <h3 class="text-secondary">⚡️{s(fee)}</h3> - </div> + <Amount amount={fee} rate={$rate} {currency} /> </div> </div> diff --git a/src/routes/(app)/send/invoice/[id]/+page.svelte b/src/routes/(app)/send/invoice/[id]/+page.svelte index f5259170..d2b3727c 100644 --- a/src/routes/(app)/send/invoice/[id]/+page.svelte +++ b/src/routes/(app)/send/invoice/[id]/+page.svelte @@ -103,7 +103,7 @@ <Amount {amount} {tip} {rate} {currency} /> - <h1 class="text-xl md:text-2xl text-secondary"> + <h1 class="text-xl text-secondary"> {$t("payments.to")} </h1> |
