diff options
| author | Jurjen de Vries <[email protected]> | 2025-09-11 00:48:15 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-09-11 00:48:15 +0200 |
| commit | 0c69c9848c6b98c92609d209103efc60003c9723 (patch) | |
| tree | 445121def885f13d0f80c06864ad2ff4d5ef95a1 /src/routes/(app)/send | |
| parent | 2abcf6e435fca5d86960f3ba4228002bfb0a19e6 (diff) | |
| parent | 9277b2921e09ee9dd5f39cf4af30a2330f4d95d1 (diff) | |
| download | coinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.tar.gz coinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.zip | |
Merge branch 'coinos:master' into master
Diffstat (limited to 'src/routes/(app)/send')
13 files changed, 163 insertions, 307 deletions
diff --git a/src/routes/(app)/send/[...text]/+page.svelte b/src/routes/(app)/send/[...text]/+page.svelte index 183b4b51..52b92501 100644 --- a/src/routes/(app)/send/[...text]/+page.svelte +++ b/src/routes/(app)/send/[...text]/+page.svelte @@ -51,6 +51,20 @@ invalidate("app:contacts"); }; + let trust = async (e, { id, trusted }) => { + e.preventDefault(); + e.stopPropagation(); + + if (trusted) { + await post("/post/trust/delete", { id }); + } else { + await post("/post/trust", { id }); + } + + if (all) all = await get("/contacts"); + invalidate("app:contacts"); + }; + $effect(() => { if (browser && pasted && text) el.click() && (pasted = false); }); @@ -74,17 +88,6 @@ </div> {/if} - <textarea - use:focus - name="text" - placeholder={$t("user.send.placeholder")} - onkeypress={keypress} - class="w-full p-4 border rounded-xl h-32 text-xl" - bind:value={text} - onpaste={() => (pasted = true)} - autocapitalize="none" - ></textarea> - <div class="flex gap-2"> <a href="/scan" class="contents"> <button type="button" class="btn !w-auto flex-grow"> @@ -101,19 +104,23 @@ </button> </div> + <textarea + use:focus + name="text" + placeholder={$t("user.send.placeholder")} + onkeypress={keypress} + class="w-full p-4 border rounded-xl h-32 text-xl" + bind:value={text} + onpaste={() => (pasted = true)} + autocapitalize="none" + ></textarea> + <button bind:this={el} type="submit" class="btn btn-accent"> <iconify-icon noobserver icon="ph:paper-plane-right-bold" width="32" ></iconify-icon> <div class="my-auto">{$t("user.send.next")}</div> </button> - <a href="/send/ecash" class="block"> - <button type="button" class="btn"> - <img src="/images/cash.png" class="w-8" /> - <div class="my-auto">{$t("payments.createEcash")}</div> - </button> - </a> - <a href="/send/fund" class="block"> <button type="button" class="btn"> <iconify-icon @@ -139,12 +146,20 @@ <div class="my-auto text-left"> <p class="ml-1 text-lg break-words">{c.username}</p> </div> - <button class="ml-auto" onclick={(e) => pin(e, c)}> - <iconify-icon - icon={c.pinned ? "ph:push-pin-fill" : "ph:push-pin-bold"} - width={32} - ></iconify-icon> - </button> + <div class="flex ml-auto gap-1"> + <button onclick={(e) => pin(e, c)}> + <iconify-icon + icon={c.pinned ? "ph:push-pin-fill" : "ph:push-pin-bold"} + width={32} + ></iconify-icon> + </button> + <button class="ml-auto" onclick={(e) => trust(e, c)}> + <iconify-icon + icon={c.trusted ? "ph:star-fill" : "ph:star-bold"} + width={32} + ></iconify-icon> + </button> + </div> </div> </a> {/each} @@ -152,7 +167,7 @@ </div> {/if} - {#if !loaded} + {#if !loaded && contacts.length > 0} <button onclick={loadMore} type="button" class="btn" >{$t("user.loadMore")}</button > diff --git a/src/routes/(app)/send/bitcoin/[address]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/+page.svelte index 060473ec..529e2691 100644 --- a/src/routes/(app)/send/bitcoin/[address]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/+page.svelte @@ -1,25 +1,32 @@ <script> + import { tick } from "svelte"; import { t } from "$lib/translations"; import Icon from "$comp/Icon.svelte"; import Numpad from "$comp/Numpad.svelte"; import { page } from "$app/stores"; import { rate } from "$lib/store"; - import { fail, s } from "$lib/utils"; + import { loc, fail, s } from "$lib/utils"; let { data } = $props(); let { balance, user } = data; let { address } = $page.params; let { currency, username } = user; + let locale = loc(user); let amount = $state(0); - let submit = $state(), fiat = $state(); + let a = $state(0); + let submit = $state(), + fiat = $state(); $effect(() => ($rate = data.rate)); + $effect(() => (amount = a)); - let setMax = (e) => { + let setMax = async (e) => { e.preventDefault(); fiat = false; amount = balance; + await tick(); + submit.click(); }; </script> @@ -28,7 +35,7 @@ <div class="text-xl text-secondary break-all">{address}</div> - <Numpad bind:amount bind:fiat {currency} {submit} bind:rate={$rate} /> + <Numpad bind:amount={a} bind:fiat {currency} {submit} bind:rate={$rate} {locale} /> <div class="flex justify-center gap-2"> <button @@ -39,7 +46,12 @@ > <form action={`/send/bitcoin/${address}/${amount}`} class="contents"> - <button use:focus bind:this={submit} type="submit" class="btn !w-auto grow btn-accent"> + <button + use:focus + bind:this={submit} + type="submit" + class="btn !w-auto grow btn-accent" + > {$t("payments.next")} </button> </form> 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 5f20d1bd..ddee9c1c 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 @@ -12,7 +12,7 @@ export async function load({ const aid = cookies.get("aid") || user.id; try { - const { fee, fees, inputs, ourfee, hex } = await post( + const { fee, fees, inputs, ourfee, hex, subtract } = await post( "/bitcoin/fee", { address, amount, feeRate, aid }, auth(cookies), @@ -29,6 +29,7 @@ export async function load({ feeRate, rate: rates[user.currency], ourfee, + subtract, hex, inputs, }; 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 7d9b8f64..1551cc2c 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -1,6 +1,4 @@ <script> - import { run } from "svelte/legacy"; - import { tick } from "svelte"; import { t } from "$lib/translations"; import { enhance } from "$app/forms"; @@ -15,7 +13,6 @@ import { applyAction } from "$app/forms"; import { hex as hexUtil } from "@scure/base"; import * as btc from "@scure/btc-signer"; - import { hash160 } from "@scure/btc-signer/utils"; import { decrypt } from "nostr-tools/nip49"; import { HDKey } from "@scure/bip32"; import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39"; @@ -71,8 +68,18 @@ let toggle = () => (submitting = !submitting); - let { account, amount, address, message, fee, fees, ourfee, hex, inputs } = - $derived(data); + let { + account, + amount, + address, + message, + fee, + fees, + subtract, + ourfee, + hex, + inputs, + } = $derived(data); let { feeRate } = $state(data); @@ -116,7 +123,11 @@ {:else} <div class="text-xl text-secondary break-all">{address}</div> - <Amount {amount} rate={$rate} {currency} /> + <Amount + amount={subtract ? amount - fee - ourfee : amount} + rate={$rate} + {currency} + /> <div class="text-center"> <h2 class="text-secondary text-lg">{$t("payments.networkFee")}</h2> @@ -137,14 +148,7 @@ <div class="text-center"> <h2 class="text-secondary text-lg">{$t("payments.platformFee")}</h2> - <div class="flex flex-wrap gap-4 justify-center"> - <div class="my-auto"> - <h2 class="text-xl"> - {f(toFiat(ourfee, $rate), currency)} - </h2> - <h3 class="text-secondary">⚡️{s(ourfee)}</h3> - </div> - </div> + <Amount amount={ourfee} rate={$rate} {currency} /> </div> {/if} diff --git a/src/routes/(app)/send/ecash/+page.server.ts b/src/routes/(app)/send/ecash/+page.server.ts deleted file mode 100644 index b290fb95..00000000 --- a/src/routes/(app)/send/ecash/+page.server.ts +++ /dev/null @@ -1,26 +0,0 @@ -import getRates from "$lib/rates"; -import { auth, fd, post } from "$lib/utils"; -import { error, redirect } from "@sveltejs/kit"; - -export async function load({ parent }) { - const rates = await getRates(); - const { user } = await parent(); - return { rate: rates[user.currency] }; -} - -export const actions = { - default: async ({ cookies, request }) => { - let id; - const body = await fd(request); - - try { - ({ id } = await post("/mint", body, auth(cookies))); - } catch (e) { - const { message } = e as Error; - console.log("problem creating ecash", e); - error(400, { message }); - } - - redirect(307, `/ecash/${id}`); - }, -}; diff --git a/src/routes/(app)/send/ecash/+page.svelte b/src/routes/(app)/send/ecash/+page.svelte deleted file mode 100644 index b9c93661..00000000 --- a/src/routes/(app)/send/ecash/+page.svelte +++ /dev/null @@ -1,63 +0,0 @@ -<script> - import { enhance, applyAction } from "$app/forms"; - import { t } from "$lib/translations"; - import Icon from "$comp/Icon.svelte"; - import Numpad from "$comp/Numpad.svelte"; - import Spinner from "$comp/Spinner.svelte"; - import { page } from "$app/stores"; - import { rate } from "$lib/store"; - import { fail, s } from "$lib/utils"; - import handler from "$lib/handler"; - - let { data } = $props(); - - let { user } = $derived(data); - let { balance, currency, username } = $derived(user); - - let amount = $state(0); - let submit = $state(), - fiat = $state(); - - let setMax = (e) => { - e.preventDefault(); - fiat = false; - amount = balance; - }; - - let submitting = $state(); - let toggle = () => (submitting = !submitting); - $effect(() => ({ rate: $rate } = data)); -</script> - -<div class="container px-4 max-w-xl mx-auto space-y-5 text-center"> - <form use:enhance={handler(toggle)} method="POST" class="space-y-5"> - <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> - {$t("payments.fundingAmount")} - </h1> - <Numpad bind:amount bind:fiat {currency} {submit} bind:rate={$rate} /> - <input type="hidden" name="amount" bind:value={amount} /> - - <div class="flex gap-2"> - <button - type="button" - class="btn !w-auto grow" - onclick={setMax} - disabled={submitting} - onkeydown={setMax}>Max ⚡️{s(balance)}</button - > - - <button - use:focus - bind:this={submit} - type="submit" - class="btn btn-accent !w-auto grow" - > - {#if submitting} - <Spinner /> - {:else} - <div class="my-auto">{$t("payments.next")}</div> - {/if} - </button> - </div> - </form> -</div> diff --git a/src/routes/(app)/send/ecash/[request]/+page.server.ts b/src/routes/(app)/send/ecash/[request]/+page.server.ts deleted file mode 100644 index 6cc593cc..00000000 --- a/src/routes/(app)/send/ecash/[request]/+page.server.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { fd, get } from "$lib/utils"; -import { decodePaymentRequest } from "@cashu/cashu-ts"; -import { redirect } from "@sveltejs/kit"; - -export async function load({ cookies, params, url }) { - if (!cookies.get("token")) - redirect(307, `/login?redirect=${encodeURIComponent(url.pathname)}`); - - const req = { ...decodePaymentRequest(params.request) }; - const [{ target, type }] = req.transport; - - let recipient = { name: "", pubkey: null }; - if (type === "nostr") { - recipient = await get(`/profile/${target}`); - } else if (type === "post") { - recipient.name = new URL(target).hostname; - } - - return { req, recipient, type, target }; -} - -export const actions = { - setAmount: async ({ request }) => { - return await fd(request); - }, -}; diff --git a/src/routes/(app)/send/ecash/[request]/+page.svelte b/src/routes/(app)/send/ecash/[request]/+page.svelte deleted file mode 100644 index 42bc8e9d..00000000 --- a/src/routes/(app)/send/ecash/[request]/+page.svelte +++ /dev/null @@ -1,128 +0,0 @@ -<script lang="ts"> - import { goto } from "$app/navigation"; - import { focus, fail, post } from "$lib/utils"; - import { t } from "$lib/translations"; - import { enhance } from "$app/forms"; - import Amount from "$comp/Amount.svelte"; - import Numpad from "$comp/Numpad.svelte"; - import Spinner from "$comp/Spinner.svelte"; - import { rate, pin } from "$lib/store"; - import { wrapEvent } from "nostr-tools/nip17"; - import { getPrivateKey, sign, send } from "$lib/nostr"; - import { getDecodedToken } from "@cashu/cashu-ts"; - import type { PaymentRequestPayload } from "@cashu/cashu-ts"; - import { AbstractSimplePool } from "nostr-tools/abstract-pool"; - - const alwaysTrue: any = (t: Event) => { - t[Symbol("verified")] = true; - return true; - }; - - export const pool = new AbstractSimplePool({ verifyEvent: alwaysTrue }); - - const { data, form } = $props(); - - let formElement = $state(); - let submitting = $state(); - let submit = async (e) => { - e.preventDefault(); - submitting = true; - - try { - let { pid, token } = await post("/ecash/mint", { amount }); - let { proofs } = getDecodedToken(token); - let { id, mints, unit = "" } = req; - let mint = mints[0]; - - const payload = { - id, - mint, - unit, - proofs, - }; - - if (type === "nostr") { - let event = wrapEvent( - await getPrivateKey(user), - { publicKey: recipient.pubkey }, - JSON.stringify(payload), - ); - - await Promise.any(pool.publish(recipient.relays, event)); - } else if (type === "post") { - await post("/ecash/post", { target, payload }); - } - - goto(`/sent/${pid}`); - } catch (e) { - console.log(e); - fail(e.message); - } - - submitting = false; - }; - - let { recipient, req, user, type, target } = $derived({ - ...data, - ...form, - }); - let { currency } = $derived(user); - let a = $state(); - let next = $state(); - let toggle = () => (show = !show); - let amount = $derived(form?.amount || data.req.amount); - - $effect(() => ($rate ||= data.rate)); - $effect(() => form && (submitting = false)); -</script> - -<div class="container px-4 max-w-xl mx-auto text-center space-y-2"> - {#if amount} - {#if form?.message} - <div class="text-red-600 text-center"> - {$t(form.message)} - </div> - {/if} - - <div> - <h1 class="text-lg text-secondary"> - {$t("payments.send")} - </h1> - - <Amount {amount} rate={$rate} {currency} /> - </div> - - <div class="text-xl"> - <span class="text-secondary">{$t("payments.to")}</span> - <span class="break-words font-semibold">{recipient.name}</span> - </div> - - <form onsubmit={submit} class="space-y-2" bind:this={formElement}> - <input name="amount" value={amount} type="hidden" /> - <input name="pin" value={$pin} type="hidden" /> - - <button - type="submit" - class="btn btn-primary" - disabled={submitting} - use:focus - > - {#if submitting} - <Spinner /> - {:else} - {$t("payments.send")} - {/if} - </button> - </form> - {:else} - <form method="POST" action="?/setAmount" class="space-y-2" use:enhance> - <input type="hidden" value={a} name="amount" /> - <input name="rate" value={$rate} type="hidden" /> - - <Numpad bind:amount={a} {currency} bind:rate={$rate} submit={next} /> - <button type="submit" class="btn" bind:this={next} - >{$t("payments.next")}</button - > - </form> - {/if} -</div> diff --git a/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte b/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte index 314be3ef..d34ed073 100644 --- a/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte +++ b/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte @@ -6,14 +6,16 @@ import { page } from "$app/stores"; import { pin } from "$lib/store"; import handler from "$lib/handler"; - import { s } from "$lib/utils"; + import { loc, s } from "$lib/utils"; + import { applyAction, deserialize } from "$app/forms"; let { data, form } = $props(); - let { amount } = $state(data); - let { balance, currency } = data.user; + let { amount, user } = $state(data); + let { balance, currency } = $derived(user); let { name, rate } = $derived(data); let loading = $state(); let fiat = $state(); + let locale = loc(user); let submit = $state(); let submitting = $state(); @@ -22,11 +24,22 @@ if (form?.message?.includes("pin")) $pin = undefined; loading = false; }); + let formElement = $state(); - let setMax = (e) => { + let setMax = async (e) => { e.preventDefault(); - fiat = false; - amount = balance; + let body = new FormData(formElement); + body.set("fiat", false); + body.set("amount", user.balance); + + const response = await fetch(formElement.action, { + method: "POST", + body, + }); + + const result = deserialize(await response.text()); + if (result.type === "success") await invalidateAll(); + applyAction(result); }; </script> @@ -37,9 +50,14 @@ {/if} <div class="container px-4 mt-20 max-w-xl mx-auto"> - <Numpad bind:amount {currency} {rate} {fiat} {submit} /> + <Numpad bind:amount {currency} {rate} {fiat} {submit} {locale} /> - <form use:enhance={handler(toggle)} method="POST" class="space-y-5"> + <form + use:enhance={handler(toggle)} + method="POST" + class="space-y-5" + bind:this={formElement} + > <input name="fund" value={name} type="hidden" /> <input name="amount" value={amount} type="hidden" /> <input name="pin" value={$pin} type="hidden" /> diff --git a/src/routes/(app)/send/invoice/[id]/+page.server.ts b/src/routes/(app)/send/invoice/[id]/+page.server.ts index 3ef933d4..70b007a8 100644 --- a/src/routes/(app)/send/invoice/[id]/+page.server.ts +++ b/src/routes/(app)/send/invoice/[id]/+page.server.ts @@ -1,12 +1,36 @@ import getRates from "$lib/rates"; import { auth, fd, get, post, types } from "$lib/utils"; -import { error, redirect } from "@sveltejs/kit"; +import { error, fail, redirect } from "@sveltejs/kit"; -export async function load({ cookies, params: { id }, parent }) { - const { user } = await parent(); +export async function load({ cookies, depends, params: { id }, parent }) { + depends("app:trust"); + const { subject, user } = await parent(); const aid = cookies.get("aid") || user.id; + const pin = cookies.get("pin"); - const invoice = await get(`/invoice/${id}`); + let invoice = await get(`/invoice/${id}`); + + const trust = await get("/trust", auth(cookies)); + const trusted = trust.includes(invoice.uid); + if (trusted && (pin || !user.haspin)) { + let p; + try { + if (!invoice.tip && invoice.user.prompt) { + if (user.tip > 0) { + invoice.tip = Math.round(invoice.amount * (user.tip / 100)); + invoice = await post(`/invoice/${id}`, { invoice }, auth(cookies)); + } else { + throw new Error("tip"); + } + } + p = await post("/payments", { ...invoice, pin }, auth(cookies)); + } catch (e) { + const { message } = e as Error; + if (message === "tip") redirect(307, `/invoice/${id}/tip`); + fail(400, { message }); + } + if (p) redirect(307, `/sent/${p.id}`); + } if (invoice.amount && invoice.prompt && invoice.tip === null) redirect(307, `/invoice/${id}/tip`); @@ -31,7 +55,7 @@ export async function load({ cookies, params: { id }, parent }) { const invoiceRate = rates[invoice.currency]; const { balance } = await get(`/account/${aid}`, auth(cookies)); - return { balance, invoice, user, rate, invoiceRate }; + return { balance, invoice, user, rate, invoiceRate, trusted }; } export const actions = { @@ -45,7 +69,7 @@ export const actions = { } catch (e) { const { message } = e as Error; console.log("payment failed", id, e); - error(500, message); + error(500, { message }); } redirect(307, `/sent/${p.id}`); diff --git a/src/routes/(app)/send/invoice/[id]/+page.svelte b/src/routes/(app)/send/invoice/[id]/+page.svelte index 7d167f41..d5fd00a8 100644 --- a/src/routes/(app)/send/invoice/[id]/+page.svelte +++ b/src/routes/(app)/send/invoice/[id]/+page.svelte @@ -1,4 +1,5 @@ <script> + import { invalidate } from "$app/navigation"; import { untrack } from "svelte"; import handler from "$lib/handler"; import { onDestroy, onMount } from "svelte"; @@ -26,7 +27,7 @@ } from "$lib/utils"; let { data, form } = $props(); - let { balance, invoice, user } = $state(data); + let { trusted, balance, invoice, user } = $state(data); let { address, hash, payreq, user: recipient, tip } = $state(invoice); let { currency } = $derived(user); let locale = $derived(loc(user)); @@ -64,6 +65,19 @@ }); }; + let toggleTrust = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (trusted) { + await post("/post/trust/delete", { id: recipient.id }); + } else { + await post("/post/trust", { id: recipient.id }); + } + + invalidate("app:trust"); + }; + $effect(() => { if (form?.message?.includes("pin")) $pin = undefined; submitting = false; @@ -193,5 +207,12 @@ <button type="button" class="btn" onclick={external} >{$t("payments.moreOptions")}</button > + + {#if !trusted} + <button type="button" class="btn" onclick={toggleTrust}> + <iconify-icon icon="ph:star-fill" width={32}></iconify-icon> + {$t("payments.trustUser")}</button + > + {/if} </form> </div> diff --git a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts index 0f7b4003..eb324775 100644 --- a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts +++ b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts @@ -39,8 +39,7 @@ export const actions = { p = await post("/payments", body, auth(cookies)); } catch (e) { - let { message } = e as Error; - if (!message.includes("Insufficient")) message = "payments.failedToRoute"; + const { message } = e as Error; return fail(400, { message }); } diff --git a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte index e979e7c4..a5cc168c 100644 --- a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte +++ b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte @@ -13,7 +13,7 @@ let { alias, memo, payreq, ourfee, user } = $derived({ ...data, ...form }); let a = $state(); - let { currency } = $derived(user); + let { currency, locked } = $derived(user); let locale = loc(user); $effect(() => ($rate ||= data.rate)); @@ -36,7 +36,12 @@ <div class="container px-4 max-w-xl mx-auto text-center space-y-2"> {#if form?.message} <div class="text-red-600 text-center"> - {$t(form.message)} + {#if form.message.includes($t("payments.insufficientFunds"))} + <div>{form.message}</div> + <div>{$t("payments.lockedBalance")}: {locked}</div> + {:else} + {$t("payments.failedToRoute")} + {/if} </div> {/if} {#if amount} |
