diff options
| author | Adam Soltys <[email protected]> | 2024-09-24 20:12:43 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-09-24 20:12:43 -0700 |
| commit | d49d4119811dee9270f0858f3f03e5d8cd0d95ba (patch) | |
| tree | 42101a3ccf47f7b7290637700b90a843476e56f7 /src/routes | |
| parent | 20c0c52f8b850bfbab490735b0b5cd5d08851488 (diff) | |
| parent | 9531ccbdf01b07c392b1b5cb6d8ad4680ee29d2c (diff) | |
| download | coinos-ui-d49d4119811dee9270f0858f3f03e5d8cd0d95ba.tar.gz coinos-ui-d49d4119811dee9270f0858f3f03e5d8cd0d95ba.zip | |
Merge branch 'self-custody'
Diffstat (limited to 'src/routes')
102 files changed, 1893 insertions, 1558 deletions
diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts index 655a9425..0901cb5d 100644 --- a/src/routes/(app)/+layout.server.ts +++ b/src/routes/(app)/+layout.server.ts @@ -1,8 +1,9 @@ -import { redirect } from "@sveltejs/kit"; - import { get, auth, protectedRoutes } from "$lib/utils"; +import { error, redirect } from "@sveltejs/kit"; +import { get, auth, protectedRoutes } from "$lib/utils"; export async function load({ cookies, request, url, params }) { let { pathname } = url; + let { username } = params; let token = cookies.get("token"); let rate, rates = { USD: 1 }; @@ -14,6 +15,18 @@ export async function load({ cookies, request, url, params }) { } catch (e) {} } + let subject; + if (username) { + try { + subject = await get(`/users/${username}`); + } catch (e) { + error(500, "Unable to retrieve user account data"); + } + } + + if (!subject) subject = user; + if (!user) user = subject; + if (user?.needsMigration) { redirect(307, `/migrate`); } @@ -26,14 +39,12 @@ export async function load({ cookies, request, url, params }) { redirect(307, `/${user.username}`); } - if ( - protectedRoutes.find((p) => pathname.match(p)) && - (!user || user.username !== params.username) - ) { + if (protectedRoutes.find((p) => pathname.match(p)) && !user) { redirect(307, "/login"); } if (user && !user.pubkey && !pathname.includes("generate")) { + console.log("user missing pubkey", user); redirect(303, `/${user.username}/generate`); } @@ -44,5 +55,5 @@ export async function load({ cookies, request, url, params }) { console.log(e); } - return { user, token, rate, rates }; + return { user, token, rate, rates, subject }; } diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 8d4e2940..db14b046 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -14,26 +14,15 @@ 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 Request from "$comp/Request.svelte"; import { getCookie, warning, protectedRoutes } from "$lib/utils"; import { t, locale, loading } from "$lib/translations"; import { goto, onNavigate } from "$app/navigation"; export let data; - onNavigate((navigation) => { - if (!document.startViewTransition) return; - - return new Promise((resolve) => { - document.startViewTransition(async () => { - resolve(); - await navigation.complete; - }); - }); - }); - let { generate, rate, user, subject, token, rates } = data; $: update(data); @@ -115,6 +104,7 @@ <SvelteToast options={{ reversed: true, intro: { y: 192 } }} /> <main class="pb-20"> + <AppHeader {user} {subject} /> {#if !$loading} <slot /> {/if} @@ -123,43 +113,3 @@ {#if $invoice} <Invoice {user} /> {/if} - -{#if $request} - <Request {user} /> -{/if} - -<style> - @keyframes fade-in { - from { - opacity: 0; - } - } - - @keyframes fade-out { - to { - opacity: 0; - } - } - - @keyframes slide-from-right { - from { - transform: translateX(60px); - } - } - - @keyframes slide-to-left { - to { - transform: translateX(-60px); - } - } - - :root::view-transition-old(root) { - animation: 280ms cubic-bezier(0.4, 0, 1, 1) both fade-out, - 180ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; - } - - :root::view-transition-new(root) { - animation: 280ms cubic-bezier(0, 0, 0.2, 1) 80ms both fade-in, - 180ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; - } -</style> diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index a213e792..30a6e325 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -82,13 +82,12 @@ <div class="container mx-auto w-full px-4 flex flex-wrap lg:flex-nowrap"> <div class="hidden lg:block lg:w-[280px] xl:w-[360px]" /> <div - class="w-full lg:w-[280px] xl:w-[360px] lg:absolute space-y-5 left-20 mx-auto mb-5" + class="w-full lg:w-[280px] xl:w-[360px] lg:absolute space-y-2 left-20 mx-auto" > - <div - class="flex text-3xl font-bold text-center mx-auto justify-center gap-2" - > - <div class="my-auto break-words w-full">{display || username}</div> - </div> + <button type="button" on:click={toggleDetails} class="flex text-3xl font-bold text-center mx-auto justify-center gap-2"> + <div class="my-auto break-words">{display || username}</div> + <Icon icon="qr" style="w-8 invert my-auto" /> + </button> {#if subject.address} <div @@ -112,68 +111,51 @@ <span class="text-secondary">{$t("user.followers")}</span></a > </div> - - <button - type="button" - on:click={toggleDetails} - class="flex text-base text-secondary mx-auto" - > - <div class="mt-1"> - {$t("user.moreDetails")} - </div> - <div - class:-rotate-90={showDetails} - class:rotate-180={!showDetails} - class="my-auto" - > - <Icon icon="chevron-left" style="w-6 mr-2" /> - </div> - </button> </div> {#if showDetails} - <div class="text-xl space-y-5"> + <div class="space-y-5 pb-20"> <div> - <div class="font-bold">{$t("user.profilePage")}</div> + <div class="text-secondary">{$t("user.lightningAddress")}</div> <div class="flex gap-4"> - <div class="break-all grow text-secondary"> - {profile} + <div class="break-all grow text-xl"> + {lnaddr} </div> <div class="flex mb-auto gap-1"> - <button class="my-auto" on:click={() => copy(profile)} + <button class="my-auto" on:click={() => copy(lnaddr)} ><Icon icon="copy" style="max-w-max w-8 min-w-[32px]" /></button > - <a - href={`/qr/${encodeURIComponent( - `${$page.url.protocol}//${profile}` - )}`} - class="my-auto" - > + <a href={`/qr/${encodeURIComponent(lnaddr)}`} class="my-auto"> <Icon icon="qr" style="invert max-w-max min-w-[32px]" /> </a> </div> </div> </div> <div> - <div class="font-bold">{$t("user.lightningAddress")}</div> + <div class="text-secondary">{$t("user.url")}</div> <div class="flex gap-4"> - <div class="break-all grow text-secondary"> - {lnaddr} + <div class="break-all grow text-xl"> + {profile} </div> <div class="flex mb-auto gap-1"> - <button class="my-auto" on:click={() => copy(lnaddr)} + <button class="my-auto" on:click={() => copy(profile)} ><Icon icon="copy" style="max-w-max w-8 min-w-[32px]" /></button > - <a href={`/qr/${encodeURIComponent(lnaddr)}`} class="my-auto"> + <a + href={`/qr/${encodeURIComponent( + `${$page.url.protocol}//${profile}` + )}`} + class="my-auto" + > <Icon icon="qr" style="invert max-w-max min-w-[32px]" /> </a> </div> </div> </div> <div> - <div class="font-bold">{$t("user.nostrPubkey")}</div> + <div class="text-secondary">{$t("user.nostrPubkey")}</div> <div class="flex gap-4"> - <div class="break-all grow text-secondary"> + <div class="break-all grow text-xl"> {npub} </div> <div class="flex my-auto gap-1"> @@ -188,9 +170,9 @@ </div> {#if user.id === subject.id} <div> - <div class="font-bold">{$t("user.nwc")}</div> + <div class="text-secondary">{$t("user.nwc")}</div> <div class="flex gap-4"> - <div class="break-all grow text-secondary"> + <div class="break-all grow text-xl"> {nwc} </div> <div class="flex my-auto gap-1"> @@ -237,7 +219,7 @@ {#if user && user.username !== subject.username && subject.pubkey} <a - href={`/${user.username}/messages/${subject.username}`} + href={`/messages/${subject.username}`} class="contents" > <button diff --git a/src/routes/(app)/[username]/(profile)/+page.server.ts b/src/routes/(app)/[username]/(profile)/+page.server.ts index 59eb44d5..ed4ee58c 100644 --- a/src/routes/(app)/[username]/(profile)/+page.server.ts +++ b/src/routes/(app)/[username]/(profile)/+page.server.ts @@ -1,11 +1,15 @@ import { auth, get } from "$lib/utils"; export async function load({ cookies, depends, parent }) { + depends("app:payments"); depends("app:items"); let { subject } = await parent(); + + let accounts = await get("/accounts", auth(cookies)); + let items = await get(`/${subject.id}/items`, auth(cookies)); items.map((i) => (i.quantity = 0)); - return { items }; + return { accounts, items }; } diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte index 32e6807a..399a33e9 100644 --- a/src/routes/(app)/[username]/(profile)/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/+page.svelte @@ -1,161 +1,54 @@ <script> - import { browser } from "$app/environment"; - import { goto, invalidate } from "$app/navigation"; - import { btc, f, sat, post } from "$lib/utils"; + import { btc, f, sat } from "$lib/utils"; import Icon from "$comp/Icon.svelte"; + import Account from "$comp/Account.svelte"; import Balance from "$comp/Balance.svelte"; - import DeleteItem from "$comp/DeleteItem.svelte"; + import Items from "$comp/Items.svelte"; import { t } from "$lib/translations"; - import { fade } from "svelte/transition"; - import { cubicIn } from "svelte/easing"; - import { flip } from "svelte/animate"; import { installPrompt } from "$lib/store"; - import { - dndzone, - SHADOW_ITEM_MARKER_PROPERTY_NAME, - SOURCES, - TRIGGERS, - } from "svelte-dnd-action"; - export let data; - let flipDurationMs = 200; - let dropTargetStyle = {}; - - let { subject, user, items, rates } = data; + let { accounts, subject, user, items, rates } = data; $: ({ currency } = subject); $: rate = rates[currency]; - $: refresh(data); - let refresh = (d) => ({ items, subject, user, rates } = d); - let dragDisabled = - subject?.id !== user?.id || (browser && window.innerWidth < 768); - - let deleting; $: total = items.reduce((a, b) => a + b.price * b.quantity, 0); - let del = (item) => { - deleting = item; - }; - - function handleConsider(e) { - const { - items: newItems, - info: { source, trigger }, - } = e.detail; - items = newItems; - if (source === SOURCES.KEYBOARD && trigger === TRIGGERS.DRAG_STOPPED) - reset(); - } - - async function handleFinalize(e) { - const { - items: newItems, - info: { source }, - } = e.detail; - items = newItems; - if (source === SOURCES.POINTER) reset(); - - if (subject?.id === user?.id) { - await post("/api/items/sort", { items }); - invalidate("app:items"); - } - } - - function startDrag(e) { - e.preventDefault(); - dragDisabled = false; - } - - function handleKeyDown(e) { - if ((e.key === "Enter" || e.key === " ") && dragDisabled) - dragDisabled = false; - } - - $: if (total > 0 && browser) { - let payButton = document.querySelector("#payButton"); - if (payButton) payButton.addEventListener("click", checkout); - } - - let checkout = async () => { - let invoice = { - type: "lightning", - amount: btc(total, rate), - currency, - items: items.filter((i) => i.quantity > 0), - prompt: subject.prompt, - memoPrompt: subject.memoPrompt, - rate, - }; - - let { id } = await post(`/${subject.username}/invoice`, { - invoice, - user: { username: subject.username }, - }); - - if (invoice.memoPrompt) { - return goto(`/${subject.username}/invoice/${id}/memo`); - } - goto(`/${subject.username}/invoice/${id}`); - }; - let reset = () => - (dragDisabled = - user?.username !== subject?.username || window.innerWidth < 768); + let refresh = (d) => ({ accounts, items, subject, user, rates } = d); + $: refresh(data); let install = async () => { if (!$installPrompt) return; await $installPrompt.prompt(); $installPrompt = null; }; -</script> - -<svelte:window on:resize={reset} /> -{#if deleting} - <DeleteItem bind:item={deleting} /> -{/if} + if (user) user.savings = 0; +</script> -<div class="space-y-5"> +<div class="space-y-2"> {#if user?.id === subject.id} - <div class="flex justify-center lg:justify-start"> - <Balance {user} {rate} /> - </div> - - {#if !user.balance} - <div class="mb-8"> - <p class="text-secondary text-lg"> - {$t("user.welcome")} - </p> - </div> - {/if} - - <div class="flex flex-wrap gap-3 justify-center w-full text-xl"> - <a href={`/send`} class="contents grow"> - <button - type="button" - class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow" - > - <div class="mx-auto flex gap-2"> - <Icon icon="send" style="w-8" /> - <div class="my-auto">{$t("user.dashboard.send")}</div> - </div> - </button> - </a> + {#each accounts as account} + <Account {user} {rate} {account} /> + {/each} - <a href={`/${user.username}/receive`} class="contents"> + <div class="flex justify-center gap-2"> + <a href={`/account/savings`} class="contents"> <button - class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow" + class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex w-full justify-center" > <div class="mx-auto flex gap-2"> - <Icon icon="receive" style="w-8" /> - <div class="my-auto">{$t("user.dashboard.receive")}</div> + <Icon icon="plus" style="w-8 mx-auto" /> + <div class="my-auto text-xl whitespace-nowrap"> + {$t("accounts.addAccount")} + </div> </div> </button> </a> {#if $installPrompt} <button - class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white grow basis-full fixed bottom-16 xl:hidden" + class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white xl:hidden w-full justify-center" on:click={install} > <div class="mx-auto flex gap-2"> @@ -169,115 +62,8 @@ </div> {/if} - {#if items.length} - <h2 class="text-2xl font-bold">Select items</h2> - {/if} - <div - class="grid sm:grid-cols-2 gap-4 pb-20" - use:dndzone={{ items, flipDurationMs, dropTargetStyle, dragDisabled }} - on:consider={handleConsider} - on:finalize={handleFinalize} - > - {#each items as i (i.id)} - <button - type="button" - class="w-full text-center cursor-pointer hover:opacity-80 mb-auto" - on:click={() => i.quantity++} - > - <div class="relative w-full h-64 overflow-hidden rounded-xl"> - {#if i[SHADOW_ITEM_MARKER_PROPERTY_NAME]} - <div - in:fade={{ duration: 200, easing: cubicIn }} - class="custom-shadow-item" - /> - {:else if i.image} - <img - src={`/api/public/${i.image}.webp`} - alt={i.name} - class="mx-auto object-cover h-full w-full" - /> - {:else} - <div - class="bg-gradient-to-r from-primary to-gradient mb-4 cursor-pointer hover:opacity-80 w-full h-full" - alt="Banner" - /> - {/if} - {#if user?.username === subject.username} - <div class="flex gap-2 justify-center absolute top-2 right-2"> - <button type="button" on:click|stopPropagation={() => {}}> - <a href={`/${user.username}/items/${i.id}`}> - <button - class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100" - > - <Icon icon="edit" style="w-8 mx-auto invert" /> - </button> - </a> - </button> - <button - class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100" - on:click|stopPropagation={() => del(i)} - > - <Icon icon="trash" style="w-8 mx-auto invert" /> - </button> - <button - type="button" - on:click|stopPropagation={() => {}} - class="sm:hidden" - on:mousedown={startDrag} - on:touchstart={startDrag} - on:keydown={handleKeyDown} - > - <a href={`/${user.username}/items/${i.id}`}> - <button - class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100" - > - <Icon icon="move" style="w-8 mx-auto invert" /> - </button> - </a> - </button> - </div> - {/if} - </div> - <div class="bg-white rounded-xl py-2"> - <div class="flex text-left gap-2"> - <div class="overflow-hidden space-y-1"> - <div class="text-2xl break-words">{i.name}</div> - <div class="flex gap-2"> - <div class="font-semibold text-lg"> - {f(i.price, currency)} - </div> - <div class="text-secondary my-auto"> - {sat(btc(i.price, rate))} - </div> - </div> - </div> - <div class="flex gap-1 ml-auto mb-auto justify-around tabular-nums"> - {#if i.quantity} - <button - type="button" - on:click|stopPropagation={() => - i.quantity > 0 && i.quantity--} - > - <Icon icon="minus" style="w-10 min-w-10" /> - </button> - {/if} - <div class="text-2xl my-auto text-center"> - {#if i.quantity}{i.quantity}{:else}—{/if} - </div> - {#if i.quantity} - <button - type="button" - on:click|stopPropagation={() => i.quantity++} - > - <Icon icon="plus" style="w-10 min-w-10" /> - </button> - {/if} - </div> - </div> - </div> - </button> - {/each} - </div> + <Items {items} {subject} {user} {total} {currency} {rate} /> + <div class="fixed inset-x-0 mx-auto flex bottom-16" class:static={!items.length} diff --git a/src/routes/(app)/[username]/+layout.server.ts b/src/routes/(app)/[username]/+layout.server.ts index 11e6d45c..9a520036 100644 --- a/src/routes/(app)/[username]/+layout.server.ts +++ b/src/routes/(app)/[username]/+layout.server.ts @@ -1,17 +1,24 @@ import { get } from "$lib/utils"; import { error } from "@sveltejs/kit"; -export let load = async ({ cookies, depends, params: { username } }) => { +export let load = async ({ + cookies, + depends, + params: { username }, + parent, +}) => { depends("app:user"); try { let subject = await get(`/users/${username}`); + let { user } = await parent(); + if (!user) user = subject; let expires = new Date(); expires.setSeconds(expires.getSeconds() + 380 * 24 * 60 * 60); cookies.set("username", username, { path: "/", expires }); - return { subject }; + return { subject, user }; } catch (e) { error(500, "Unable to retrieve user account data"); } diff --git a/src/routes/(app)/[username]/+layout.svelte b/src/routes/(app)/[username]/+layout.svelte index dd29e3aa..ce152831 100644 --- a/src/routes/(app)/[username]/+layout.svelte +++ b/src/routes/(app)/[username]/+layout.svelte @@ -1,57 +1,9 @@ <script> - import AppHeader from "$comp/AppHeader.svelte"; - import { onNavigate } from "$app/navigation"; export let data; - - onNavigate((navigation) => { - if (!document.startViewTransition) return; - - return new Promise((resolve) => { - document.startViewTransition(async () => { - resolve(); - await navigation.complete; - }); - }); - }); </script> -<AppHeader {data} /> <div class="content"> <slot /> </div> -<style> - @keyframes fade-in { - from { - opacity: 0; - } - } - - @keyframes fade-out { - to { - opacity: 0; - } - } - - @keyframes slide-from-right { - from { - transform: translateX(60px); - } - } - - @keyframes slide-to-left { - to { - transform: translateX(-60px); - } - } - - :root::view-transition-old(root) { - animation: 280ms cubic-bezier(0.4, 0, 1, 1) both fade-out, - 180ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; - } - - :root::view-transition-new(root) { - animation: 280ms cubic-bezier(0, 0, 0.2, 1) 80ms both fade-in, - 180ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; - } -</style> +<slot name="fixed" /> diff --git a/src/routes/(app)/[username]/address/+page.server.ts b/src/routes/(app)/[username]/address/+page.server.ts deleted file mode 100644 index 2f7841ab..00000000 --- a/src/routes/(app)/[username]/address/+page.server.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { auth, post } from "$lib/utils"; -import { redirect } from "@sveltejs/kit"; - -export async function load({ cookies, parent }) { - let user = (await parent()).subject; - let invoice = { type: "bitcoin" }; - let { id } = await post(`/invoice`, { invoice, user }, auth(cookies)); - redirect(307, `/${user.username}/invoice/${id}`); -} diff --git a/src/routes/(app)/[username]/card/[serial]/+page.server.ts b/src/routes/(app)/[username]/card/[serial]/+page.server.ts deleted file mode 100644 index 945eee30..00000000 --- a/src/routes/(app)/[username]/card/[serial]/+page.server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { auth, get } from "$lib/utils"; -import { redirect } from "@sveltejs/kit"; - -export let load = async ({ cookies, params }) => { - let { serial } = params; - let fund; - try { - fund = await get(`/fund/${serial}`, auth(cookies)); - } catch (e) { - console.log(e); - } - - if (fund) redirect(307, `/fund/${serial}`); - - return params; -}; diff --git a/src/routes/(app)/[username]/card/[serial]/+page.svelte b/src/routes/(app)/[username]/card/[serial]/+page.svelte deleted file mode 100644 index 1d11ebee..00000000 --- a/src/routes/(app)/[username]/card/[serial]/+page.svelte +++ /dev/null @@ -1,36 +0,0 @@ -<script> - import Card from "$comp/Card.svelte"; - import Icon from "$comp/Icon.svelte"; - export let data; - let { serial } = data; - let balance = 0; -</script> - -<div class="space-y-5"> - <div class="relative flex justify-center gap-2"> - <h1 class="text-2xl md:text-3xl font-semibold my-auto">Card {serial}</h1> - </div> - <div class="flex justify-center"> - <a href={`/send/pot/${serial}`}> - <button - class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60 bg-black text-white justify-center gap-2" - > - <svg - xmlns="http://www.w3.org/2000/svg" - class="invert text-black peer-placeholder-shown:text-gray-300 h-6 w-6 my-auto" - fill="none" - viewBox="0 0 24 24" - stroke="currentColor" - > - <path - stroke-linecap="round" - stroke-linejoin="round" - stroke-width="2" - d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" - /> - </svg> - <div class="my-auto">Add Funds</div> - </button> - </a> - </div> -</div> diff --git a/src/routes/(app)/[username]/customers/+page.svelte b/src/routes/(app)/[username]/customers/+page.svelte deleted file mode 100644 index e69de29b..00000000 --- a/src/routes/(app)/[username]/customers/+page.svelte +++ /dev/null diff --git a/src/routes/(app)/[username]/funds/+page.server.ts b/src/routes/(app)/[username]/funds/+page.server.ts deleted file mode 100644 index e69de29b..00000000 --- a/src/routes/(app)/[username]/funds/+page.server.ts +++ /dev/null diff --git a/src/routes/(app)/[username]/funds/+page.svelte b/src/routes/(app)/[username]/funds/+page.svelte deleted file mode 100644 index 25686e86..00000000 --- a/src/routes/(app)/[username]/funds/+page.svelte +++ /dev/null @@ -1,52 +0,0 @@ -<script> - import Avatar from "$comp/Avatar.svelte"; - import Icon from "$comp/Icon.svelte"; - import { t } from "$lib/translations"; - - export let data; - - let { funds } = data; -</script> - -<div class="mt-24 mb-20"> - <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold mb-10"> - Funds - </h1> - - <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5"> - <div class="text-base"> - <div class="grid grid-cols-3 border-b h-24 hover:bg-gray-100 px-4"> - <div class="whitespace-nowrap my-auto"> - <div class="text-secondary" /> - </div> - - <div class="flex my-auto"> - <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> - </div> - - <div class="text-secondary text-right text-sm my-auto"> - <div /> - </div> - </div> - </div> - - <button - class="ml-auto rounded-full border py-2 px-4 w-36 hover:opacity-80 flex mx-auto" - > - <Icon icon="plus" style="opacity-50 mr-2 my-auto" /> - <div class="my-auto">{$t("payments.create")}</div> - </button> - </div> -</div> - -<style> - .active * { - @apply bg-black text-white border-black; - } -</style> diff --git a/src/routes/(app)/[username]/invoice/[id]/+layout.server.ts b/src/routes/(app)/[username]/invoice/[id]/+layout.server.ts deleted file mode 100644 index 41554c9f..00000000 --- a/src/routes/(app)/[username]/invoice/[id]/+layout.server.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Qr from "qrcode-base64"; -import { get } from "$lib/utils"; -import { redirect } from "@sveltejs/kit"; - -export async function load({ depends, params, url, parent }) { - depends("app:invoice"); - - let { subject, user } = await parent(); - let { id } = params; - let invoice = await get(`/invoice/${id}`); - let options = !!url.searchParams.get("options"); - - if ( - user && - invoice.uid !== user.id && - !(url.pathname.includes("tip") || url.pathname.includes("memo")) && - !options - ) { - redirect(307, `/send/invoice/${id}`); - } - - let { amount, pending, received } = invoice; - amount = parseInt(amount); - - let paid = - (!amount && (pending || received)) || - (amount > 0 && (pending >= amount || received >= amount)); - if (paid && !url.pathname.endsWith("paid")) { - redirect( - 307, - `/${params.username}/invoice/${id}/paid` + - (options ? "?options=true" : "") - ); - } - - let src = Qr.drawImg(invoice.text || "", { size: 340 }); - - return { id, invoice, subject, src }; -} diff --git a/src/routes/(app)/[username]/map/+page.server.ts b/src/routes/(app)/[username]/map/+page.server.ts deleted file mode 100644 index 616d28f6..00000000 --- a/src/routes/(app)/[username]/map/+page.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { get } from "$lib/utils"; - -// export const prerender = true; -export let load = async () => { - let data = { locations: [] }; - - try { - data = await get("/locations"); - } catch (e) {} - - return data; -}; diff --git a/src/routes/(app)/[username]/map/+page.svelte b/src/routes/(app)/[username]/map/+page.svelte deleted file mode 100644 index bfa115e4..00000000 --- a/src/routes/(app)/[username]/map/+page.svelte +++ /dev/null @@ -1,8 +0,0 @@ -<script> - import LocationsMap from "$comp/LocationsMap.svelte"; - export let data; - - let { locations } = data; -</script> - -<LocationsMap {locations} /> diff --git a/src/routes/(app)/[username]/receive/+page.svelte b/src/routes/(app)/[username]/receive/+page.svelte new file mode 100644 index 00000000..11587211 --- /dev/null +++ b/src/routes/(app)/[username]/receive/+page.svelte @@ -0,0 +1,343 @@ +<script> + import { send } from "$lib/socket"; + import { + btc, + post, + back, + copy, + f, + get, + types, + sat, + reverseFormat, + s, + sats, + } from "$lib/utils"; + import { tick, onMount, onDestroy } from "svelte"; + import { browser } from "$app/environment"; + import { last, showQr, amountPrompt } from "$lib/store"; + import Avatar from "$comp/Avatar.svelte"; + import Icon from "$comp/Icon.svelte"; + import Heart from "$comp/Heart.svelte"; + import Image from "$comp/Image.svelte"; + import Numpad from "$comp/Numpad.svelte"; + import { t } from "$lib/translations"; + import { goto, invalidate } from "$app/navigation"; + import Toggle from "$comp/Toggle.svelte"; + + export let data; + + let showOptions; + + let reading = async ({ message, serialNumber }) => { + let name = serialNumber.replace(/:/g, "-"); + let result = await post(`/fund/${name}/withdraw`, { amount, hash, name }); + }; + + let readingerror = (e) => console.log("nfc error", e); + + $: refresh(data); + let { invoice, id, subject, user, src } = data; + let { + aid, + amount, + hash, + type, + rate, + text, + tip, + user: { username, currency }, + } = invoice; + + let qr; + let tipPercent = 0; + + let refresh = async (data) => { + ({ invoice, id, user, src } = data); + ({ + aid, + amount, + hash, + type, + rate, + text, + tip, + user: { username, currency }, + } = invoice); + + tipPercent = (tip / amount) * 100; + }; + + $: amountFiat = parseFloat(((amount * rate) / sats).toFixed(2)); + $: tipAmount = ((tip * rate) / sats).toFixed(2); + + let subbed; + + onMount(async () => { + if (browser) { + last.subscribe((v) => { + if (!v || subbed) return; + subbed = true; + send("subscribe", invoice); + }); + + if ($amountPrompt && tip === undefined) toggleAmount(); + } + }); + + $: link = [types.bitcoin, types.liquid].includes(type) + ? text + : `lightning:${text}`; + $: txt = [types.bitcoin, types.liquid].includes(type) ? hash : text; + + let setType = async (type) => { + invoice.type = type; + ({ id } = await post(`/invoice`, { + invoice, + user: { username, currency }, + })); + + goto(`./${id}?options=true`, { invalidateAll: true, noScroll: true }); + }; + + let setAmount = async () => { + if (typeof $amountPrompt === "undefined") $amountPrompt = true; + settingAmount = false; + amount = newAmount; + invoice.amount = newAmount; + invoice.tip = 0; + ({ id } = await post(`/invoice`, { + invoice, + user: { username, currency }, + })); + + let url = `./${id}`; + if (subject?.prompt) url += "/tip"; + else url += "?options=true"; + + goto(url, { invalidateAll: true, noScroll: true }); + }; + + let newAmount; + let settingAmount; + let toggleAmount = () => (settingAmount = !settingAmount); + let fiat; + let submit; +</script> + +<div class="invoice container mx-auto max-w-xl px-4 space-y-2"> + {#if $showQr} + <div> + <a + href={[types.bitcoin, types.liquid].includes(invoice.type) + ? invoice.text + : "lightning:" + invoice.text} + > + <img + {src} + class="mx-auto z-10 max-w-[360px]" + bind:this={qr} + alt={txt} + /> + </a> + </div> + {:else} + <div class="break-all pb-5 text-center text-secondary text-xl"> + {txt} + </div> + {/if} + + {#each invoice.items as i} + <div class="grid grid-cols-12 text-xl"> + <div class="col-span-1 my-auto">{i.quantity}</div> + <div class="mr-auto grow col-span-7 my-auto"> + {i.name} + </div> + <div class="col-span-2 font-semibold text-right my-auto"> + {f(i.price * i.quantity, currency)} + </div> + <div class="col-span-2 text-secondary text-right text-lg my-auto"> + {sat(btc(i.price * i.quantity, rate))} + </div> + </div> + {/each} + + {#if amount > 0} + <div class="text-center font-bold text-2xl"> + <div> + {f(amountFiat, currency)} + + {#if tip} + <span class="text-sm"> + +{f(tip * (rate / sats), currency)} + </span> + {/if} + </div> + <div> + <span class="text-secondary font-normal text-xl" + >⚡️{`${s(amount)}`}</span + > + + {#if tip} + <span class="text-sm text-secondary font-normal"> + +⚡️{s(tip)} + </span> + {/if} + </div> + </div> + {/if} + + <div class="text-secondary space-y-2 text-xl pt-2"> + <button + type="button" + class="flex gap-2 text-center break-all rounded-2xl hover:opacity-80 py-5 px-6 w-full mx-auto justify-center border whitespace-nowrap mb-2" + on:click={() => copy(txt)} + > + <Icon icon="copy" style="w-8 my-auto" /> + <div class="my-auto"> + {$t("payments.copy")} + </div> + </button> + + {#if user?.id === invoice?.user?.id} + <button + class="w-full flex justify-center rounded-2xl border py-5 px-6 hover:opacity-80" + on:click={toggleAmount} + > + <Icon icon="edit" style="mr-1 w-8" /> + <div class="my-auto">{$t("payments.setAmount")}</div> + </button> + {:else} + <a href={link} class="w-full"> + <button + class="w-full flex justify-center rounded-2xl border py-5 px-6 hover:opacity-80" + > + <Icon icon="mobile" style="mr-1 w-8" /> + <div class="my-auto">{$t("payments.openLink")}</div> + </button> + </a> + {/if} + + <div class="w-full flex justify-center gap-2 flex-wrap"> + <button + class="w-full flex justify-center rounded-2xl border py-5 px-6 hover:opacity-80" + on:click={() => ($showQr = !$showQr)} + > + {#if $showQr} + <Icon icon="text" style="w-6 mr-2 my-auto" /> + {:else} + <Icon icon="qr" style="w-8 mr-1 invert my-auto" /> + {/if} + <div class="my-auto"> + {$showQr ? $t("payments.showText") : $t("payments.showQr")} + </div></button + > + </div> + </div> + + {#if aid === user.id} + <div class="grid grid-cols-2 justify-around text-secondary"> + <button + class="hover:bg-primary my-auto flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center" + class:bg-primary={type === types.bitcoin} + class:text-black={type === types.bitcoin} + on:click={() => setType(types.bitcoin)} + > + <img src="/images/bitcoin.svg" class="w-8" alt="Bitcoin" /> + <div class="my-auto text-lg">Bitcoin</div> + </button> + + <button + class="hover:bg-primary my-auto flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center" + class:bg-primary={type === types.liquid} + class:text-black={type === types.liquid} + on:click={() => setType(types.liquid)} + > + <img src="/images/liquid.svg" class="w-8" alt="Liquid" /> + <div class="my-auto text-lg">Liquid</div> + </button> + + <button + class="hover:bg-primary flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center" + class:bg-primary={type === types.lightning} + class:text-black={type === types.lightning} + on:click={() => setType(types.lightning)} + > + <div class="bg-white rounded-full w-8 h-8 text-center flex"> + <div class="m-auto">⚡️</div> + </div> + <div class="my-auto text-lg">Lightning</div> + </button> + + {#if user?.id === invoice?.user?.id} + <a href={`/receive`} class="block contents"> + <button + class="hover:bg-primary flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center" + > + <img src="/images/cash.png" class="w-8 my-auto" /> + <div class="my-auto text-lg">Ecash</div> + </button> + </a> + {/if} + </div> + + {#if type === types.liquid} + <div class="flex justify-center"> + <div class="my-auto text-xl text-center text-secondary"> + {$t("payments.onlyLbtc")} + <span class="text-teal-500 font-bold">L-BTC</span>, + {$t("payments.dontDeposit")} + </div> + </div> + {/if} + {/if} +</div> + +{#if settingAmount} + <div + class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50" + > + <div + class="relative mx-auto p-12 border w-96 shadow-lg rounded-md bg-white space-y-5 top-10" + > + <form on:submit|preventDefault={setAmount}> + <Numpad + bind:amount={newAmount} + bind:currency + bind:rate + bind:fiat + bind:submit + /> + <div class="w-full flex flex-wrap gap-2"> + <button + bind:this={submit} + type="submit" + on:click={setAmount} + class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto bg-black text-white w-full" + > + <div class="my-auto">Ok</div> + </button> + <button + type="button" + class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto w-full" + on:click={toggleAmount} + on:keydown={toggleAmount} + > + <div class="my-auto">Cancel</div> + </button> + </div> + </form> + + <div class="flex justify-center gap-3"> + <div class="text-secondary">{$t("payments.amountPrompt")}</div> + <Toggle id="notify" bind:value={$amountPrompt} /> + </div> + </div> + </div> +{/if} + +<style> + .invoice { + view-transition-name: invoice; + } +</style> diff --git a/src/routes/(app)/[username]/receive/[...param]/+page.server.ts b/src/routes/(app)/[username]/receive/[...param]/+page.server.ts deleted file mode 100644 index 502678a9..00000000 --- a/src/routes/(app)/[username]/receive/[...param]/+page.server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { redirect } from "@sveltejs/kit"; -import { auth, post } from "$lib/utils"; -import type { Invoice } from "$lib/types"; - -export let load = async ({ cookies, parent }) => { - let {subject, user, rates } = await parent(); - - let invoice: Invoice = { - type: "lightning", - rate: rates[user?.currency || subject?.currency], - }; - - if(!user) user = subject; - invoice = await post("/invoice", { invoice, user }, auth(cookies)); - let { id } = invoice; - - if (invoice.memoPrompt && !invoice.memo) { - redirect(307, `/${user.username}/invoice/${id}/memo`); - } else redirect(307, `/${user.username}/invoice/${id}`); -}; diff --git a/src/routes/(app)/[username]/receive/[...param]/+page.svelte b/src/routes/(app)/[username]/receive/[...param]/+page.svelte deleted file mode 100644 index f9267105..00000000 --- a/src/routes/(app)/[username]/receive/[...param]/+page.svelte +++ /dev/null @@ -1,116 +0,0 @@ -<script> - import { page } from "$app/stores"; - import { fly } from "svelte/transition"; - import { enhance } from "$app/forms"; - import { tick } from "svelte"; - import { browser } from "$app/environment"; - import { t } from "$lib/translations"; - import AppHeader from "$comp/AppHeader.svelte"; - import Avatar from "$comp/Avatar.svelte"; - import Icon from "$comp/Icon.svelte"; - import Spinner from "$comp/Spinner.svelte"; - import { back, fail, focus } from "$lib/utils"; - - export let data; - export let form; - data.subject = data.user; - - let { contacts } = data; - let w; - - let el, text; - - let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click()); - - let paste = async () => { - text = await navigator.clipboard.readText(); - }; -</script> - -<svelte:window bind:innerWidth={w} /> - -<AppHeader {data} /> -<div class="container px-4 max-w-lg mx-auto space-y-5 mt-20"> - <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> - {$t("payments.send")} - </h1> - - <form method="POST" use:enhance class="space-y-2 text-xl"> - {#if form?.error} - <div class="mb-5"> - <div class="text-red-600"> - {$t("error.unrecognizedInput")} - </div> - </div> - {/if} - - <div class="mb-2"> - <textarea - use:focus - name="text" - placeholder={$t("user.send.placeholder")} - on:keypress={keypress} - class="w-full p-4 border rounded-xl h-48 text-xl" - bind:value={text} - autocapitalize="none" - /> - </div> - - <div class="flex gap-2"> - <a href="/scan" class="w-full"> - <button - type="button" - class="flex border rounded-2xl px-6 py-5 font-bold hover:opacity-80 w-full bg-primary justify-center" - > - <Icon icon="camera" style="mr-2 w-6 my-auto" /> - <div class="my-auto">{$t("user.send.scan")}</div> - </button> - </a> - - <button - type="button" - class="flex border rounded-2xl px-6 py-5 font-bold hover:opacity-80 w-full bg-primary justify-center" - on:click={paste} - > - <Icon icon="paste" style="mr-2 w-6 my-auto" /> - <div class="my-auto">{$t("user.send.paste")}</div> - </button> - </div> - - <button - bind:this={el} - type="submit" - class="bg-black text-white border rounded-2xl px-6 py-5 w-full font-bold" - > - {$t("user.send.next")} - </button> - </form> - - {#if contacts.length} - <div class="space-y-5"> - <h1 class="px-3 md:px-0 text-2xl font-semibold mt-10"> - {$t("user.send.contacts")} - </h1> - <div> - {#each contacts as c} - <a href={`/pay/${c.username}`}> - <div - class="border-b p-2 last:border-b-0 hover:bg-gray-100 rounded-2xl" - > - <div class="flex"> - <div> - <div class="flex"> - <Avatar user={c} size={20} disabled={true} /> - <div class="my-auto text-left"> - <p class="ml-1 text-lg break-words">{c.username}</p> - </div> - </div> - </div> - </div> - </div> - </a> - {/each} - </div> - </div> - {/if} -</div> diff --git a/src/routes/(app)/[username]/reset/+server.ts b/src/routes/(app)/[username]/reset/+server.ts deleted file mode 100644 index f7dba02c..00000000 --- a/src/routes/(app)/[username]/reset/+server.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { json } from "@sveltejs/kit"; -import { auth, post } from "$lib/utils"; - -export async function POST({ cookies, request, params }) { - let { username } = params; - let r = await request.json(); - let { password } = r; - let result = await post("/reset", { password, username }, auth(cookies)); - return json(result); -} diff --git a/src/routes/(app)/account/[id]/+page.server.ts b/src/routes/(app)/account/[id]/+page.server.ts new file mode 100644 index 00000000..35b5517d --- /dev/null +++ b/src/routes/(app)/account/[id]/+page.server.ts @@ -0,0 +1,25 @@ +import { error, redirect } from "@sveltejs/kit"; +import { auth, get, post, fd } from "$lib/utils"; + + +export let load = async ({ cookies, params }) => { + let account = await get(`/account/${params.id}`, auth(cookies)); + return { account }; +} + +export const actions = { + default: async ({ cookies, params, request }) => { + let { id } = params; + let body = await fd(request); + let username = cookies.get("username"); + + try { + await post(`/account/${id}`, body, auth(cookies)); + } catch (e: any) { + console.log(e); + error(400, { message: e.message }); + } + + redirect(303, `/${username}`); + }, +}; diff --git a/src/routes/(app)/account/[id]/+page.svelte b/src/routes/(app)/account/[id]/+page.svelte new file mode 100644 index 00000000..76473a89 --- /dev/null +++ b/src/routes/(app)/account/[id]/+page.svelte @@ -0,0 +1,94 @@ +<script> + import Mnemonic from "$comp/Mnemonic.svelte"; + import WalletPass from "$comp/WalletPass.svelte"; + import { goto } from "$app/navigation"; + import { copy, fail, focus, post } from "$lib/utils"; + import { enhance } from "$app/forms"; + import { t } from "$lib/translations"; + import Icon from "$comp/Icon.svelte"; + import { decrypt } from "nostr-tools/nip49"; + import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39"; + import { wordlist } from "@scure/bip39/wordlists/english"; + + export let data; + let { account, user } = data; + let { id, name, seed } = account; + let mnemonic, password; + let passwordPrompt; + let cancel = () => (passwordPrompt = false); + let toggle = () => (passwordPrompt = !passwordPrompt); + + let submit = async () => { + toggle(); + let entropy = await decrypt(seed, password); + mnemonic = entropyToMnemonic(entropy, wordlist); + }; + + let del = async () => { + try { + await post("/api/account/delete", { id }); + goto(`/${user.username}`); + } catch (e) { + fail(e.message); + } + }; +</script> + +<div class="space-y-5"> + <h1 class="text-center text-3xl font-semibold">Account settings</h1> + + <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-2"> + <form class="space-y-5" method="POST"> + <!-- <div> --> + <!-- <label for="display" class="font-bold mb-1 block">Account name</label> --> + <!-- <input use:focus type="text" name="name" bind:value={name} /> --> + <!-- </div> --> + <!-- --> + <div class="space-y-2"> + <!-- <button --> + <!-- type="submit" --> + <!-- class="rounded-2xl border py-5 font-bold mx-auto bg-black text-white px-6 w-full" --> + <!-- >{$t("accounts.save")}</button --> + <!-- > --> + {#if mnemonic} + <Mnemonic {mnemonic} /> + + <button + on:click={() => copy(mnemonic)} + type="button" + class="flex gap-2 rounded-2xl border py-5 mx-auto px-6 w-full justify-center" + > + <Icon icon="copy" style="w-8 my-auto" /> + <div class="my-auto">{$t("accounts.copy")}</div></button + > + {:else if seed} + <button + on:click={toggle} + type="button" + class="flex gap-2 rounded-2xl border py-5 mx-auto px-6 w-full justify-center" + > + <Icon icon="eye" style="w-8 my-auto" /> + <div class="my-auto">{$t("accounts.revealMnemonic")}</div></button + > + {/if} + + {#if seed} + <button + on:click={del} + type="button" + class="flex gap-2 rounded-2xl border py-5 mx-auto px-6 w-full justify-center" + > + <Icon icon="trash" style="w-8 my-auto" /> + <div class="my-auto">{$t("accounts.deleteAccount")}</div></button + > + {/if} + </div> + + <div /> + </form> + </div> +</div> + +{#if passwordPrompt} + <WalletPass bind:password bind:cancel bind:submit /> +{/if} diff --git a/src/routes/(app)/account/cash/+page.svelte b/src/routes/(app)/account/cash/+page.svelte new file mode 100644 index 00000000..b0e2e757 --- /dev/null +++ b/src/routes/(app)/account/cash/+page.svelte @@ -0,0 +1,37 @@ +<script> + import { t } from "$lib/translations"; + import Icon from "$comp/Icon.svelte"; +</script> + +<div class="space-y-5"> + <h1 class="text-center text-3xl font-semibold">Choose your custodian</h1> + + <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-2"> + <div class="space-y-5"> + <a href="/account/cash" class="block"> + <div class="shadow p-8 rounded-xl flex gap-4 hover:bg-primary"> + <img src="/icons/logo-symbol.svg" class="w-20" /> + <div> + <div class="text-xl">Coinos</div> + <div class="text-secondary">https://mint.coinos.io</div> + </div> + </div> + </a> + <a href="/account/cash" class="block"> + <div class="shadow p-8 rounded-xl flex gap-4 hover:bg-primary"> + <img src="/images/minibits.png" class="w-20" /> + <div> + <div class="text-xl">Minibits</div> + <div class="text-secondary">https://mint.minibits.cash/bitcoin</div> + </div> + </div> + </a> + </div> + <!-- <a href="/account/mints" class="block"> --> + <!-- <div class="p-4 border rounded-xl flex justify-center gap-2"> --> + <!-- <Icon icon="plus" style="w-8 my-auto" /> --> + <!-- <div class="text-xl my-auto">More</div> --> + <!-- </div> --> + <!-- </a> --> + </div> +</div> diff --git a/src/routes/(app)/account/import/+page.svelte b/src/routes/(app)/account/import/+page.svelte new file mode 100644 index 00000000..a1d6549b --- /dev/null +++ b/src/routes/(app)/account/import/+page.svelte @@ -0,0 +1,55 @@ +<script> + import { goto } from "$app/navigation"; + import { mnemonic } from "$lib/store"; + import { fail, focus, versions } from "$lib/utils"; + import { t } from "$lib/translations"; + import { browser } from "$app/environment"; + import Icon from "$comp/Icon.svelte"; + import { validateMnemonic } from "@scure/bip39"; + import { wordlist } from "@scure/bip39/wordlists/english"; + + let el, pasted, text; + + let paste = async () => { + text = await navigator.clipboard.readText(); + pasted = true; + }; + + let submit = () => { + text = text.replace(/,/g, " ").trim(); + let valid = validateMnemonic(text, wordlist); + if (valid) { + $mnemonic = text; + goto("/account/pass"); + } else { + fail("Failed to validate seed phrase"); + } + }; + + $: if (browser && pasted && text) form.submit() && (pasted = false); +</script> + +<div class="space-y-5"> + <h1 class="text-center text-3xl font-semibold">{$t("accounts.import")}</h1> + + <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-2"> + <form on:submit|preventDefault={submit} class="space-y-2"> + <textarea + use:focus + name="text" + placeholder={$t("accounts.enterSeed")} + class="w-full p-4 border rounded-xl h-48 text-xl" + bind:value={text} + autocapitalize="none" + /> + <button + bind:this={el} + type="submit" + class="flex bg-black text-white border rounded-2xl px-6 py-5 w-full font-bold gap-2 justify-center" + > + <Icon icon="send" style="w-8 my-auto invert" /> + <div class="my-auto">{$t("user.send.next")}</div> + </button> + </form> + </div> +</div> diff --git a/src/routes/(app)/account/mints/+page.svelte b/src/routes/(app)/account/mints/+page.svelte new file mode 100644 index 00000000..1080269e --- /dev/null +++ b/src/routes/(app)/account/mints/+page.svelte @@ -0,0 +1 @@ +mints diff --git a/src/routes/(app)/account/pass/+page.svelte b/src/routes/(app)/account/pass/+page.svelte new file mode 100644 index 00000000..77298cc4 --- /dev/null +++ b/src/routes/(app)/account/pass/+page.svelte @@ -0,0 +1,149 @@ +<script lang="ts"> + import { onMount } from "svelte"; + import { t } from "$lib/translations"; + import { mnemonic } from "$lib/store"; + import Icon from "$comp/Icon.svelte"; + import { encrypt } from "nostr-tools/nip49"; + import { + mnemonicToSeed, + mnemonicToEntropy, + entropyToMnemonic, + } from "@scure/bip39"; + import { wordlist } from "@scure/bip39/wordlists/english"; + import { focus, versions, fail, post } from "$lib/utils"; + import { goto } from "$app/navigation"; + import { HDKey } from "@scure/bip32"; + + export let data; + + let { user } = data; + + let confirm, password, revealPassword, revealConfirm; + let type = "bitcoin"; + let name = $t("accounts.savings"); + + onMount(() => { + if (!$mnemonic) goto("/account/savings"); + }); + + let submit = async () => { + if (!password || password !== confirm) { + fail($t("accounts.passwordMismatch")); + return; + } + + try { + let seed = await encrypt( + mnemonicToEntropy($mnemonic, wordlist), + password + ); + let master = HDKey.fromMasterSeed( + await mnemonicToSeed($mnemonic, password), + versions + ); + let child = master.derive("m/84'/0'/0'"); + let pubkey = child.publicExtendedKey; + let fingerprint = child.fingerprint.toString(16).padStart(8, "0"); + await post("/api/accounts", { fingerprint, pubkey, name, seed, type }); + goto(`/${user.username}`); + } catch (e) { + console.log(e); + fail(e.message); + } + + $mnemonic = ""; + }; +</script> + +<div class="space-y-5"> + <div> + <h1 class="text-center text-3xl font-semibold"> + {$t("accounts.accountPassword")} + </h1> + </div> + + <form + on:submit|preventDefault={submit} + class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5" + > + <p class="text-secondary mb-1"> + {$t("accounts.passwordWarning")} + </p> + + <div class="relative"> + {#if revealPassword} + <input + name="password" + type="text" + required + bind:value={password} + autocapitalize="none" + /> + {:else} + <input + use:focus + name="password" + type="password" + placeholder={$t("accounts.password")} + required + bind:value={password} + autocapitalize="none" + /> + {/if} + <button + tabindex="-1" + type="button" + on:click={() => (revealPassword = !revealPassword)} + class="absolute right-5 -translate-y-1/2 top-1/2" + > + <Icon icon={revealPassword ? "eye" : "eye-off"} /> + </button> + </div> + + <div class="relative"> + {#if revealConfirm} + <input + name="password" + type="text" + placeholder="Confirm password" + required + bind:value={confirm} + autocapitalize="none" + /> + {:else} + <input + type="password" + placeholder={$t("accounts.confirmPassword")} + required + bind:value={confirm} + autocapitalize="none" + /> + {/if} + <button + type="button" + tabindex="-1" + on:click={() => (revealConfirm = !revealConfirm)} + class="absolute right-5 -translate-y-1/2 top-1/2" + > + <Icon icon={revealConfirm ? "eye" : "eye-off"} /> + </button> + </div> + + <div class="flex gap-2"> + <a href={`/account/seed`} class="contents"> + <button + type="button" + class="border grow px-5 py-6 text-xl rounded-2xl" + > + {$t("accounts.back")} + </button> + </a> + <button + type="submit" + class="border grow px-5 py-6 text-xl rounded-2xl" + > + {$t("accounts.next")} + </button> + </div> + </form> +</div> diff --git a/src/routes/(app)/account/savings/+page.svelte b/src/routes/(app)/account/savings/+page.svelte new file mode 100644 index 00000000..f9c6305c --- /dev/null +++ b/src/routes/(app)/account/savings/+page.svelte @@ -0,0 +1,31 @@ +<script> + import { t } from "$lib/translations"; + import Icon from "$comp/Icon.svelte"; +</script> + +<div class="space-y-5"> + <h1 class="text-center text-3xl font-semibold">{$t("accounts.accountSetup")}</h1> + + <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-2"> + <div class="space-y-5"> + <a href="/account/seed" class="block"> + <div class="shadow p-8 rounded-xl flex gap-4 hover:bg-primary"> + <img src="/icons/random.svg" class="w-20" /> + <div> + <div class="text-xl">{$t("accounts.create")}</div> + <div class="text-secondary">{$t("accounts.generateSeed")}</div> + </div> + </div> + </a> + <a href="/account/import" class="block"> + <div class="shadow p-8 rounded-xl flex gap-4 hover:bg-primary"> + <img src="/images/keys.png" class="w-20" /> + <div> + <div class="text-xl">{$t("accounts.import")}</div> + <div class="text-secondary">{$t("accounts.importSeed")}</div> + </div> + </div> + </a> + </div> + </div> +</div> diff --git a/src/routes/(app)/account/seed/+page.svelte b/src/routes/(app)/account/seed/+page.svelte new file mode 100644 index 00000000..ec5ab1cf --- /dev/null +++ b/src/routes/(app)/account/seed/+page.svelte @@ -0,0 +1,76 @@ +<script> + import Mnemonic from "$comp/Mnemonic.svelte"; + import { browser } from "$app/environment"; + import { tick, onMount } from "svelte"; + import { copy } from "$lib/utils"; + import { t } from "$lib/translations"; + import Icon from "$comp/Icon.svelte"; + import { mnemonic } from "$lib/store"; + import { generateMnemonic } from "@scure/bip39"; + import { wordlist } from "@scure/bip39/wordlists/english"; + + export let data; + let { user } = data; + + onMount(async () => { + if (!browser) return; + if (!$mnemonic) await generate(); + }); + + let generate = async () => { + $mnemonic = await generateMnemonic(wordlist); + }; +</script> + +<div class="space-y-5"> + <h1 class="text-center text-3xl font-semibold"> + {$t("accounts.seedPhrase")} + </h1> + + <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5"> + {#if $mnemonic} + <Mnemonic mnemonic={$mnemonic} /> + {/if} + + <div> + <button + type="button" + class="flex gap-2 text-center break-all rounded-2xl hover:opacity-80 py-5 px-6 w-full mx-auto justify-center border whitespace-nowrap mb-2" + on:click={generate} + > + <Icon icon="random" style="w-8 my-auto" /> + <div class="my-auto">{$t("accounts.regenerate")}</div> + </button> + + <button + type="button" + class="flex gap-2 text-center break-all rounded-2xl hover:opacity-80 py-5 px-6 w-full mx-auto justify-center border whitespace-nowrap mb-2" + on:click={() => copy($mnemonic)} + > + <Icon icon="copy" style="w-8 my-auto" /> + <div class="my-auto"> + {$t("payments.copy")} + </div> + </button> + </div> + + <div class="flex gap-2"> + <a href={`/account/savings`} class="contents"> + <button + type="button" + class="border w-full py-5 px-6 text-xl rounded-2xl" + > + {$t("accounts.back")} + </button> + </a> + <a href={`/account/pass`} class="contents"> + <button + type="button" + class="border w-full py-5 px-6 text-xl rounded-2xl" + > + {$t("accounts.next")} + </button> + </a> + </div> + </div> +</div> diff --git a/src/routes/(app)/account/setup/+page.svelte b/src/routes/(app)/account/setup/+page.svelte new file mode 100644 index 00000000..f5a5ac09 --- /dev/null +++ b/src/routes/(app)/account/setup/+page.svelte @@ -0,0 +1,34 @@ +<script> + import { t } from "$lib/translations"; +</script> + +<div class="space-y-5"> + <h1 class="text-center text-3xl font-semibold">Choose account type</h1> + + <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5"> + <a href="/account/cash" class="block"> + <div class="shadow p-8 rounded-xl flex gap-4 hover:bg-primary"> + <img src="/images/cash.png" class="w-20 my-auto" /> + <div> + <div class="text-xl">Cash</div> + <div class="text-secondary"> + Your balance is held with a custodian. Payments are fast and + free. + </div> + </div> + </div> + </a> + <a href="/account/savings" class="block"> + <div class="shadow p-8 rounded-xl flex gap-4 hover:bg-primary"> + <img src="/images/keys.png" class="w-20 my-auto" /> + <div> + <div class="text-xl">Savings</div> + <div class="text-secondary"> + You hold the keys to the account and are responsible for + safeguarding them. + </div> + </div> + </div> + </a> + </div> +</div> diff --git a/src/routes/(app)/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte index c61a2e45..214bf5b3 100644 --- a/src/routes/(app)/admin/+page.svelte +++ b/src/routes/(app)/admin/+page.svelte @@ -1,7 +1,7 @@ <script> import { post } from "$lib/utils"; export let data; - let reset = (username) => post(`/${username}/reset`, { password }); + let reset = (username) => post(`/reset`, { password }); let { users } = data; let password; </script> diff --git a/src/routes/(app)/ecash/[id]/[...version]/+layout.svelte b/src/routes/(app)/ecash/[id]/[...version]/+layout.svelte index 679fbe76..89de4b58 100644 --- a/src/routes/(app)/ecash/[id]/[...version]/+layout.svelte +++ b/src/routes/(app)/ecash/[id]/[...version]/+layout.svelte @@ -1,5 +1,4 @@ <script> - import AppHeader from "$comp/AppHeader.svelte"; import { copy, sats, f, s } from "$lib/utils"; import { t } from "$lib/translations"; import Icon from "$comp/Icon.svelte"; diff --git a/src/routes/(app)/fund/[id]/+page.server.ts b/src/routes/(app)/fund/[id]/+page.server.ts index e553b1b4..6fd3af68 100644 --- a/src/routes/(app)/fund/[id]/+page.server.ts +++ b/src/routes/(app)/fund/[id]/+page.server.ts @@ -15,7 +15,7 @@ export const actions = { if (user) { await post("/redeem", params, auth(cookies)); - redirect(307, `/${user.username}/payments`); + redirect(307, `/payments`); } redirect(307, "/register"); diff --git a/src/routes/(app)/fund/[id]/sweep/+page.svelte b/src/routes/(app)/fund/[id]/sweep/+page.svelte index 4a48edf8..e142f00d 100644 --- a/src/routes/(app)/fund/[id]/sweep/+page.svelte +++ b/src/routes/(app)/fund/[id]/sweep/+page.svelte @@ -32,7 +32,7 @@ </h3> </div> -<a href={`/${user.username}/payments`}> +<a href={`/payments`}> <div class="opacity-0 w-screen h-screen fixed top-0 left-0 z-50" /> </a> diff --git a/src/routes/(app)/[username]/generate/+page.svelte b/src/routes/(app)/generate/+page.svelte index 767b7545..49d75dc6 100644 --- a/src/routes/(app)/[username]/generate/+page.svelte +++ b/src/routes/(app)/generate/+page.svelte @@ -28,7 +28,7 @@ try { user.pin = $pin; - await post(`/${user.username}/generate`, user); + await post(`/generate`, user); goto($loginRedirect || `/${user.username}`, { invalidateAll: true }); } catch (e) { $pin = ""; diff --git a/src/routes/(app)/[username]/generate/+server.ts b/src/routes/(app)/generate/+server.ts index 5a442982..5a442982 100644 --- a/src/routes/(app)/[username]/generate/+server.ts +++ b/src/routes/(app)/generate/+server.ts diff --git a/src/routes/(app)/invoice/+page.server.ts b/src/routes/(app)/invoice/+page.server.ts new file mode 100644 index 00000000..b8a2fe06 --- /dev/null +++ b/src/routes/(app)/invoice/+page.server.ts @@ -0,0 +1,28 @@ +import { auth, post } from "$lib/utils"; +import { error, redirect } from "@sveltejs/kit"; +import type { Invoice } from "$lib/types"; + +export let load = async ({ cookies, parent }) => { + let aid = cookies.get("aid"); + let { subject, user, rates } = await parent(); + + let invoice: Invoice = { + aid, + rate: rates[user?.currency || subject?.currency], + }; + + if (!user) user = subject; + + try { + invoice = await post("/invoice", { invoice, aid, user }, auth(cookies)); + } catch (e) { + console.log(e); + error(500, "Failed to generate invoice"); + } + + let { id } = invoice; + + if (invoice.memoPrompt && !invoice.memo) { + redirect(307, `/invoice/${id}/memo`); + } else redirect(307, `/invoice/${id}`); +}; diff --git a/src/routes/(app)/[username]/address/+page.svelte b/src/routes/(app)/invoice/+page.svelte index e69de29b..e69de29b 100644 --- a/src/routes/(app)/[username]/address/+page.svelte +++ b/src/routes/(app)/invoice/+page.svelte diff --git a/src/routes/(app)/[username]/invoice/+server.ts b/src/routes/(app)/invoice/+server.ts index 5e3d0300..5e3d0300 100644 --- a/src/routes/(app)/[username]/invoice/+server.ts +++ b/src/routes/(app)/invoice/+server.ts diff --git a/src/routes/(app)/invoice/[id]/+layout.server.ts b/src/routes/(app)/invoice/[id]/+layout.server.ts new file mode 100644 index 00000000..55c44f36 --- /dev/null +++ b/src/routes/(app)/invoice/[id]/+layout.server.ts @@ -0,0 +1,38 @@ +import * as Qr from "qrcode-base64"; +import { get } from "$lib/utils"; +import { redirect } from "@sveltejs/kit"; + +export async function load({ depends, params, url, parent }) { + depends("app:invoice"); + + let { subject, user } = await parent(); + let { id } = params; + let invoice; + + if (id) { + invoice = await get(`/invoice/${id}`); + let options = !!url.searchParams.get("options"); + + if ( + user && + invoice.uid !== user.id && + !(url.pathname.includes("tip") || url.pathname.includes("memo")) && + !options + ) { + redirect(307, `/send/invoice/${id}`); + } + + let { amount, pending, received } = invoice; + amount = parseInt(amount); + + let paid = + (!amount && (pending || received)) || + (amount > 0 && (pending >= amount || received >= amount)); + if (paid && !url.pathname.endsWith("paid")) { + redirect(307, `/invoice/${id}/paid` + (options ? "?options=true" : "")); + } + } + + let src = Qr.drawImg(invoice?.text || "", { size: 500 }); + return { id, invoice, subject, src }; +} diff --git a/src/routes/(app)/[username]/invoice/[id]/+layout.svelte b/src/routes/(app)/invoice/[id]/+layout.svelte index 4fa864ce..4fa864ce 100644 --- a/src/routes/(app)/[username]/invoice/[id]/+layout.svelte +++ b/src/routes/(app)/invoice/[id]/+layout.svelte diff --git a/src/routes/(app)/[username]/invoice/[id]/+page.server.ts b/src/routes/(app)/invoice/[id]/+page.server.ts index a57efa64..a57efa64 100644 --- a/src/routes/(app)/[username]/invoice/[id]/+page.server.ts +++ b/src/routes/(app)/invoice/[id]/+page.server.ts diff --git a/src/routes/(app)/[username]/invoice/[id]/+page.svelte b/src/routes/(app)/invoice/[id]/+page.svelte index 28bfe27b..5ef9e5e5 100644 --- a/src/routes/(app)/[username]/invoice/[id]/+page.svelte +++ b/src/routes/(app)/invoice/[id]/+page.svelte @@ -28,7 +28,6 @@ export let data; - let ndef; let showOptions; let reading = async ({ message, serialNumber }) => { @@ -41,6 +40,7 @@ $: refresh(data); let { invoice, id, subject, user, src } = data; let { + aid, amount, hash, type, @@ -56,6 +56,7 @@ let refresh = async (data) => { ({ invoice, id, user, src } = data); ({ + aid, amount, hash, type, @@ -66,21 +67,6 @@ } = invoice); tipPercent = (tip / amount) * 100; - - // if (browser && window.NDEFReader) { - // try { - // ndef = new NDEFReader(); - // await ndef.scan(); - // - // ndef.removeEventListener('readingerror', readingerror); - // ndef.removeEventListener('reading', reading); - // - // ndef.addEventListener('readingerror', readingerror); - // ndef.addEventListener('reading', reading); - // } catch (e) { - // console.log(e); - // } - // } }; $: amountFiat = parseFloat(((amount * rate) / sats).toFixed(2)); @@ -96,14 +82,7 @@ send("subscribe", invoice); }); - if ($amountPrompt && !tip) toggleAmount(); - } - }); - - onDestroy(() => { - if (ndef) { - ndef.removeEventListener("readingerror", readingerror); - ndef.removeEventListener("reading", reading); + if ($amountPrompt && tip === null) toggleAmount(); } }); @@ -114,7 +93,7 @@ let setType = async (type) => { invoice.type = type; - ({ id } = await post(`/${username}/invoice`, { + ({ id } = await post(`/invoice`, { invoice, user: { username, currency }, })); @@ -128,7 +107,7 @@ amount = newAmount; invoice.amount = newAmount; invoice.tip = 0; - ({ id } = await post(`/${username}/invoice`, { + ({ id } = await post(`/invoice`, { invoice, user: { username, currency }, })); @@ -157,14 +136,14 @@ > <img {src} - class="mx-auto z-10 shadow-inner rounded-2xl" + class="mx-auto z-10 max-w-[360px]" bind:this={qr} alt={txt} /> </a> </div> {:else} - <div class="break-all pb-5 text-lg text-center"> + <div class="break-all pb-5 text-center text-secondary text-xl"> {txt} </div> {/if} @@ -209,7 +188,7 @@ </div> {/if} - <div class="text-secondary space-y-2 text-xl"> + <div class="text-secondary space-y-2 text-xl pt-2"> <button type="button" class="flex gap-2 text-center break-all rounded-2xl hover:opacity-80 py-5 px-6 w-full mx-auto justify-center border whitespace-nowrap mb-2" @@ -257,72 +236,72 @@ </div> </div> - <div class="flex justify-around text-secondary"> - <button - class="hover:bg-primary my-auto flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 w-full justify-center" - class:bg-primary={type === types.bitcoin} - class:text-black={type === types.bitcoin} - on:click={() => setType(types.bitcoin)} - > - <img src="/images/bitcoin.svg" class="w-8" alt="Bitcoin" /> - <div class="my-auto text-lg">Bitcoin</div> - </button> + {#if aid === user.id} + <div class="grid grid-cols-2 justify-around text-secondary"> + <button + class="hover:bg-primary my-auto flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center" + class:bg-primary={type === types.bitcoin} + class:text-black={type === types.bitcoin} + on:click={() => setType(types.bitcoin)} + > + <img src="/images/bitcoin.svg" class="w-8" alt="Bitcoin" /> + <div class="my-auto text-lg">Bitcoin</div> + </button> - <button - class="hover:bg-primary my-auto flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 w-full justify-center" - class:bg-primary={type === types.liquid} - class:text-black={type === types.liquid} - on:click={() => setType(types.liquid)} - > - <img src="/images/liquid.svg" class="w-8" alt="Liquid" /> - <div class="my-auto text-lg">Liquid</div> - </button> + <button + class="hover:bg-primary my-auto flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center" + class:bg-primary={type === types.liquid} + class:text-black={type === types.liquid} + on:click={() => setType(types.liquid)} + > + <img src="/images/liquid.svg" class="w-8" alt="Liquid" /> + <div class="my-auto text-lg">Liquid</div> + </button> - <button - class="hover:bg-primary flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 w-full justify-center" - class:bg-primary={type === types.lightning} - class:text-black={type === types.lightning} - on:click={() => setType(types.lightning)} - > - <div class="bg-white rounded-full w-8 h-8 text-center flex"> - <div class="m-auto">⚡️</div> - </div> - <div class="my-auto text-lg">Lightning</div> - </button> - </div> + <button + class="hover:bg-primary flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center" + class:bg-primary={type === types.lightning} + class:text-black={type === types.lightning} + on:click={() => setType(types.lightning)} + > + <div class="bg-white rounded-full w-8 h-8 text-center flex"> + <div class="m-auto">⚡️</div> + </div> + <div class="my-auto text-lg">Lightning</div> + </button> - {#if type === types.liquid} - <div class="flex justify-center"> - <div class="my-auto text-xl text-center text-secondary"> - {$t("payments.onlyLbtc")} - <span class="text-teal-500 font-bold">L-BTC</span>, - {$t("payments.dontDeposit")} - </div> + {#if user?.id === invoice?.user?.id} + <a href={`/receive`} class="block contents"> + <button + class="hover:bg-primary flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center" + > + <img src="/images/cash.png" class="w-8 my-auto" /> + <div class="my-auto text-lg">Ecash</div> + </button> + </a> + {/if} </div> - {/if} - {#if user?.id === invoice?.user?.id} - <a href={`/receive`} class="block"> - <button - class="flex gap-2 text-center break-all rounded-2xl hover:opacity-80 py-5 px-6 w-full mx-auto justify-center border text-xl whitespace-nowrap text-secondary" - > - <Icon icon="voucher" style="w-8 my-auto" /> - <div class="my-auto"> - {$t("payments.redeemVoucher")} - </div></button - > - </a> + {#if type === types.liquid} + <div class="flex justify-center"> + <div class="my-auto text-xl text-center text-secondary"> + {$t("payments.onlyLbtc")} + <span class="text-teal-500 font-bold">L-BTC</span>, + {$t("payments.dontDeposit")} + </div> + </div> + {/if} {/if} </div> {#if settingAmount} <div - class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-20" + class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50" > <div - class="relative top-1/3 mx-auto p-12 border w-96 shadow-lg rounded-md bg-white space-y-5" + class="relative mx-auto p-12 border w-96 shadow-lg rounded-md bg-white space-y-5 top-10" > - <form submit={setAmount}> + <form on:submit|preventDefault={setAmount}> <Numpad bind:amount={newAmount} bind:currency @@ -332,6 +311,7 @@ /> <div class="w-full flex flex-wrap gap-2"> <button + bind:this={submit} type="submit" on:click={setAmount} class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto bg-black text-white w-full" @@ -339,7 +319,6 @@ <div class="my-auto">Ok</div> </button> <button - bind:this={submit} type="button" class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto w-full" on:click={toggleAmount} @@ -350,8 +329,8 @@ </div> </form> - <div class="flex justify-between items-center"> - <span class="font-bold">{$t("payments.amountPrompt")}</span> + <div class="flex justify-center gap-3"> + <div class="text-secondary">{$t("payments.amountPrompt")}</div> <Toggle id="notify" bind:value={$amountPrompt} /> </div> </div> diff --git a/src/routes/(app)/[username]/invoice/[id]/memo/+page.server.ts b/src/routes/(app)/invoice/[id]/memo/+page.server.ts index 58d56018..24dec1af 100644 --- a/src/routes/(app)/[username]/invoice/[id]/memo/+page.server.ts +++ b/src/routes/(app)/invoice/[id]/memo/+page.server.ts @@ -4,7 +4,7 @@ import { redirect } from "@sveltejs/kit"; export async function load({ parent, params }) { let { invoice } = await parent(); if (!invoice.amount) { - redirect(307, `/${params.username}/invoice/${invoice.id}`); + redirect(307, `/invoice/${invoice.id}`); } } diff --git a/src/routes/(app)/[username]/invoice/[id]/memo/+page.svelte b/src/routes/(app)/invoice/[id]/memo/+page.svelte index 4d23ff19..4d23ff19 100644 --- a/src/routes/(app)/[username]/invoice/[id]/memo/+page.svelte +++ b/src/routes/(app)/invoice/[id]/memo/+page.svelte diff --git a/src/routes/(app)/[username]/invoice/[id]/paid/+page.svelte b/src/routes/(app)/invoice/[id]/paid/+page.svelte index 700ac972..e4279e48 100644 --- a/src/routes/(app)/[username]/invoice/[id]/paid/+page.svelte +++ b/src/routes/(app)/invoice/[id]/paid/+page.svelte @@ -76,7 +76,7 @@ {/if} </div> -<a href={`/${user.username}/receive`}> +<a href={`/${user.username}`}> <div class="opacity-0 w-screen h-screen fixed top-0 left-0 z-50" /> </a> diff --git a/src/routes/(app)/[username]/invoice/[id]/tip/+page.server.ts b/src/routes/(app)/invoice/[id]/tip/+page.server.ts index b81e2364..48ebf970 100644 --- a/src/routes/(app)/[username]/invoice/[id]/tip/+page.server.ts +++ b/src/routes/(app)/invoice/[id]/tip/+page.server.ts @@ -1,10 +1,10 @@ import invoice from "$lib/invoice"; import { redirect } from "@sveltejs/kit"; -export async function load({ parent, params }) { +export async function load({ parent }) { let { invoice } = await parent(); if (!invoice.amount) - redirect(307, `/${params.username}/invoice/${invoice.id}`); + redirect(307, `/invoice/${invoice.id}`); } export const actions = { diff --git a/src/routes/(app)/[username]/invoice/[id]/tip/+page.svelte b/src/routes/(app)/invoice/[id]/tip/+page.svelte index 35b24bf1..da2d041e 100644 --- a/src/routes/(app)/[username]/invoice/[id]/tip/+page.svelte +++ b/src/routes/(app)/invoice/[id]/tip/+page.svelte @@ -59,6 +59,7 @@ $: refresh(data); let { invoice, id, user, rates } = data; let { + aid, amount, hash, items, @@ -81,6 +82,7 @@ ({ invoice, id } = data); ({ + aid, amount, hash, items, @@ -111,6 +113,7 @@ <div class="container px-4 max-w-lg text-center mx-auto"> <form method="POST" use:enhance class:invisible={submitting}> + <input type="hidden" name="aid" value={aid} /> <input type="hidden" name="amount" value={amount} /> <input type="hidden" name="tip" value={tip} /> <input type="hidden" name="username" value={username} /> diff --git a/src/routes/(app)/[username]/invoice/sent/+page.svelte b/src/routes/(app)/invoice/sent/+page.svelte index b95bc85e..b95bc85e 100644 --- a/src/routes/(app)/[username]/invoice/sent/+page.svelte +++ b/src/routes/(app)/invoice/sent/+page.svelte diff --git a/src/routes/(app)/[username]/items/[id]/+page.server.ts b/src/routes/(app)/items/[id]/+page.server.ts index 67cebd93..67cebd93 100644 --- a/src/routes/(app)/[username]/items/[id]/+page.server.ts +++ b/src/routes/(app)/items/[id]/+page.server.ts diff --git a/src/routes/(app)/[username]/items/[id]/+page.svelte b/src/routes/(app)/items/[id]/+page.svelte index 65720350..65720350 100644 --- a/src/routes/(app)/[username]/items/[id]/+page.svelte +++ b/src/routes/(app)/items/[id]/+page.svelte diff --git a/src/routes/(app)/[username]/items/create/+page.server.ts b/src/routes/(app)/items/create/+page.server.ts index 2e006a3e..2e006a3e 100644 --- a/src/routes/(app)/[username]/items/create/+page.server.ts +++ b/src/routes/(app)/items/create/+page.server.ts diff --git a/src/routes/(app)/[username]/items/create/+page.svelte b/src/routes/(app)/items/create/+page.svelte index b82889c9..b82889c9 100644 --- a/src/routes/(app)/[username]/items/create/+page.svelte +++ b/src/routes/(app)/items/create/+page.svelte diff --git a/src/routes/(app)/ln/[lnurl]/+page.server.ts b/src/routes/(app)/ln/[lnurl]/+page.server.ts index d1b1b86e..c078ae8e 100644 --- a/src/routes/(app)/ln/[lnurl]/+page.server.ts +++ b/src/routes/(app)/ln/[lnurl]/+page.server.ts @@ -70,6 +70,6 @@ export const actions = { let c = callback.includes("?") ? "&" : "?"; await fetch(`${callback}${c}k1=${k1}&pr=${pr}`).then((r) => r.json()); - redirect(307, `/${username}/payments`); + redirect(307, `/payments`); }, }; diff --git a/src/routes/(app)/ln/withdrawal/sent/+page.svelte b/src/routes/(app)/ln/withdrawal/sent/+page.svelte index c1a2bdb2..90814960 100644 --- a/src/routes/(app)/ln/withdrawal/sent/+page.svelte +++ b/src/routes/(app)/ln/withdrawal/sent/+page.svelte @@ -19,7 +19,7 @@ <h1 class="text-3xl md:text-4xl font-bold mb-6">Request Sent!</h1> </div> -<a href={`/${user.username}/payments`}> +<a href={`/payments`}> <div class="opacity-0 w-screen h-screen fixed top-0 left-0 z-50" /> </a> diff --git a/src/routes/(app)/login/+page.svelte b/src/routes/(app)/login/+page.svelte deleted file mode 100644 index 61ea23b7..00000000 --- a/src/routes/(app)/login/+page.svelte +++ /dev/null @@ -1,188 +0,0 @@ -<script> - import { onMount } from "svelte"; - import { applyAction, deserialize } from "$app/forms"; - import { tick } from "svelte"; - import { fly } from "svelte/transition"; - import { enhance } from "$app/forms"; - import Pin from "$comp/Pin.svelte"; - import Icon from "$comp/Icon.svelte"; - import Spinner from "$comp/Spinner.svelte"; - import { focus, fail } from "$lib/utils"; - import { password, pin, loginRedirect } from "$lib/store"; - import { t } from "$lib/translations"; - import { page } from "$app/stores"; - import { generate } from "$lib/nostr"; - import { invalidateAll } from "$app/navigation"; - - onMount(() => { - $pin = undefined; - localStorage.clear(); - sessionStorage.clear(); - - let lang = $page.url.searchParams.get("lang"); - if (lang) document.cookie = `lang=${lang} ;`; - }); - - export let form; - - let token, formElement; - let code = []; - let redirect; - - $password = undefined; - - $: need2fa = form?.message === "2fa"; - let cancel = () => (need2fa = false); - $: if (form?.message === "2fa" && form.token === token) token = ""; - - let username, email, btn; - - $: update(form); - let update = (form) => form && ({ username, password: $password } = form); - - $: token && token?.length === 6 && tick().then(() => btn.click()); - - let revealPassword = false; - - let loading; - async function handleSubmit(e) { - loading = true; - - let data = new FormData(this); - let user = Object.fromEntries(data); - await generate(user); - - for (let k in user) { - data.set(k, user[k]); - } - - const response = await fetch("/login", { - method: "POST", - body: data, - }); - - const result = deserialize(await response.text()); - - if (result.type === "success") { - await invalidateAll(); - } - - applyAction(result); - loading = false; - } -</script> - -{#if need2fa} - <Pin bind:value={token} title="Enter 2FA Code" {cancel} notify={false} /> -{/if} - -<div class="pt-10"> - <div class="w-[243px] mx-auto mb-10"> - <a href="/"> - <Icon icon="logo" /> - </a> - </div> - - <div class="flex justify-center items-center"> - <div - class="shadow-xl rounded-3xl p-10 pb-12 space-y-5 w-full mx-5 md:mx-0 md:w-[400px]" - > - <h1 class="text-2xl font-bold text-center">{$t("login.signIn")}</h1> - - {#if form?.error} - <div class="text-red-600 text-center" in:fly> - {form.error} - </div> - {/if} - - <form - class="space-y-5" - on:submit|preventDefault={handleSubmit} - method="POST" - > - <input - type="hidden" - name="loginRedirect" - value={$loginRedirect || $page.url.searchParams.get("redirect")} - /> - <input type="hidden" name="token" value={token} /> - - <div> - <label for="username" class="font-semibold" - >{$t("login.username")}</label - > - <input - name="username" - type="text" - required - class="bg-primary" - bind:value={username} - use:focus - autocapitalize="none" - /> - </div> - - <div class="relative"> - <label for="password" class="block font-semibold" - >{$t("login.password")}</label - > - {#if revealPassword} - <input - name="password" - type="text" - required - class="bg-primary" - bind:value={$password} - autocapitalize="none" - /> - {:else} - <input - name="password" - type="password" - required - class="bg-primary" - bind:value={$password} - autocapitalize="none" - /> - {/if} - <button - type="button" - on:click={() => (revealPassword = !revealPassword)} - class="absolute right-5 top-10" - > - <Icon icon={revealPassword ? "eye" : "eye-off"} /> - </button> - </div> - - <div class="flex justify-end items-center"> - <a href="/forgot" class="underline underline-offset-4 text-black" - >{$t("login.forgotUserOrPassword")}</a - > - </div> - - <button - type="submit" - class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80" - disabled={loading} - bind:this={btn} - > - {#if loading} - <Spinner /> - {:else} - {$t("login.signIn")} - {/if} - </button> - </form> - - <p class="text-secondary text-center font-medium"> - {$t("login.noAccount")} - <a - href={"/register"} - class="block md:inline text-black underline underline-offset-4 hover:opacity-80" - > - {$t("login.register")} - </a> - </p> - </div> - </div> -</div> diff --git a/src/routes/(app)/logout/+page.server.ts b/src/routes/(app)/logout/+page.server.ts index ab5df7b9..374f244a 100644 --- a/src/routes/(app)/logout/+page.server.ts +++ b/src/routes/(app)/logout/+page.server.ts @@ -7,6 +7,7 @@ const opts = { export async function load({ cookies }) { let lang = cookies.get("lang"); + cookies.set("aid", "", opts); cookies.set("lang", "", opts); cookies.set("username", "", opts); cookies.set("token", "", opts); diff --git a/src/routes/(app)/[username]/messages/+page.server.ts b/src/routes/(app)/messages/+page.server.ts index 94659621..94659621 100644 --- a/src/routes/(app)/[username]/messages/+page.server.ts +++ b/src/routes/(app)/messages/+page.server.ts diff --git a/src/routes/(app)/[username]/messages/+page.svelte b/src/routes/(app)/messages/+page.svelte index 4738ea2c..547a1f21 100644 --- a/src/routes/(app)/[username]/messages/+page.svelte +++ b/src/routes/(app)/messages/+page.svelte @@ -71,7 +71,7 @@ <div class="relative"> {#each latest as { content, pubkey, author, recipient }} {@const user = author.id === user.id ? recipient : author} - <a href={`/${subject.username}/messages/${user.username}`}> + <a href={`/messages/${user.username}`}> <div class="flex hover:bg-gray-100 p-4 rounded-2xl"> <div class="my-auto"> <Avatar {user} size={"20"} disabled={true} /> diff --git a/src/routes/(app)/[username]/messages/[recipient]/+page.server.ts b/src/routes/(app)/messages/[recipient]/+page.server.ts index 6be9f6ec..6be9f6ec 100644 --- a/src/routes/(app)/[username]/messages/[recipient]/+page.server.ts +++ b/src/routes/(app)/messages/[recipient]/+page.server.ts diff --git a/src/routes/(app)/[username]/messages/[recipient]/+page.svelte b/src/routes/(app)/messages/[recipient]/+page.svelte index 98638389..98638389 100644 --- a/src/routes/(app)/[username]/messages/[recipient]/+page.svelte +++ b/src/routes/(app)/messages/[recipient]/+page.svelte diff --git a/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts b/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts index e9a0ec9e..cf8143f0 100644 --- a/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts +++ b/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts @@ -29,7 +29,7 @@ export async function load({ cookies, params, parent }) { auth(cookies) ); - redirect(307, `/${username}/invoice/${id}`); + redirect(307, `/invoice/${id}`); } return { amount, rate, subject }; @@ -37,14 +37,15 @@ export async function load({ cookies, params, parent }) { export const actions = { default: async ({ cookies, request }) => { + let p; try { let body = await fd(request); - await post("/payments", body, auth(cookies)); + p = await post("/payments", body, auth(cookies)); } catch (e: any) { console.log(e); return fail(400, { message: e.message }); } - redirect(307, "/sent"); + redirect(307, `/sent/${p.id}`); }, }; diff --git a/src/routes/(app)/pay/[username]/[...amount]/+page.svelte b/src/routes/(app)/pay/[username]/[...amount]/+page.svelte index 1652952a..8e3abab4 100644 --- a/src/routes/(app)/pay/[username]/[...amount]/+page.svelte +++ b/src/routes/(app)/pay/[username]/[...amount]/+page.svelte @@ -3,7 +3,6 @@ import { goto } from "$app/navigation"; import { pin } from "$lib/store"; import { enhance } from "$app/forms"; - import AppHeader from "$comp/AppHeader.svelte"; import Avatar from "$comp/Avatar.svelte"; import Icon from "$comp/Icon.svelte"; import Numpad from "$comp/Numpad.svelte"; @@ -33,7 +32,7 @@ rate = fiat ? (sats * amountFiat) / amount : r; let id; - ({ id, hash } = await post(`/${subject.username}/invoice`, { + ({ id, hash } = await post(`/invoice`, { invoice: { amount, rate: rates[subject.currency], @@ -56,15 +55,13 @@ }; </script> -<AppHeader {data} /> - {#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-8"> +<div class="container px-4 mt-20 max-w-xl mx-auto space-y-2"> <Numpad bind:amount={a} bind:amountFiat={af} diff --git a/src/routes/(app)/payment/[id]/+page.svelte b/src/routes/(app)/payment/[id]/+page.svelte index ea07be14..f7a36717 100644 --- a/src/routes/(app)/payment/[id]/+page.svelte +++ b/src/routes/(app)/payment/[id]/+page.svelte @@ -3,7 +3,6 @@ import { onMount, tick } from "svelte"; import { t, loading } from "$lib/translations"; import { - back, copy, f, s, @@ -34,6 +33,7 @@ rate, type, ref, + path, tip, ourfee, fee, @@ -57,6 +57,7 @@ rate, type, ref, + path, tip, ourfee, fee, @@ -96,12 +97,6 @@ }); </script> -<a href={`/${username}/payments`}> - <button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80"> - <Icon icon="arrow-left" style="w-10" /> - </button> -</a> - <div class="container mx-auto max-w-lg px-4 space-y-8 break-all text-2xl"> <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold mb-10"> {$t(amount < 0 ? "payments.sent" : "payments.received")} @@ -225,40 +220,18 @@ </div> {/if} - <div class="flex gap-2"> - {#if user.admin} - <button - class="rounded-full border py-3 px-6 hover:opacity-80 flex w-full md:w-60" - on:click={bump} - > - <div class="mx-auto flex"> - <Icon icon="clock" style="my-auto h-6 mr-2" /> - <div class="my-auto mt-1 text-base">{$t("payments.bump")}</div> - </div> - </button> - {/if} - - {#if user.hasprinter} - <button - class="rounded-full border py-3 px-6 hover:opacity-80 flex w-full md:w-60" - 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> - </div> - </button> - {:else} - <a href={`/payment/${id}/plain`}> + {#if path} + <div> + <span class="text-lg text-secondary">Path</span> + <div class="flex"> + <div>{path}</div> <button - class="rounded-full border py-3 px-6 hover:opacity-80 flex w-full md:w-60" + class="flex font-bold hover:opacity-80 mb-auto my-auto" + on:click={() => copy(path)} > - <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> - </div> + <Icon icon="copy" style="ml-2 w-12 my-auto" /> </button> - </a> - {/if} - </div> + </div> + </div> + {/if} </div> diff --git a/src/routes/(app)/[username]/payments/[...page]/+page.server.ts b/src/routes/(app)/payments/[...page]/+page.server.ts index 070349f8..f1818955 100644 --- a/src/routes/(app)/[username]/payments/[...page]/+page.server.ts +++ b/src/routes/(app)/payments/[...page]/+page.server.ts @@ -5,6 +5,7 @@ let limit = 25; export async function load({ cookies, depends, params }) { depends("app:payments"); + let aid = cookies.get("aid"); let parts = params.page.split("/"); let start, end, page; @@ -16,7 +17,9 @@ export async function load({ cookies, depends, params }) { let offset = (page - 1) * limit; if (!start) start = Date.now() / 1000 - 24 * 60 * 60; - let url = `/payments?start=${start * 1000}&limit=${limit}&offset=${offset}`; + let url = `/payments?aid=${aid}&start=${ + start * 1000 + }&limit=${limit}&offset=${offset}`; if (end) url += `&end=${end * 1000}`; let { count, payments, incoming, outgoing } = await get(url, auth(cookies)); diff --git a/src/routes/(app)/[username]/payments/[...page]/+page.svelte b/src/routes/(app)/payments/[...page]/+page.svelte index 1efdc2ec..bfd30f55 100644 --- a/src/routes/(app)/[username]/payments/[...page]/+page.svelte +++ b/src/routes/(app)/payments/[...page]/+page.svelte @@ -65,7 +65,7 @@ : $page.url.pathname; let csv = async () => { - let url = `/${user.username}/payments`; + let url = `/payments`; if (start) url += `/${start}`; if (end) url += `/${end}`; @@ -78,7 +78,9 @@ platform_fee: p.ourfee, amount_fiat: f((p.amount * p.rate) / sats, p.currency), fee_fiat: p.fee ? f((p.fee * p.rate) / sats, p.currency) : null, - platform_fee_fiat: p.ourfee ? f((p.ourfee * p.rate) / sats, p.currency) : null, + platform_fee_fiat: p.ourfee + ? f((p.ourfee * p.rate) / sats, p.currency) + : null, tip_fiat: p.tip ? f((p.tip * p.rate) / sats, p.currency) : null, total_fiat: f(((p.amount + (p.tip || 0)) * p.rate) / sats, p.currency), })); @@ -127,30 +129,28 @@ }; </script> -<div class="mt-24 mb-20 payments"> - <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold mb-10"> +<div class="mt-24 mb-20 payments space-y-5"> + <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> {$t("payments.header")} </h1> - <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5"> - <div class="flex text-md text-secondary relative"> - <div class="mx-auto flex justify-center w-full gap-1"> - {#each presets as { start, end, title }, i} - <a - class:active={selection === i} - href={`/${user.username}/payments/${getUnixTime(start) + "/"}1`} + <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-2"> + <div class="mx-auto flex justify-center w-full gap-1"> + {#each presets as { start, end, title }, i} + <a + class:active={selection === i} + href={`/payments/${getUnixTime(start) + "/"}1`} + > + <button + class="text-sm md:text-lg rounded-full border py-2 px-4 hover:opacity-80 min-w-[72px]" > - <button - class="text-sm md:text-lg rounded-full border py-2 px-4 hover:opacity-80 min-w-[72px]" - > - <div class="my-auto">{title}</div> - </button> - </a> - {/each} - </div> + <div class="my-auto">{title}</div> + </button> + </a> + {/each} </div> - <div class="flex flex-wrap justify-center mb-8"> + <div class="flex flex-wrap justify-center"> {#if pages.length > 1} {#each pages as _, i} <a @@ -213,6 +213,8 @@ <div class="text-secondary flex"> {#if [types.ecash, types.lightning].includes(p.type)} <div class="text-3xl">⚡️</div> + {:else if p.type === types.reconcile} + <Icon icon="balance" style="w-6 mr-2" /> {:else if p.type === types.bitcoin} <div class="my-auto mr-1"> <img @@ -259,16 +261,14 @@ <div class="grid grid-cols-3 w-full text-center text-lg"> {#each Object.keys(incoming) as c} - <span class="text-base text-secondary text-left"></span> + <span class="text-base text-secondary text-left" /> <!-- <span class="text-base text-secondary" --> <!-- >{$t("payments.subtotal")}</span --> <!-- > --> <span class="text-base text-secondary"> {#if tipsIn || tipsOut}{$t("payments.tips")}{/if} </span> - <span class="text-base text-secondary" - >{$t("payments.total")}</span - > + <span class="text-base text-secondary">{$t("payments.total")}</span> {@const totalIn = incoming[c]?.fiat || 0} {@const tipsIn = incoming[c]?.fiatTips || 0} @@ -293,15 +293,14 @@ <span><b>{totalOut ? f(totalOut, c) : "-"}</b></span> {/each} </div> - - <button - class="ml-auto rounded-full border py-2 px-4 w-36 hover:opacity-80 flex mx-auto" - on:click={csv} - > - <Icon icon="save" style="opacity-50 mr-2 my-auto" /> - <div class="my-auto">{$t("payments.export")}</div> - </button> </div> + <button + class="ml-auto rounded-2xl border py-5 px-6 hover:opacity-80 flex mx-auto text-xl gap-2" + on:click={csv} + > + <Icon icon="save" style="my-auto" /> + <div class="my-auto">{$t("payments.export")}</div> + </button> </div> <style> diff --git a/src/routes/(app)/[username]/payments/[...page]/+server.ts b/src/routes/(app)/payments/[...page]/+server.ts index a81faaf3..a81faaf3 100644 --- a/src/routes/(app)/[username]/payments/[...page]/+server.ts +++ b/src/routes/(app)/payments/[...page]/+server.ts diff --git a/src/routes/(app)/qr/[text]/+page.svelte b/src/routes/(app)/qr/[text]/+page.svelte index c9562582..5469c084 100644 --- a/src/routes/(app)/qr/[text]/+page.svelte +++ b/src/routes/(app)/qr/[text]/+page.svelte @@ -6,26 +6,17 @@ let { src, text } = data; </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-4xl mx-auto space-y-5"> - <div class="flex justify-center items-center"> - <div - class="md:shadow-xl rounded-3xl md:px-10 pt-5 pb-10 space-y-5 w-full md:mx-5" - > - <Qr {src} /> + <Qr {src} /> - <button - type="button" - class="flex gap-2 text-center break-all rounded-2xl hover:opacity-80 py-5 px-6 w-full mx-auto justify-center border text-xl hover:bg-slate-100" - on:click={() => copy(text)} - > - <div class="my-auto"> - {text} - </div> - </button> - </div> - </div> + <button + type="button" + class="flex gap-2 text-center break-all rounded-2xl hover:opacity-80 py-5 px-6 w-full mx-auto justify-center border text-xl hover:bg-slate-100" + on:click={() => copy(text)} + > + <Icon icon="copy" style="w-8" /> + <div class="my-auto"> + {text} + </div> + </button> </div> diff --git a/src/routes/(app)/qr/create/+page.svelte b/src/routes/(app)/qr/create/+page.svelte new file mode 100644 index 00000000..fd99a54b --- /dev/null +++ b/src/routes/(app)/qr/create/+page.svelte @@ -0,0 +1,60 @@ +<script> + import { goto } from "$app/navigation"; + import { browser } from "$app/environment"; + import { t } from "$lib/translations"; + import Icon from "$comp/Icon.svelte"; + let el, text, pasted; + + let keypress = (e) => + e.key === "Enter" ? e.preventDefault() || el.click() : (pasted = false); + + let paste = async () => { + text = await navigator.clipboard.readText(); + pasted = true; + }; + + let submit = () => goto(`/qr/${encodeURIComponent(text)}`); + + $: if (browser && pasted && text) el.click() && (pasted = false); +</script> + +<div class="container px-4 max-w-lg mx-auto space-y-5 mt-20"> + <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> + Create QR + </h1> + + <form on:submit|preventDefault={submit} class="space-y-2 text-xl"> + <div class="mb-2"> + <textarea + use:focus + name="text" + placeholder="Enter some text or a URL to put in a QR" + on:keypress={keypress} + class="w-full p-4 border rounded-xl h-32 text-xl" + bind:value={text} + on:paste={() => (pasted = true)} + autocapitalize="none" + /> + </div> + + <div class="flex gap-2"> + <button + type="button" + class="flex border rounded-2xl px-6 py-5 font-bold hover:opacity-80 w-full bg-primary justify-center gap-2" + on:click={paste} + > + <Icon icon="paste" style="w-8 my-auto" /> + <div class="my-auto">{$t("user.send.paste")}</div> + </button> + + <button + bind:this={el} + type="submit" + class="flex bg-black text-white border rounded-2xl px-6 py-5 w-full font-bold gap-2 justify-center" + > + <Icon icon="send" style="w-8 my-auto invert" /> + <div class="my-auto">{$t("user.dashboard.go")}</div> + </button> + </div> + </form> +</div> diff --git a/src/routes/(app)/receive/[...token]/+page.svelte b/src/routes/(app)/receive/[...token]/+page.svelte index 2fd4b879..988c1e6c 100644 --- a/src/routes/(app)/receive/[...token]/+page.svelte +++ b/src/routes/(app)/receive/[...token]/+page.svelte @@ -5,7 +5,6 @@ import { tick } from "svelte"; import { browser } from "$app/environment"; import { t } from "$lib/translations"; - import AppHeader from "$comp/AppHeader.svelte"; import Avatar from "$comp/Avatar.svelte"; import Icon from "$comp/Icon.svelte"; import Spinner from "$comp/Spinner.svelte"; @@ -30,10 +29,9 @@ <svelte:window bind:innerWidth={w} /> -<AppHeader {data} /> <div class="container px-4 max-w-lg mx-auto space-y-5 mt-20"> <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> - {$t("payments.voucher")} + {$t("user.dashboard.receive")} </h1> <form method="POST" use:enhance class="space-y-2 text-xl"> @@ -53,7 +51,7 @@ name="text" placeholder={$t("payments.voucherText")} on:keypress={keypress} - class="w-full p-4 border rounded-xl h-48 text-xl" + class="w-full p-4 border rounded-xl h-32 text-xl" bind:value={text} on:paste={() => (pasted = true)} autocapitalize="none" @@ -64,19 +62,19 @@ <a href="/scan" class="w-full"> <button type="button" - class="flex border rounded-2xl px-6 py-5 font-bold hover:opacity-80 w-full bg-primary justify-center" + class="flex border rounded-2xl px-5 py-4 hover:opacity-80 w-full justify-center gap-1" > - <Icon icon="camera" style="mr-2 w-6 my-auto" /> + <Icon icon="camera" style="w-8 my-auto" /> <div class="my-auto">{$t("user.send.scan")}</div> </button> </a> <button type="button" - class="flex border rounded-2xl px-6 py-5 font-bold hover:opacity-80 w-full bg-primary justify-center" + class="flex border rounded-2xl px-5 py-4 hover:opacity-80 w-full justify-center gap-1" on:click={paste} > - <Icon icon="paste" style="mr-2 w-6 my-auto" /> + <Icon icon="paste" style="w-8 my-auto" /> <div class="my-auto">{$t("user.send.paste")}</div> </button> </div> diff --git a/src/routes/(app)/register/+page.svelte b/src/routes/(app)/register/+page.svelte deleted file mode 100644 index 0a761228..00000000 --- a/src/routes/(app)/register/+page.svelte +++ /dev/null @@ -1,314 +0,0 @@ -<script> - import punks from "$lib/punks"; - import { upload } from "$lib/upload"; - import { afterNavigate, invalidateAll } from "$app/navigation"; - import { applyAction, deserialize } from "$app/forms"; - import { tick } from "svelte"; - import { fly } from "svelte/transition"; - import { enhance } from "$app/forms"; - - import Pin from "$comp/Pin.svelte"; - import Icon from "$comp/Icon.svelte"; - import Spinner from "$comp/Spinner.svelte"; - - import { focus, fail } from "$lib/utils"; - import { avatar, password, loginRedirect } from "$lib/store"; - import { t } from "$lib/translations"; - import { page } from "$app/stores"; - import { generate } from "$lib/nostr"; - import { - NumberDictionary, - uniqueNamesGenerator, - colors, - adjectives, - animals, - } from "unique-names-generator"; - - export let form; - export let data; - - $: data && ($avatar = undefined); - let username; - let { index } = data; - - let cleared; - let clear = () => { - if (!cleared) { - cleared = true; - username = ""; - $password = ""; - revealPassword = false; - } - }; - - let refresh = async () => { - username = uniqueNamesGenerator({ - dictionaries: [animals, NumberDictionary.generate({ min: 10, max: 99 })], - length: 2, - separator: "", - }); - - $password = uniqueNamesGenerator({ - dictionaries: [colors, NumberDictionary.generate({ min: 100, max: 999 })], - length: 2, - separator: "", - }); - - revealPassword = true; - }; - - afterNavigate(async () => { - try { - await invalidateAll(); - } catch (e) { - console.log(e); - } - }); - - let token, formElement; - let code = []; - let redirect; - - $: need2fa = form?.message === "2fa"; - let cancel = () => (need2fa = false); - $: if (form?.message === "2fa" && form.token === token) token = ""; - - let email, btn; - - $: update(form); - let update = (form) => form && ({ username, password: $password } = form); - - $: token && token?.length === 6 && tick().then(() => btn.click()); - - let revealPassword; - - let loading; - async function handleSubmit(e) { - loading = true; - - let data = new FormData(this); - let user = Object.fromEntries(data); - await generate(user); - - for (let k in user) { - data.set(k, user[k]); - } - - data.set("profile", punks[index]); - if ($avatar) { - try { - let { hash } = JSON.parse( - await upload($avatar.file, $avatar.type, $avatar.progress, token) - ); - - data.set("profile", hash); - await fetch(`/api/public/${hash}.webp`, { - cache: "reload", - mode: "no-cors", - }); - } catch (e) { - console.log("problem uploading avatar", e); - } - } - - const response = await fetch("/register", { - method: "POST", - body: data, - }); - - const result = deserialize(await response.text()); - - if (result.type === "success") { - await invalidateAll(); - } - - applyAction(result); - loading = false; - } - - let avatarInput; - $: src = `/api/public/${punks[index]}.webp`; - let decr = () => (index = index <= 0 ? 63 : index - 1); - let incr = () => (index = index >= 63 ? 0 : index + 1); - let selectAvatar = () => avatarInput.click(); - - let progress; - let handleFile = async ({ target }) => { - let type = "profile"; - let file = target.files[0]; - if (!file) return; - - if (file.size > 10000000) form.error = "File too large"; - $avatar = { file, type, progress }; - - var reader = new FileReader(); - reader.onload = async (e) => { - src = e.target.result; - }; - - reader.readAsDataURL(file); - }; -</script> - -{#if need2fa} - <Pin bind:value={token} title="Enter 2FA Code" {cancel} notify={false} /> -{/if} - -<div class="pt-4"> - <div class="w-[243px] mx-auto"> - <a href="/"> - <Icon icon="logo" /> - </a> - </div> - - <div class="flex justify-center items-center"> - <div - class="shadow-xl rounded-3xl p-4 pt-0 space-y-5 w-full mx-5 md:mx-0 md:w-[400px]" - > - <input - type="file" - class="hidden" - bind:this={avatarInput} - on:change={(e) => handleFile(e, "profile")} - /> - - <div class="relative"> - <button - class="absolute w-8 h-12 left-12 bg-white rounded top-12" - on:click={decr} - > - <Icon icon="chevron-left" style="w-8 " /> - </button> - <button - class="block relative w-32 mx-auto" - on:click={selectAvatar} - > - <button - class="w-32 h-32 rounded-full border-4 border-white overflow-hidden flex mx-auto relative" - > - <img - {src} - class="w-full h-full object-cover object-center overflow-hidden" - alt={username} - /> - </button> - <button - class="absolute bg-white rounded-full p-2 mx-auto right-0 bottom-0 z-10 bg-opacity-80" - > - <Icon icon="upload" style="w-8" /> - </button> - </button> - <button - class="absolute w-8 h-12 right-12 bg-white rounded top-12" - on:click={incr} - > - <Icon icon="chevron-left" style="w-8 rotate-180" /> - </button> - </div> - - {#if form?.error} - <div class="text-red-600 text-center" in:fly> - {form.error} - </div> - {/if} - - <form - class="space-y-5" - on:submit|preventDefault={handleSubmit} - method="POST" - > - <input - type="hidden" - name="loginRedirect" - value={$loginRedirect || $page.url.searchParams.get("redirect")} - /> - <input type="hidden" name="token" value={token} /> - - <div class="relative"> - <label for="username" class="font-semibold" - >{$t("login.username")}</label - > - <input - use:focus - name="username" - type="text" - required - class="bg-primary" - bind:value={username} - on:focus={clear} - autocapitalize="none" - /> - <button - tabindex="-1" - type="button" - on:click={refresh} - class="absolute right-0 top-4" - > - <Icon icon="random" style="w-10 m-4" /> - </button> - </div> - - <div class="relative"> - <label for="password" class="block font-semibold" - >{$t("login.password")}</label - > - {#if revealPassword} - <input - name="password" - type="text" - required - class="bg-primary" - bind:value={$password} - autocapitalize="none" - /> - {:else} - <input - name="password" - type="password" - required - class="bg-primary" - bind:value={$password} - autocapitalize="none" - on:focus={clear} - /> - {/if} - <button - type="button" - tabindex="-1" - on:click={() => (revealPassword = !revealPassword)} - class="absolute right-0 top-4" - > - <Icon icon={revealPassword ? "eye" : "eye-off"} style="m-6" /> - </button> - </div> - - <p class="text-secondary text-sm"> - {$t("login.passwordRecommendation")} - </p> - - <button - type="submit" - class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80" - disabled={loading} - bind:this={btn} - > - {#if loading} - <Spinner /> - {:else} - {$t("login.register")} - {/if} - </button> - </form> - - <p class="text-secondary text-center font-medium"> - {$t("login.haveAccount")} - <a - href={"/login"} - class="block md:inline text-black underline underline-offset-4 hover:opacity-80" - > - {$t("login.signIn")} - </a> - </p> - </div> - </div> -</div> diff --git a/src/routes/(app)/scan/+page.svelte b/src/routes/(app)/scan/+page.svelte index e23b9bb8..150a66f4 100644 --- a/src/routes/(app)/scan/+page.svelte +++ b/src/routes/(app)/scan/+page.svelte @@ -21,10 +21,6 @@ onDestroy(() => scanner?.stop()); </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="flex w-full mb-4 px-4"> <div class="bg-black mx-auto rounded-3xl"> <video diff --git a/src/routes/(app)/send/[...text]/+page.svelte b/src/routes/(app)/send/[...text]/+page.svelte index 780f70c1..faa4a766 100644 --- a/src/routes/(app)/send/[...text]/+page.svelte +++ b/src/routes/(app)/send/[...text]/+page.svelte @@ -5,7 +5,6 @@ import { tick } from "svelte"; import { browser } from "$app/environment"; import { t } from "$lib/translations"; - import AppHeader from "$comp/AppHeader.svelte"; import Avatar from "$comp/Avatar.svelte"; import Icon from "$comp/Icon.svelte"; import Spinner from "$comp/Spinner.svelte"; @@ -19,7 +18,7 @@ let el, text, pasted, w; - let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click()); + let keypress = (e) => e.key === "Enter" ? (e.preventDefault() || el.click()) : (pasted = false); let paste = async () => { text = await navigator.clipboard.readText(); @@ -31,7 +30,6 @@ <svelte:window bind:innerWidth={w} /> -<AppHeader {data} /> <div class="container px-4 max-w-lg mx-auto space-y-5 mt-20"> <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> {$t("payments.send")} @@ -54,7 +52,7 @@ name="text" placeholder={$t("user.send.placeholder")} on:keypress={keypress} - class="w-full p-4 border rounded-xl h-48 text-xl" + class="w-full p-4 border rounded-xl h-32 text-xl" bind:value={text} on:paste={() => (pasted = true)} autocapitalize="none" @@ -65,7 +63,7 @@ <a href="/scan" class="w-full"> <button type="button" - class="flex border rounded-2xl px-6 py-5 font-bold hover:opacity-80 w-full bg-primary justify-center gap-2" + class="flex border rounded-2xl px-5 py-4 hover:opacity-80 w-full justify-center gap-2" > <Icon icon="camera" style="w-8 my-auto" /> <div class="my-auto">{$t("user.send.scan")}</div> @@ -74,7 +72,7 @@ <button type="button" - class="flex border rounded-2xl px-6 py-5 font-bold hover:opacity-80 w-full bg-primary justify-center gap-2" + class="flex border rounded-2xl px-5 py-4 hover:opacity-80 w-full justify-center gap-2" on:click={paste} > <Icon icon="paste" style="w-8 my-auto" /> @@ -97,7 +95,7 @@ type="button" class="flex border rounded-2xl px-6 py-5 w-full font-bold gap-2 justify-center text-xl bg-primary" > - <Icon icon="voucher" style="w-8 my-auto" /> + <img src="/images/cash.png" class="w-8" /> <div class="my-auto">{$t("payments.createEcash")}</div> </button> </a> diff --git a/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts b/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts new file mode 100644 index 00000000..a6edf8b0 --- /dev/null +++ b/src/routes/(app)/send/bitcoin/[address]/+layout.server.ts @@ -0,0 +1,23 @@ +import { auth, get } from "$lib/utils"; +import { redirect } from "@sveltejs/kit"; + +export async function load({ cookies, params, parent }) { + let { user } = await parent(); + let aid = cookies.get("aid"); + let { address } = params; + let { balance } = await get(`/account/${aid}`, auth(cookies)); + + try { + let invoice = await get(`/invoice/${address}`); + + if (invoice) { + let recipient = await get(`/users/${invoice.uid}`); + if (recipient?.id !== user.id) + redirect(307, `/pay/${recipient.username}`); + } + } catch (e) { + // invoice not found + } + + return { balance }; +} diff --git a/src/routes/(app)/send/bitcoin/[address]/+page.server.ts b/src/routes/(app)/send/bitcoin/[address]/+page.server.ts new file mode 100644 index 00000000..a9b72787 --- /dev/null +++ b/src/routes/(app)/send/bitcoin/[address]/+page.server.ts @@ -0,0 +1,7 @@ +import { auth, get } from "$lib/utils"; + +export async function load({ cookies }) { + let aid = cookies.get("aid"); + let { balance } = await get(`/account/${aid}`, auth(cookies)); + return { balance }; +} diff --git a/src/routes/(app)/send/bitcoin/[address]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/+page.svelte index 838c5c04..6b938c38 100644 --- a/src/routes/(app)/send/bitcoin/[address]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/+page.svelte @@ -8,9 +8,9 @@ export let data; - let { rates, user } = data; + let { balance, rates, user } = data; let { address } = $page.params; - let { balance, currency, username } = user; + let { currency, username } = user; let amount = 0; let submit, fiat; @@ -24,12 +24,6 @@ }; </script> -<a href="/send"> - <button type="button" class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80"> - <Icon icon="arrow-left" style="w-10" /> - </button> -</a> - <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> 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 7efe6e49..e46a9f56 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 @@ -1,15 +1,29 @@ import { fail, redirect } from "@sveltejs/kit"; -import { auth, post, fd } from "$lib/utils"; +import { auth, get, post, fd } from "$lib/utils"; export async function load({ params: { address, amount, feeRate }, cookies }) { + let aid = cookies.get("aid"); + try { - let { fee, fees, ourfee, hex } = await post( + let { fee, fees, inputs, ourfee, hex } = await post( "/bitcoin/fee", - { address, amount, feeRate }, + { address, amount, feeRate, aid }, auth(cookies) ); - return { amount, address, fee, fees, feeRate, ourfee, hex }; + let account = await get(`/account/${aid}`, auth(cookies)); + + return { + account, + amount, + address, + fee, + fees, + feeRate, + ourfee, + hex, + inputs, + }; } catch (e: any) { return { amount, address, feeRate, message: e.message }; } @@ -21,13 +35,15 @@ export const actions = { params: { address, amount, feeRate }, request, }) => { + let p; try { - await post("/bitcoin/send", await fd(request), auth(cookies)); + let body = await fd(request); + p = await post("/bitcoin/send", body, auth(cookies)); } catch (e: any) { console.log("problem sending bitcoin", e); return fail(400, { address, amount, feeRate, message: e.message }); } - redirect(307, "/sent"); + 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 84506e6b..ba87436f 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -1,20 +1,96 @@ <script> + import { tick } from "svelte"; 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 { fiat as toFiat, f, focus, s, sat, closest } from "$lib/utils"; + import { + fiat as 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"; + 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"; + import { wordlist } from "@scure/bip39/wordlists/english"; export let data; export let form; - let { amount, address, message, fee, fees, feeRate, ourfee, rates, hex } = - data; + let { encode, decode } = hexUtil; + let passwordPrompt; + let password; + let cancel = () => (passwordPrompt = false); + let signed; + + let togglePassword = () => (passwordPrompt = !passwordPrompt); + let handler = ({ cancel }) => { + if (account.seed && !signed) cancel(), togglePassword(); + return async ({ result }) => { + if (result.type === "redirect") { + goto(result.location); + } else { + await applyAction(result); + invalidate("app:payments"); + } + }; + }; + + let signTx = async () => { + let entropy = await decrypt(account.seed, password); + let mnemonic = entropyToMnemonic(entropy, wordlist); + let seed = await mnemonicToSeed(mnemonic, password); + let master = HDKey.fromMasterSeed(seed, network); + let child = master.derive("m/84'/0'/0'"); + + let tx = btc.Transaction.fromRaw(decode(hex)); + + for (let [i, input] of tx.inputs.entries()) { + let { witnessUtxo, path } = inputs[i]; + witnessUtxo.amount = BigInt(witnessUtxo.amount); + witnessUtxo.script = decode(witnessUtxo.script); + let key = child.derive(path); + tx.updateInput(i, { witnessUtxo }); + tx.signIdx(key.privateKey, i); + } + + tx.finalize(); + hex = tx.hex; + console.log("HEX", hex); + signed = true; + await tick(); + submit.click(); + }; + + let toggle = () => (submitting = !submitting); + + let { + account, + amount, + address, + message, + fee, + fees, + feeRate, + ourfee, + rates, + hex, + inputs, + } = data; $: reload(data); let reload = () => { @@ -48,15 +124,6 @@ let goBack = () => goto(`/send/bitcoin/${address}`); </script> -<button - type="button" - class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" - data-sveltekit-preload-data="false" - on:click={goBack} -> - <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" > @@ -115,10 +182,12 @@ </div> {/if} - <form method="POST" use:enhance on:submit={() => (submitting = true)}> + <form method="POST" use:enhance={handler}> <input name="pin" value={$pin} type="hidden" /> <input name="hex" value={hex} type="hidden" /> <input name="rate" value={$rate} type="hidden" /> + <input name="aid" value={account.id} type="hidden" /> + <input name="signed" value={signed} type="hidden" /> <div class="flex justify-center gap-2"> <button @@ -139,6 +208,10 @@ {/if} </div> +{#if passwordPrompt} + <WalletPass bind:password bind:cancel submit={signTx} /> +{/if} + <style> .no-transition { view-transition-name: fee; diff --git a/src/routes/(app)/send/ecash/+page.svelte b/src/routes/(app)/send/ecash/+page.svelte index 15c7c5d3..ef8e368c 100644 --- a/src/routes/(app)/send/ecash/+page.svelte +++ b/src/routes/(app)/send/ecash/+page.svelte @@ -29,25 +29,15 @@ let toggle = () => (submitting = !submitting); </script> -<a href="/send"> - <button type="button" class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80"> - <Icon icon="arrow-left" style="w-10" /> - </button> -</a> - <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.createEcash")} - </h1> - <form use:enhance={handler(toggle)} method="POST"> <Numpad bind:amount bind:fiat {currency} {submit} bind:rate={$rate} /> <input type="hidden" name="amount" bind:value={amount} /> - <div class="flex justify-center gap-2"> + <div> <button type="button" - class="hover:opacity-80 bg-black text-white rounded-2xl py-3 px-4 mt-2 border font-bold" + class="hover:opacity-80 rounded-2xl py-3 px-4 mt-2 border font-bold w-full max-w-[340px] mx-auto" on:click|preventDefault={setMax} disabled={submitting} on:keydown={setMax}>Max ⚡️{s(balance)}</button @@ -57,12 +47,13 @@ use:focus 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" + class="opacity-100 hover:opacity-80 rounded-2xl border py-3 font-bold mt-2 bg-black text-white px-4 w-full flex justify-center gap-2 max-w-[340px] mx-auto" > {#if submitting} <Spinner /> {:else} - {$t("payments.mint")} + <img src="/images/cash.png" class="w-8" /> + <div class="my-auto">{$t("payments.mint")}</div> {/if} </button> </div> diff --git a/src/routes/(app)/send/invoice/[id]/+page.server.ts b/src/routes/(app)/send/invoice/[id]/+page.server.ts index ed0198cf..6c89912d 100644 --- a/src/routes/(app)/send/invoice/[id]/+page.server.ts +++ b/src/routes/(app)/send/invoice/[id]/+page.server.ts @@ -7,31 +7,32 @@ export async function load({ params: { id }, parent }) { let invoice = await get(`/invoice/${id}`); if (invoice.prompt && invoice.tip === null) - redirect(307, `/${invoice.user.username}/invoice/${id}/tip`); + redirect(307, `/invoice/${id}/tip`); if (invoice.memoPrompt && invoice.memo === null) - redirect(307, `/${invoice.user.username}/invoice/${id}/memo`); + redirect(307, `/invoice/${id}/memo`); - if (invoice.user.username === user?.username) + if (invoice.account === user?.id) error(500, { message: "Cannot send to self" }); - if (!user) redirect(307, `/${invoice.user.username}/invoice/${id}`); + if (!user) redirect(307, `/invoice/${id}`); return { invoice, rates, user }; } export const actions = { default: async ({ cookies, params: { id }, request }) => { + let p; try { let body = await fd(request); body.hash = id; - await post("/payments", body, auth(cookies)); + p = await post("/payments", body, auth(cookies)); } catch (e: any) { console.log("payment failed", id, e); error(500, e.message); } - redirect(307, "/sent"); + 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 81a2c521..4348e473 100644 --- a/src/routes/(app)/send/invoice/[id]/+page.svelte +++ b/src/routes/(app)/send/invoice/[id]/+page.svelte @@ -52,7 +52,7 @@ let toggle = () => (submitting = !submitting); let external = async () => { - let { id } = await post(`/${recipient.username}/invoice`, { + let { id } = await post(`/invoice`, { invoice: { ...invoice, type: "lightning", @@ -62,7 +62,7 @@ user: { username: recipient.username }, }); - goto(`/${recipient.username}/invoice/${id}?options=true`, { + goto(`/invoice/${id}?options=true`, { invalidateAll: true, }); }; @@ -93,10 +93,6 @@ // }); </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} diff --git a/src/routes/(app)/send/lightning/[payreq]/+page.server.ts b/src/routes/(app)/send/lightning/[payreq]/+page.server.ts index ddec2968..3e87fa53 100644 --- a/src/routes/(app)/send/lightning/[payreq]/+page.server.ts +++ b/src/routes/(app)/send/lightning/[payreq]/+page.server.ts @@ -9,16 +9,17 @@ export const actions = { setAmount: async ({ request }) => fd(request), send: async ({ cookies, request }) => { + let p; try { let body = await fd(request); - await post("/payments", body, auth(cookies)); + p = await post("/payments", body, auth(cookies)); } catch (e: any) { if (e.message.includes("unusable")) e.message = "Failed to route payment, try sending a lower amount"; return fail(400, { message: e.message }); } - redirect(307, "/sent"); + redirect(307, `/sent/${p.id}`); }, }; diff --git a/src/routes/(app)/send/lightning/[payreq]/+page.svelte b/src/routes/(app)/send/lightning/[payreq]/+page.svelte index 57a17e01..74c100f8 100644 --- a/src/routes/(app)/send/lightning/[payreq]/+page.svelte +++ b/src/routes/(app)/send/lightning/[payreq]/+page.svelte @@ -58,11 +58,7 @@ let toggle = () => (show = !show); </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 text-center space-y-5"> +<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"> @@ -81,12 +77,9 @@ <h3 class="text-secondary text-xl">⚡️{s(amount)}</h3> </div> - <div> - <div class="text-lg text-secondary"> - {$t("payments.to")} - </div> - - <p class="text-2xl break-words font-semibold mb-8">{alias}</p> + <div class="text-xl"> + <span class="text-secondary">{$t("payments.to")}</span> + <span class="break-words font-semibold">{alias}</span> </div> {#if ourfee} @@ -109,7 +102,7 @@ use:enhance on:submit={submit} action="?/send" - class="space-y-5" + class="space-y-2" > <input name="payreq" value={payreq} type="hidden" /> <input name="amount" value={amount} type="hidden" /> diff --git a/src/routes/(app)/sent/+page.server.ts b/src/routes/(app)/sent/+page.server.ts deleted file mode 100644 index 62a4d9b4..00000000 --- a/src/routes/(app)/sent/+page.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { get, auth } from "$lib/utils"; - -export let load = async ({ cookies }) => { - let { payments } = await get("/payments", auth(cookies)); - return { payments }; -}; diff --git a/src/routes/(app)/sent/[id]/+page.server.ts b/src/routes/(app)/sent/[id]/+page.server.ts new file mode 100644 index 00000000..38d95e4e --- /dev/null +++ b/src/routes/(app)/sent/[id]/+page.server.ts @@ -0,0 +1,7 @@ +import { get, auth } from "$lib/utils"; + +export let load = async ({ cookies, params }) => { + let { id } = params; + let p = await get(`/payments/${id}`, auth(cookies)); + return p; +}; diff --git a/src/routes/(app)/sent/+page.svelte b/src/routes/(app)/sent/[id]/+page.svelte index 136bd395..ce2c8d6e 100644 --- a/src/routes/(app)/sent/+page.svelte +++ b/src/routes/(app)/sent/[id]/+page.svelte @@ -6,11 +6,8 @@ import { t } from "$lib/translations"; export let data; - let { payments, user } = data; + let { amount, rate, tip, user } = data; let { currency, username } = user; - - let payment = payments[0]; - let { amount, rate, tip } = payment; amount = Math.abs(amount); toast.pop(0); @@ -18,7 +15,7 @@ <div class="text-center mt-20 md:mt-0"> <div - class="flex w-full max-w-[200px] mx-auto py-20" + class="flex w-full max-w-[200px] mx-auto py-4" in:scale={{ start: 0.5 }} > <Icon icon="check" style="mx-auto" /> @@ -43,7 +40,7 @@ </h3> </div> -<a href={`/${username}/payments`} use:focus> +<a href={`/${username}`} use:focus> <div class="opacity-0 w-screen h-screen fixed top-0 left-0 z-50" /> </a> diff --git a/src/routes/(app)/[username]/settings/[...tab]/+page.server.ts b/src/routes/(app)/settings/[...tab]/+page.server.ts index 42639b38..42639b38 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/+page.server.ts +++ b/src/routes/(app)/settings/[...tab]/+page.server.ts diff --git a/src/routes/(app)/[username]/settings/[...tab]/+page.svelte b/src/routes/(app)/settings/[...tab]/+page.svelte index 12980291..f59806d1 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/+page.svelte +++ b/src/routes/(app)/settings/[...tab]/+page.svelte @@ -137,7 +137,7 @@ content: JSON.stringify({ name: user.username, about: user.address, - picture: `${PUBLIC_COINOS_URL}/public/${user.profile}.webp`, + picture: `${$page.url.origin}/public/${user.profile}.webp`, }), tags: [], }; @@ -206,7 +206,7 @@ <input type="hidden" name="pin" value={$pin} /> <input type="hidden" name="tab" value={tab} /> - <div class="mt-24 mb-20 px-3 md:px-0 w-full md:w-[400px] mx-auto space-y-8"> + <div class="mt-24 mb-20 px-3 md:px-0 w-full md:max-w-lg mx-auto space-y-8"> <div class="header"> <h1 class="text-center text-3xl md:text-4xl font-semibold mb-10"> {$t("user.settings.header")} @@ -216,7 +216,7 @@ class="font-bold flex justify-around items-center border-b pb-3 text-secondary" > {#each tabs.filter((t) => t.name !== "shopify") as { name, key }} - <a href={`/${user.username}/settings/${name}`}> + <a href={`/settings/${name}`}> <button type="button" class="hover:opacity-80" @@ -249,9 +249,3 @@ </button> </div> </form> - -<style> - .settings { - view-transition-name: settings; - } -</style> diff --git a/src/routes/(app)/[username]/settings/[...tab]/_account.svelte b/src/routes/(app)/settings/[...tab]/_account.svelte index 8e3323c7..8e3323c7 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/_account.svelte +++ b/src/routes/(app)/settings/[...tab]/_account.svelte diff --git a/src/routes/(app)/[username]/settings/[...tab]/_pos.svelte b/src/routes/(app)/settings/[...tab]/_pos.svelte index 6a1c92a1..dcfcbe44 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/_pos.svelte +++ b/src/routes/(app)/settings/[...tab]/_pos.svelte @@ -63,17 +63,6 @@ </div> <div> - <label for="memoPrompt" class="font-bold mb-1 block" - >{$t("user.settings.memoPrompt")}</label - > - <textarea - name="memoPrompt" - placeholder={$t("user.settings.memoPromptPlaceholder")} - bind:value={user.memoPrompt} - /> -</div> - -<div> <div class="flex justify-between items-center"> <span class="font-bold">{$t("user.settings.notifications")}</span> <Toggle id="notify" bind:value={user.notify} /> @@ -95,18 +84,6 @@ <div> <div class="flex justify-between items-center"> - <span class="font-bold">{$t("user.settings.publishNip5")}</span> - <Toggle id="nip5" bind:value={user.nip5} /> - </div> - <p class="text-secondary mt-1 w-9/12"> - {$t("user.settings.nip5a")} - <span class="font-bold">{user.username}@{$page.url.host}</span> - {$t("user.settings.nip5b")} - </p> -</div> - -<div> - <div class="flex justify-between items-center"> <span class="font-bold">{$t("user.settings.autoWithdraw")}</span> <Toggle id="autowithdraw" bind:value={user.autowithdraw} /> </div> diff --git a/src/routes/(app)/[username]/settings/[...tab]/_security.svelte b/src/routes/(app)/settings/[...tab]/_security.svelte index 6b44f830..6b44f830 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/_security.svelte +++ b/src/routes/(app)/settings/[...tab]/_security.svelte diff --git a/src/routes/(app)/[username]/settings/[...tab]/_shopify.svelte b/src/routes/(app)/settings/[...tab]/_shopify.svelte index 0266fa9c..0266fa9c 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/_shopify.svelte +++ b/src/routes/(app)/settings/[...tab]/_shopify.svelte diff --git a/src/routes/(app)/[username]/verify/[code]/+page.server.ts b/src/routes/(app)/verify/[code]/+page.server.ts index fec1a85c..66ef39ad 100644 --- a/src/routes/(app)/[username]/verify/[code]/+page.server.ts +++ b/src/routes/(app)/verify/[code]/+page.server.ts @@ -10,5 +10,5 @@ export let load = async ({ params: { code } }) => { return { error: "Verification failed" }; } - redirect(307, `/${user.username}/settings/account?verified=true`); + redirect(307, `/settings/account?verified=true`); }; diff --git a/src/routes/(app)/[username]/verify/[code]/+page.svelte b/src/routes/(app)/verify/[code]/+page.svelte index 1e099dae..1e099dae 100644 --- a/src/routes/(app)/[username]/verify/[code]/+page.svelte +++ b/src/routes/(app)/verify/[code]/+page.svelte diff --git a/src/routes/(static)/+layout.svelte b/src/routes/(static)/+layout.svelte index 13073d5a..8b14fd38 100644 --- a/src/routes/(static)/+layout.svelte +++ b/src/routes/(static)/+layout.svelte @@ -9,7 +9,7 @@ <SvelteToast options={{ reversed: true, intro: { y: 192 } }} /> <LandingHeader {user} /> -<main class="container flex px-10 md:px-40 mx-auto min-h-[600px] pb-20"> +<main class="container flex px-4 md:px-40 mx-auto min-h-[600px]"> <slot /> </main> <Footer /> diff --git a/src/routes/(app)/login/+page.server.ts b/src/routes/(static)/login/+page.server.ts index fd7fec8c..fd7fec8c 100644 --- a/src/routes/(app)/login/+page.server.ts +++ b/src/routes/(static)/login/+page.server.ts diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte new file mode 100644 index 00000000..df7db5e6 --- /dev/null +++ b/src/routes/(static)/login/+page.svelte @@ -0,0 +1,171 @@ +<script> + import { onMount } from "svelte"; + import { applyAction, deserialize } from "$app/forms"; + import { tick } from "svelte"; + import { fly } from "svelte/transition"; + import { enhance } from "$app/forms"; + import Pin from "$comp/Pin.svelte"; + import Icon from "$comp/Icon.svelte"; + import Spinner from "$comp/Spinner.svelte"; + import { focus, fail } from "$lib/utils"; + import { password, pin, loginRedirect } from "$lib/store"; + import { t } from "$lib/translations"; + import { page } from "$app/stores"; + import { generate } from "$lib/nostr"; + import { invalidateAll } from "$app/navigation"; + + onMount(() => { + $pin = undefined; + localStorage.clear(); + sessionStorage.clear(); + + let lang = $page.url.searchParams.get("lang"); + if (lang) document.cookie = `lang=${lang} ;`; + }); + + export let form; + + let token, formElement; + let code = []; + let redirect; + + $password = undefined; + + $: need2fa = form?.message === "2fa"; + let cancel = () => (need2fa = false); + $: if (form?.message === "2fa" && form.token === token) token = ""; + + let username, email, btn; + + $: update(form); + let update = (form) => form && ({ username, password: $password } = form); + + $: token && token?.length === 6 && tick().then(() => btn.click()); + + let revealPassword = false; + + let loading; + async function handleSubmit(e) { + loading = true; + + let data = new FormData(this); + let user = Object.fromEntries(data); + await generate(user); + + for (let k in user) { + data.set(k, user[k]); + } + + const response = await fetch("/login", { + method: "POST", + body: data, + }); + + const result = deserialize(await response.text()); + + if (result.type === "success") { + await invalidateAll(); + } + + applyAction(result); + loading = false; + } +</script> + +{#if need2fa} + <Pin bind:value={token} title="Enter 2FA Code" {cancel} notify={false} /> +{/if} + +<div class="mx-auto md:shadow-xl rounded-3xl max-w-xl w-full md:w-[480px] md:p-8 mb-20"> + <h1 class="text-2xl font-bold text-center">{$t("login.signIn")}</h1> + + {#if form?.error} + <div class="text-red-600 text-center" in:fly> + {form.error} + </div> + {/if} + + <form class="space-y-5" on:submit|preventDefault={handleSubmit} method="POST"> + <input + type="hidden" + name="loginRedirect" + value={$loginRedirect || $page.url.searchParams.get("redirect")} + /> + <input type="hidden" name="token" value={token} /> + + <div> + <label for="username" class="font-semibold">{$t("login.username")}</label> + <input + name="username" + type="text" + required + class="bg-primary" + bind:value={username} + use:focus + autocapitalize="none" + /> + </div> + + <div class="relative"> + <label for="password" class="block font-semibold" + >{$t("login.password")}</label + > + {#if revealPassword} + <input + name="password" + type="text" + required + class="bg-primary" + bind:value={$password} + autocapitalize="none" + /> + {:else} + <input + name="password" + type="password" + required + class="bg-primary" + bind:value={$password} + autocapitalize="none" + /> + {/if} + <button + type="button" + on:click={() => (revealPassword = !revealPassword)} + class="absolute right-5 top-10" + > + <Icon icon={revealPassword ? "eye" : "eye-off"} /> + </button> + </div> + + <div class="flex justify-end items-center"> + <a href="/forgot" class="underline underline-offset-4 text-black" + >{$t("login.forgotUserOrPassword")}</a + > + </div> + + <button + type="submit" + class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80" + disabled={loading} + bind:this={btn} + > + {#if loading} + <Spinner /> + {:else} + {$t("login.signIn")} + {/if} + </button> + + + <p class="text-secondary text-center font-medium"> + {$t("login.noAccount")} + <a + href={"/register"} + class="block md:inline text-black underline underline-offset-4 hover:opacity-80" + > + {$t("login.register")} + </a> + </p> + </form> +</div> diff --git a/src/routes/(app)/register/+page.server.ts b/src/routes/(static)/register/+page.server.ts index 731530c4..731530c4 100644 --- a/src/routes/(app)/register/+page.server.ts +++ b/src/routes/(static)/register/+page.server.ts diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte new file mode 100644 index 00000000..3491bd5d --- /dev/null +++ b/src/routes/(static)/register/+page.svelte @@ -0,0 +1,292 @@ +<script> + import punks from "$lib/punks"; + import { upload } from "$lib/upload"; + import { afterNavigate, invalidateAll } from "$app/navigation"; + import { applyAction, deserialize } from "$app/forms"; + import { tick } from "svelte"; + import { fly } from "svelte/transition"; + import { enhance } from "$app/forms"; + + import Pin from "$comp/Pin.svelte"; + import Icon from "$comp/Icon.svelte"; + import Spinner from "$comp/Spinner.svelte"; + + import { focus, fail } from "$lib/utils"; + import { avatar, password, loginRedirect } from "$lib/store"; + import { t } from "$lib/translations"; + import { page } from "$app/stores"; + import { generate } from "$lib/nostr"; + import { + NumberDictionary, + uniqueNamesGenerator, + colors, + adjectives, + animals, + } from "unique-names-generator"; + + export let form; + export let data; + + $: data && ($avatar = undefined); + let username; + let { index } = data; + + let cleared; + let clear = () => { + if (!cleared) { + cleared = true; + username = ""; + $password = ""; + revealPassword = false; + } + }; + + let refresh = async () => { + username = uniqueNamesGenerator({ + dictionaries: [animals, NumberDictionary.generate({ min: 10, max: 99 })], + length: 2, + separator: "", + }); + + $password = uniqueNamesGenerator({ + dictionaries: [colors, NumberDictionary.generate({ min: 100, max: 999 })], + length: 2, + separator: "", + }); + + revealPassword = true; + }; + + afterNavigate(async () => { + try { + await invalidateAll(); + } catch (e) { + console.log(e); + } + }); + + let token, formElement; + let code = []; + let redirect; + + $: need2fa = form?.message === "2fa"; + let cancel = () => (need2fa = false); + $: if (form?.message === "2fa" && form.token === token) token = ""; + + let email, btn; + + $: update(form); + let update = (form) => form && ({ username, password: $password } = form); + + $: token && token?.length === 6 && tick().then(() => btn.click()); + + let revealPassword; + + let loading; + async function handleSubmit(e) { + loading = true; + + let data = new FormData(this); + let user = Object.fromEntries(data); + await generate(user); + + for (let k in user) { + data.set(k, user[k]); + } + + data.set("profile", punks[index]); + if ($avatar) { + try { + let { hash } = JSON.parse( + await upload($avatar.file, $avatar.type, $avatar.progress, token) + ); + + data.set("profile", hash); + await fetch(`/api/public/${hash}.webp`, { + cache: "reload", + mode: "no-cors", + }); + } catch (e) { + console.log("problem uploading avatar", e); + } + } + + const response = await fetch("/register", { + method: "POST", + body: data, + }); + + const result = deserialize(await response.text()); + + if (result.type === "success") { + await invalidateAll(); + } + + applyAction(result); + loading = false; + } + + let avatarInput; + $: src = `/api/public/${punks[index]}.webp`; + let decr = () => (index = index <= 0 ? 63 : index - 1); + let incr = () => (index = index >= 63 ? 0 : index + 1); + let selectAvatar = () => avatarInput.click(); + + let progress; + let handleFile = async ({ target }) => { + let type = "profile"; + let file = target.files[0]; + if (!file) return; + + if (file.size > 10000000) form.error = "File too large"; + $avatar = { file, type, progress }; + + var reader = new FileReader(); + reader.onload = async (e) => { + src = e.target.result; + }; + + reader.readAsDataURL(file); + }; +</script> + +{#if need2fa} + <Pin bind:value={token} title="Enter 2FA Code" {cancel} notify={false} /> +{/if} + +<div + class="mx-auto md:shadow-xl rounded-3xl max-w-xl w-full md:w-[480px] md:p-8 space-y-5 mb-20" +> + <h1 class="text-2xl font-bold text-center">{$t("login.createAccount")}</h1> + <input + type="file" + class="hidden" + bind:this={avatarInput} + on:change={(e) => handleFile(e, "profile")} + /> + + <div class="relative"> + <button + class="absolute w-8 h-12 left-12 bg-white rounded top-12" + on:click={decr} + > + <Icon icon="chevron-left" style="w-8 " /> + </button> + <button class="block relative w-32 mx-auto" on:click={selectAvatar}> + <button + class="w-32 h-32 rounded-full border-4 border-white overflow-hidden flex mx-auto relative" + > + <img + {src} + class="w-full h-full object-cover object-center overflow-hidden" + alt={username} + /> + </button> + <button + class="absolute bg-white rounded-full p-2 mx-auto right-0 bottom-0 z-10 bg-opacity-80" + > + <Icon icon="upload" style="w-8" /> + </button> + </button> + <button + class="absolute w-8 h-12 right-12 bg-white rounded top-12" + on:click={incr} + > + <Icon icon="chevron-left" style="w-8 rotate-180" /> + </button> + </div> + + {#if form?.error} + <div class="text-red-600 text-center" in:fly> + {form.error} + </div> + {/if} + + <form class="space-y-5" on:submit|preventDefault={handleSubmit} method="POST"> + <input + type="hidden" + name="loginRedirect" + value={$loginRedirect || $page.url.searchParams.get("redirect")} + /> + <input type="hidden" name="token" value={token} /> + + <div class="relative"> + <label for="username" class="font-semibold">{$t("login.username")}</label> + <input + use:focus + name="username" + type="text" + required + class="bg-primary" + bind:value={username} + on:focus={clear} + autocapitalize="none" + /> + <button + tabindex="-1" + type="button" + on:click={refresh} + class="absolute right-0 top-4" + > + <Icon icon="random" style="w-10 m-4" /> + </button> + </div> + + <div class="relative"> + <label for="password" class="block font-semibold" + >{$t("login.password")}</label + > + {#if revealPassword} + <input + name="password" + type="text" + required + class="bg-primary" + bind:value={$password} + autocapitalize="none" + /> + {:else} + <input + name="password" + type="password" + required + class="bg-primary" + bind:value={$password} + autocapitalize="none" + on:focus={clear} + /> + {/if} + <button + type="button" + tabindex="-1" + on:click={() => (revealPassword = !revealPassword)} + class="absolute right-0 top-4" + > + <Icon icon={revealPassword ? "eye" : "eye-off"} style="m-6" /> + </button> + </div> + + <button + type="submit" + class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80" + disabled={loading} + bind:this={btn} + > + {#if loading} + <Spinner /> + {:else} + {$t("login.register")} + {/if} + </button> + + <p class="text-secondary text-center font-medium"> + {$t("login.haveAccount")} + <a + href={"/login"} + class="block md:inline text-black underline underline-offset-4 hover:opacity-80" + > + {$t("login.signIn")} + </a> + </p> + </form> +</div> diff --git a/src/routes/(app)/support/+page.svelte b/src/routes/(static)/support/+page.svelte index ad31ad1a..6b32124e 100644 --- a/src/routes/(app)/support/+page.svelte +++ b/src/routes/(static)/support/+page.svelte @@ -1,8 +1,8 @@ <script> + import { browser } from "$app/environment"; import { onDestroy } from "svelte"; import { fail, success, post } from "$lib/utils"; import { scale } from "svelte/transition"; - import AppHeader from "$comp/AppHeader.svelte"; import Icon from "$comp/Icon.svelte"; import { t } from "$lib/translations"; @@ -30,6 +30,7 @@ }; onDestroy(() => { + if (!browser) return; const nodeBadge = document.querySelector(".grecaptcha-badge"); if (nodeBadge) { document.body.removeChild(nodeBadge.parentNode); @@ -52,13 +53,9 @@ ></script></svelte:head > -{#if user} - <AppHeader {data} /> -{/if} - -<div class="container px-4 max-w-lg mx-auto mt-20 space-y-8"> +<div class="container max-w-lg mx-auto space-y-8 p-4 pb-20"> {#if sent} - <h1 class="text-center text-3xl md:text-4xl font-semibold">Thank you!</h1> + <h1 class="text-center text-3xl lg:text-4xl font-semibold">Thank you!</h1> <div class="text-center text-lg text-secondary"> Someone will be in touch shortly. </div> @@ -89,7 +86,6 @@ >{$t("user.support.accountName")}</label > <input - class="bg-primary" type="text" name="account" required @@ -102,7 +98,6 @@ >{$t("user.support.email")}</label > <input - class="bg-primary" type="email" name="email" bind:value={email} @@ -116,7 +111,6 @@ > <textarea rows={5} - class="bg-primary" type="text" name="message" bind:value={message} @@ -127,12 +121,8 @@ <button type="submit" disabled={!email || !message} - class="g-recaptcha {!email || !message - ? 'opacity-50' - : 'opacity-100'} bg-black text-white font-bold rounded-xl py-3 w-full mx-auto {email && - message - ? 'hover:opacity-80' - : ''}">{$t("user.support.send")}</button + class="g-recaptcha bg-black text-white font-bold rounded-xl py-3 w-full mx-auto hover:opacity-80" + >{$t("user.support.send")}</button > </form> {/if} diff --git a/src/routes/(app)/support/+server.ts b/src/routes/(static)/support/+server.ts index e9a16cfc..e9a16cfc 100644 --- a/src/routes/(app)/support/+server.ts +++ b/src/routes/(static)/support/+server.ts diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 00000000..faa7d738 --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,37 @@ +<script> + import Icon from "$comp/Icon.svelte"; + import { page } from "$app/stores"; + + export let data; + let { user } = data; + + let link = user ? `/${user.username}` : "/"; +</script> + +<div class="container px-4 max-w-xl mx-auto mt-10 space-y-5"> + <div class="w-full flex"> + <a href="/" class="mx-auto"> + <Icon icon="logo" /> + </a> + </div> + + <div class="flex justify-center items-center"> + <div class="shadow-xl rounded-3xl px-10 pt-5 pb-10 space-y-5 w-full mx-5"> + <div class="relative"> + <h1 class="text-2xl md:text-3xl font-semibold text-center">Error</h1> + </div> + <p class="text-secondary text-center">Something went wrong</p> + <p class="text-secondary text-center">{$page.error.message}</p> + + <div class="flex justify-center py-24"> + <a href={link}> + <button + class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 text-xl" + > + Back + </button> + </a> + </div> + </div> + </div> +</div> diff --git a/src/routes/start/+server.ts b/src/routes/start/+server.ts index 44561d40..2a1d88c3 100644 --- a/src/routes/start/+server.ts +++ b/src/routes/start/+server.ts @@ -3,7 +3,7 @@ import { redirect } from "@sveltejs/kit"; export async function GET({ cookies }) { let username = cookies.get("username"); if (username) { - redirect(307, `/${username}/receive`); + redirect(307, `/invoice`); } redirect(307, "/"); |
