diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/Numpad.svelte | 5 | ||||
| -rw-r--r-- | src/components/Toggle.svelte | 2 | ||||
| -rw-r--r-- | src/lib/utils.js | 5 | ||||
| -rw-r--r-- | src/locales/en.json | 2 | ||||
| -rw-r--r-- | src/routes/(app)/+layout.svelte | 6 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/payments/[...page]/+page.svelte | 2 | ||||
| -rw-r--r-- | src/routes/(app)/payment/[id]/+page.svelte | 27 | ||||
| -rw-r--r-- | src/routes/(app)/send/bitcoin/[address]/+page.svelte | 56 | ||||
| -rw-r--r-- | src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js | 40 | ||||
| -rw-r--r-- | src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte | 162 | ||||
| -rw-r--r-- | src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.js | 36 | ||||
| -rw-r--r-- | src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte | 134 | ||||
| -rw-r--r-- | src/routes/+page.svelte | 2 |
13 files changed, 261 insertions, 218 deletions
diff --git a/src/components/Numpad.svelte b/src/components/Numpad.svelte index 7006d7aa..72dad375 100644 --- a/src/components/Numpad.svelte +++ b/src/components/Numpad.svelte @@ -118,6 +118,8 @@ .replace(/\./g, "") .replace("F", "."); + if (!clean) clean = " "; + if (fiat) clean = clean.replace(",", ""); if (clean !== html) { html = clean; @@ -171,8 +173,9 @@ } }; - let keydown = (e) => + let keydown = (e) => { e.key === "Enter" && (e.preventDefault() || submit.click()); + }; </script> <div class="flex justify-center items-center mb-3 px-3"> diff --git a/src/components/Toggle.svelte b/src/components/Toggle.svelte index 1e4cf4c4..2f92837d 100644 --- a/src/components/Toggle.svelte +++ b/src/components/Toggle.svelte @@ -12,6 +12,6 @@ class="sr-only peer" /> <div - class="w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-focus:ring-4 peer-focus:ring-gray-300 dark:peer-focus:ring-gray-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-gray-600" + class="w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-focus:ring-4 peer-focus:ring-gray-300 dark:peer-focus:ring-gray-800 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-green-500" /> </label> diff --git a/src/lib/utils.js b/src/lib/utils.js index f55b47b6..5303ec35 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -281,3 +281,8 @@ export function shuffleArray(array) { [array[i], array[j]] = [array[j], array[i]]; } } + + export let closest = (a, n) => + a.reduce((prev, curr) => + Math.abs(curr - n) < Math.abs(prev - n) ? curr : prev + ); diff --git a/src/locales/en.json b/src/locales/en.json index ac8b0075..cb42fa78 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -272,6 +272,8 @@ } }, "payments": { + "networkFee": "Network fee", + "ourFee": "Our fee", "create": "Create", "fund": "Fund", "addFunds": "Add Funds", diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index e439d43e..0480fd79 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -18,7 +18,7 @@ import Password from "$comp/Password.svelte"; import Request from "$comp/Request.svelte"; import { warning, protectedRoutes } from "$lib/utils"; - import { t, locale } from "$lib/translations"; + import { t, locale, loading } from "$lib/translations"; import { goto, onNavigate } from "$app/navigation"; export let data; @@ -129,7 +129,9 @@ <SvelteToast options={{ reversed: true, intro: { y: 192 } }} /> <main class="pb-20"> - <slot /> + {#if !$loading} + <slot /> + {/if} </main> {#if $invoice} diff --git a/src/routes/(app)/[username]/payments/[...page]/+page.svelte b/src/routes/(app)/[username]/payments/[...page]/+page.svelte index 317f776b..1f20ea5c 100644 --- a/src/routes/(app)/[username]/payments/[...page]/+page.svelte +++ b/src/routes/(app)/[username]/payments/[...page]/+page.svelte @@ -238,7 +238,7 @@ {format(new Date(p.created), "h:mm aaa")} </div> <div> - {format(new Date(p.created), "MMM d")} + {format(new Date(p.created), "MMM d, yyyy")} </div> </div> </div> diff --git a/src/routes/(app)/payment/[id]/+page.svelte b/src/routes/(app)/payment/[id]/+page.svelte index a1c7bbed..15c17a00 100644 --- a/src/routes/(app)/payment/[id]/+page.svelte +++ b/src/routes/(app)/payment/[id]/+page.svelte @@ -1,5 +1,7 @@ <script> - import { t } from "$lib/translations"; + import { browser } from "$app/environment"; + import { onMount, tick } from "svelte"; + import { t, loading } from "$lib/translations"; import { back, copy, @@ -30,9 +32,14 @@ fee = fee || 0; - let direction = amount > 0 ? $t("payments.from") : $t("payments.to"); - direction = - direction[0].toUpperCase() + direction.substr(1, direction.length); + let direction = ""; + onMount(() => { + direction = amount > 0 ? $t("payments.from") : $t("payments.to"); + + if (direction) + direction = + direction[0].toUpperCase() + direction.substr(1, direction.length); + }); </script> <a href={`/${username}/payments`}> @@ -124,8 +131,8 @@ <div> <span class="text-lg text-secondary">Date</span> <div> - {format(new Date(created), "MMMM d")}, - {format(new Date(created), "h:mm aaa")} + {format(new Date(created), "h:mmaaa")} + {format(new Date(created), "MMM d, yyyy")} </div> </div> @@ -140,8 +147,6 @@ class="flex font-bold hover:opacity-80 mb-auto my-auto" on:click={() => copy(id)} ><Icon icon="copy" style="ml-2 w-20 my-auto" /> - - </button> </div> </div> @@ -151,7 +156,8 @@ <div> <button class="rounded-full border py-3 px-6 hover:opacity-80 flex w-full md:w-60" - on:click={print}> + on:click={print} + > <div class="mx-auto flex"> <Icon icon="printer" style="my-auto h-6 mr-2" /> <div class="my-auto mt-1 text-base">{$t("payments.print")}</div> @@ -162,7 +168,8 @@ <div> <a href={`/payment/${id}/plain`}> <button - class="rounded-full border py-3 px-6 hover:opacity-80 flex w-full md:w-60"> + class="rounded-full border py-3 px-6 hover:opacity-80 flex w-full md:w-60" + > <div class="mx-auto flex"> <Icon icon="printer" style="my-auto h-6 mr-2" /> <div class="my-auto mt-1 text-base">{$t("payments.print")}</div> diff --git a/src/routes/(app)/send/bitcoin/[address]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/+page.svelte new file mode 100644 index 00000000..7648d41f --- /dev/null +++ b/src/routes/(app)/send/bitcoin/[address]/+page.svelte @@ -0,0 +1,56 @@ +<script> + 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 { back, s } from "$lib/utils"; + + export let data; + + let { rates } = data; + let { address } = $page.params; + let { balance, currency } = data.user; + + let amount = 0; + let submit, fiat; + + $: update(data); + let update = () => ($rate = data.rates[currency]); + + let setMax = () => { + fiat = false; + amount = balance; + }; +</script> + +<button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" on:click={back}> + <Icon icon="arrow-left" style="w-10" /> +</button> + +<div class="container px-4 max-w-xl mx-auto space-y-5 text-center"> + <h1 class="text-3xl md:text-4xl font-semibold mb-2">{$t("payments.send")}</h1> + + <div class="text-lg text-secondary break-all">{address}</div> + + <Numpad bind:amount bind:fiat {currency} {submit} bind:rate={$rate} /> + + <div class="flex justify-center gap-2"> + <button + type="button" + class="hover:opacity-80 bg-black text-white rounded-2xl py-3 px-4 mt-2 border font-bold" + on:click|preventDefault={setMax} + on:keydown={setMax}>Max ⚡️{s(balance)}</button + > + + <form action={`./${address}/${amount}`}> + <button + bind:this={submit} + type="submit" + class="opacity-100 hover:opacity-80 rounded-2xl border py-3 font-bold mt-2 bg-black text-white px-4 w-24" + > + {$t("payments.next")} + </button> + </form> + </div> +</div> diff --git a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js deleted file mode 100644 index 2998f22a..00000000 --- a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js +++ /dev/null @@ -1,40 +0,0 @@ -import { fail, redirect } from "@sveltejs/kit"; -import { auth, post, fd } from "$lib/utils"; - -let seen = {}; -export const actions = { - default: async ({ cookies, request }) => { - try { - let { - address, - amount, - confirmed, - feeRate, - subtract, - pin, - stale, - ts, - } = await await fd(request); - if (seen[ts]) return fail(400, { message: "Duplicate form submission" }); - - let estimate = await post( - "/bitcoin/fee", - { address, amount, feeRate, subtract }, - auth(cookies) - ); - - let { tx } = estimate; - - if (stale || !confirmed) return fail(400, estimate); - - seen[ts] = 1; - setTimeout(() => delete seen[ts], 30000); - - await post("/bitcoin/send", { pin, subtract, tx }, auth(cookies)); - } catch (e) { - return fail(400, { message: e.message }); - } - - redirect(307, "/sent"); - }, -}; diff --git a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte deleted file mode 100644 index a8b86718..00000000 --- a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte +++ /dev/null @@ -1,162 +0,0 @@ -<script> - import { tick } from "svelte"; - import { t } from "$lib/translations"; - import { enhance } from "$app/forms"; - import Toggle from "$comp/Toggle.svelte"; - import Slider from "$comp/Slider.svelte"; - import Icon from "$comp/Icon.svelte"; - import Numpad from "$comp/Numpad.svelte"; - import Spinner from "$comp/Spinner.svelte"; - import { page } from "$app/stores"; - import { fiat as toFiat, f, back, s, sat } from "$lib/utils"; - import { pin } from "$lib/store"; - - export let data; - export let form; - - let { rates } = data; - let { address, amount } = $page.params; - let { balance, currency } = data.user; - let loading, - submit, - fiat, - confirmed, - feeRate, - min, - max, - fee, - ourfee, - stale, - subtract, - adjusting; - - $: rate = rates[currency]; - - let toggle = () => (loading = true); - let toggleAdjusting = () => (adjusting = !adjusting); - - $: update(form); - let update = () => { - loading = false; - if (form?.message?.includes("pin")) $pin = undefined; - if (form) ({ min, max, feeRate, fee, ourfee, subtract } = form); - if (!ourfee) ourfee = 0; - if (feeRate) confirmed = true; - stale = false; - }; - - let setMax = () => { - fiat = false; - amount = balance; - subtract = true; - }; - - $: if (amount + fee > balance) subtract = true; - - let timeout; - let handle = (e) => { - stale = true; - feeRate = e.target.value; - clearTimeout(timeout); - timeout = setTimeout(() => { - tick().then(() => submit.click()); - }, 200); - }; -</script> - -<button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" on:click={back}> - <Icon icon="arrow-left" style="w-10" /> -</button> - -{#if form?.message} - <div class="text-red-600 text-center"> - {form.message} - </div> -{/if} - -<div class="container px-4 mt-20 max-w-xl mx-auto space-y-5 text-center"> - <h1 class="text-3xl md:text-4xl font-semibold mb-2">{$t("payments.send")}</h1> - - <div class="text-lg text-secondary break-all">{address}</div> - - {#if confirmed} - <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> - - <div class="text-center"> - <h2 class="text-secondary mb-5 text-lg">with fee</h2> - - <div class="relative"> - <h2 class="text-xl font-semibold"> - {f(toFiat(fee + ourfee, rate), currency)} - </h2> - <h3 class="text-secondary mb-6 mt-1">⚡️{s(fee + ourfee)}</h3> - <button - class="absolute right-1/4 top-0 text-secondary p-4" - on:click={toggleAdjusting} - ><Icon icon="settings" style="opacity-30" /></button - > - </div> - </div> - {:else} - <Numpad bind:amount bind:fiat {currency} {submit} bind:rate /> - {/if} - - <form method="POST" use:enhance on:submit={toggle}> - <input name="ts" value={Date.now()} type="hidden" /> - <input name="address" value={address} type="hidden" /> - <input name="amount" value={amount} type="hidden" /> - <input name="pin" value={$pin} type="hidden" /> - <input name="confirmed" value={confirmed} type="hidden" /> - <input name="feeRate" value={feeRate} type="hidden" /> - <input name="stale" value={stale} type="hidden" /> - <input name="subtract" value={subtract} type="hidden" /> - - {#if confirmed && adjusting} - <div class="relative mt-10 mb-20"> - <div class="absolute top-5 left-0 text-secondary"> - Slower (~40 blocks) - </div> - <div class="absolute top-5 right-0 text-secondary"> - Faster (~1 block) - </div> - <Slider bind:value={feeRate} {handle} {min} {max} /> - </div> - <div class="flex justify-center gap-2 mb-10"> - <div class="my-auto"> - <Toggle id="subtract" checked={subtract} bind:value={subtract} /> - </div> - <label for="subtract" class="text-secondary text-lg my-auto mb-2" - >Take fee from amount</label - > - </div> - {/if} - - <div class="flex justify-center gap-2"> - {#if !confirmed} - <button - type="button" - class="hover:opacity-80 bg-black text-white rounded-2xl py-3 px-4 mt-2 border font-bold" - on:click|preventDefault={setMax} - on:keydown={setMax}>Max ⚡️{s(balance)}</button - > - {/if} - - <button - bind:this={submit} - type="submit" - class="opacity-100 hover:opacity-80 rounded-2xl border py-3 font-bold mt-2 bg-black text-white px-4 w-24" - disabled={loading}> - {#if confirmed} - {$t("payments.send")} - {:else} - {$t("payments.next")} - {/if} - </button> - </div> - </form> -</div> diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.js b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.js new file mode 100644 index 00000000..08741296 --- /dev/null +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.js @@ -0,0 +1,36 @@ +import { fail, redirect } from "@sveltejs/kit"; +import { auth, post, fd } from "$lib/utils"; + +let seen = {}; + +export async function load({ params: { address, amount, feeRate }, cookies }) { + try { + let fees = await fetch( + `https://mempool.space/api/v1/fees/recommended` + ).then((r) => r.json()); + + if (!feeRate) feeRate = fees.halfHourFee; + + let { fee, ourfee, hex } = await post( + "/bitcoin/fee", + { address, amount, feeRate: feeRate }, + auth(cookies) + ); + + return { amount, address, fee, fees, feeRate, ourfee, hex }; + } catch (e) { + throw redirect(307, `/send/bitcoin/${address}`); + } +} + +export const actions = { + default: async ({ cookies, request }) => { + try { + let r = await post("/bitcoin/send", await fd(request), auth(cookies)); + } catch (e) { + return fail(400, { message: e.message }); + } + + redirect(307, "/sent"); + }, +}; diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte new file mode 100644 index 00000000..d583024c --- /dev/null +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -0,0 +1,134 @@ +<script> + 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 { page } from "$app/stores"; + import { fiat as toFiat, f, back, s, sat, closest } from "$lib/utils"; + import { pin } from "$lib/store"; + import { goto, invalidate } from "$app/navigation"; + import { rate } from "$lib/store"; + + export let data; + export let form; + + let { amount, address, fee, fees, feeRate, ourfee, rates, hex } = data; + + $: reload(data); + let reload = () => { + ({ amount, address, fee, fees, feeRate, ourfee, rates, hex } = data); + feeRate = closest(Object.values(fees), feeRate); + if (!$rate) $rate = rates[currency]; + }; + + let { balance, currency } = data.user; + let submitting, submit, showSettings; + + let feeNames = { + fastestFee: "Fastest", + halfHourFee: "Fast", + hourFee: "Medium", + economyFee: "Slow", + minimumFee: "Slowest", + }; + + let toggleSettings = () => (showSettings = !showSettings); + + $: update(form); + let update = () => { + submitting = false; + if (form?.message?.includes("pin")) $pin = undefined; + }; + + let setFee = () => goto(`/send/bitcoin/${address}/${amount}/${feeRate}`); +</script> + +<button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" on:click={back}> + <Icon icon="arrow-left" style="w-10" /> +</button> + +<div + class="container px-4 max-w-xl mx-auto space-y-5 text-center no-transition" +> + {#if form?.message} + <div class="mb-5"> + <div class="text-red-600">{form.message}</div> + </div> + {/if} + + <h1 class="text-3xl md:text-4xl font-semibold mb-2">{$t("payments.send")}</h1> + + <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> + + <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} + on:change={setFee} + class="border text-lg bg-white p-4 rounded-2xl text-center my-auto" + > + {#each Object.keys(fees) as feeName} + <option value={fees[feeName]} + >{feeNames[feeName]} — ⚡️{fees[feeName]}/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> + </div> + </div> + + <div class="text-center"> + <h2 class="text-secondary text-lg">{$t("payments.ourFee")}</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> + </div> + + <form method="POST" use:enhance on:submit={() => (submitting = true)}> + <input name="pin" value={$pin} type="hidden" /> + <input name="hex" value={hex} type="hidden" /> + <input name="rate" value={$rate} type="hidden" /> + + <div class="flex justify-center gap-2"> + <button + bind:this={submit} + type="submit" + class="opacity-100 hover:opacity-80 rounded-2xl border py-3 font-bold mt-2 bg-black text-white px-4 w-24" + disabled={submitting} + > + {#if submitting} + <Spinner /> + {:else} + {$t("payments.send")} + {/if} + </button> + </div> + </form> +</div> + +<style> + .no-transition { + view-transition-name: fee; + } +</style> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3cffea21..23aaacfa 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,4 +1,4 @@ -<script> + <script> import LandingHeader from "$comp/LandingHeader.svelte"; import LandingHero from "$comp/LandingHero.svelte"; import LandingInfoCard from "$comp/LandingInfoCard.svelte"; |
