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 | |
| parent | 20c0c52f8b850bfbab490735b0b5cd5d08851488 (diff) | |
| parent | 9531ccbdf01b07c392b1b5cb6d8ad4680ee29d2c (diff) | |
| download | coinos-ui-d49d4119811dee9270f0858f3f03e5d8cd0d95ba.tar.gz coinos-ui-d49d4119811dee9270f0858f3f03e5d8cd0d95ba.zip | |
Merge branch 'self-custody'
141 files changed, 2793 insertions, 3780 deletions
diff --git a/.env.production b/.env.production index 1ae57c17..8b72abf4 100644 --- a/.env.production +++ b/.env.production @@ -6,3 +6,4 @@ PUBLIC_LIQUID_EXPLORER=https://liquid.network PUBLIC_DOMAIN=coinos.io PUBLIC_COINOS_PUBKEY=ba80990666ef0b6f4ba5059347beb13242921e54669e680064ca755256a1e3a6 PUBLIC_COINOS_RELAY=wss://relay.coinos.io +PUBLIC_COINOS_NETWORK=regtest Binary files differdiff --git a/package.json b/package.json index 848db710..2ed4c699 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,9 @@ "@capacitor/cli": "^6.0.0", "@capacitor/core": "^6.0.0", "@capacitor/ios": "^6.1.2", - "@scure/base": "^1.1.1", + "@scure/base": "^1.1.8", "@scure/bip39": "^1.2.1", + "@scure/btc-signer": "^1.3.2", "@sveltejs/adapter-node": "^2.0.0", "@sveltejs/kit": "^2.5.22", "@sveltejs/vite-plugin-svelte": "^3.0.0", diff --git a/src/components/About.svelte b/src/components/About.svelte index 1e60db72..01ff4c7c 100644 --- a/src/components/About.svelte +++ b/src/components/About.svelte @@ -91,14 +91,15 @@ {@html $t("about.locations.description")} </p> <!-- <LocationsMap {locations} /> --> + <a href="/map"> <div class="relative"> <img src="/map.png" class="mx-auto" /> <button - class="bg-black text-white border rounded-full px-6 py-2 font-bold hover:opacity-80 absolute left-1/2 top-1/2 text-2xl" + class="bg-black text-white border border-4 rounded-full px-6 py-2 font-bold hover:bg-secondary absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 text-2xl" > - View map + {$t("about.locations.viewMap")} </button> </div> </a> diff --git a/src/components/Account.svelte b/src/components/Account.svelte new file mode 100644 index 00000000..458f67c6 --- /dev/null +++ b/src/components/Account.svelte @@ -0,0 +1,64 @@ +<script> + import Icon from "$comp/Icon.svelte"; + import Balance from "$comp/Balance.svelte"; + import { t } from "$lib/translations"; + + export let user, rate, account; + let { name, seed, balance, id } = account; + $: type = seed ? "Savings" : "Cash"; + + $: refresh(account); + let refresh = (a) => ({ seed, balance, id } = a); + + let setAccount = () => (document.cookie = `aid=${id}; path=/; max-age=86400`); +</script> + +<a href={`/payments`} class="block" on:click={setAccount}> + <div class="py-4 px-2 space-y-5 bg-primary rounded-2xl border"> + <div class="flex"> + <Balance {balance} {user} {rate} /> + {#if seed} + <a href={`/account/${id}`} class="contents"> + <button class="ml-auto flex gap-1 mb-auto pb-4 pl-4"> + <Icon icon="settings" style="w-8 my-auto" /> + </button> + </a> + {/if} + </div> + + <div class="flex gap-1 justify-center w-full text-xl"> + <a href={`/invoice`} class="contents" on:click={setAccount}> + <button + class="rounded-2xl border py-5 px-4 hover:opacity-80 flex grow bg-white" + > + <div class="mx-auto flex gap-2"> + <Icon icon="receive" style="w-8" /> + <div class="my-auto">{$t("user.dashboard.receive")}</div> + </div> + </button> + </a> + + <a href={`/scan`} class="contents" on:click={setAccount}> + <button + class="rounded-2xl border py-5 px-4 hover:opacity-80 flex shrink bg-white" + > + <div class="mx-auto flex gap-2"> + <Icon icon="camera" style="w-8" /> + </div> + </button> + </a> + + <a href={`/send`} class="contents grow" on:click={setAccount}> + <button + type="button" + class="rounded-2xl border py-5 px-6 hover:opacity-80 flex bg-white grow" + > + <div class="mx-auto flex gap-1"> + <Icon icon="send" style="w-8" /> + <div class="my-auto">{$t("user.dashboard.send")}</div> + </div> + </button> + </a> + </div> + </div> +</a> diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte index 3b0303a8..cc521e93 100644 --- a/src/components/AppHeader.svelte +++ b/src/components/AppHeader.svelte @@ -7,11 +7,7 @@ import { loading, t } from "$lib/translations"; import { page } from "$app/stores"; - export let data; - - let { subject, user } = data; - $: update(data); - let update = (data) => ({ subject, user } = data); + export let user, subject; let menuButtons; $: if (user) @@ -19,15 +15,15 @@ { key: "nav.settings", icon: "settings", - href: `/${user.username}/settings`, + href: `/settings`, }, { key: "nav.support", icon: "support", href: `/support` }, - { key: "nav.map", icon: "map", href: `/${user.username}/map` }, - { - key: "nav.messages", - icon: "message", - href: `/${user.username}/messages`, - }, + { key: "nav.map", icon: "map", href: `/map` }, + // { + // key: "nav.messages", + // icon: "message", + // href: `/messages`, + // }, { key: "nav.merch", icon: "shop", href: `https://coinosmerch.com` }, { key: "nav.signOut", icon: "logout", href: `/logout` }, ]; @@ -60,19 +56,19 @@ ><Icon icon="home" style="mx-auto w-6 sm:w-8" /> </button> </a> - <a href={`/${user.username}/receive`}> + <a href={`/invoice`}> <button class="bg-white p-2 rounded-full w-12 h-12 sm:w-16 sm:h-16 drop-shadow-xl border border-black/10 {$page - .url.pathname === `/${user.username}/receive` + .url.pathname.includes('invoice') ? 'opacity-100' : 'opacity-70 hover:opacity-80'}" ><Icon icon="receive" style="mx-auto w-6 sm:w-8" /> </button> </a> - <a href={`/${user.username}/payments`}> + <a href={`/payments`}> <button class="bg-white p-2 rounded-full w-12 h-12 sm:w-16 sm:h-16 drop-shadow-xl border border-black/10 {$page - .url.pathname === `/${user.username}/payments` + .url.pathname === `/payments` ? 'opacity-100' : 'opacity-70 hover:opacity-80'} relative" ><Icon icon="clock" style="mx-auto w-6 sm:w-8" /> @@ -102,9 +98,8 @@ <div class="relative"> <OutClick on:outclick={() => (showMenu = false)}> <button - class="bg-white p-2 rounded-full w-12 h-12 sm:w-16 sm:h-16 drop-shadow-xl border border-black/10 {$page - .url.pathname === `/${user.username}` || - $page.url.pathname === `/${user.username}/settings` || + class="bg-white p-2 rounded-full w-12 h-12 sm:w-16 sm:h-16 drop-shadow-xl border border-black/10 { + $page.url.pathname === `/settings` || $page.url.pathname === `/support` ? 'opacity-100' : `opacity-70 hover:opacity-80 ${ diff --git a/src/components/Balance.svelte b/src/components/Balance.svelte index 4d5ac6bd..51c56555 100644 --- a/src/components/Balance.svelte +++ b/src/components/Balance.svelte @@ -6,7 +6,7 @@ import { t } from "$lib/translations"; let show; - export let user, rate; + export let user, rate, balance; </script> {#if show && user.haspin && $pin?.length !== 6} @@ -27,12 +27,12 @@ {#if isNaN(rate)} <div class="text-gray-200">—</div> {:else} - {f((user.balance * rate) / sats, user.currency)} + {f((balance * rate) / sats, user.currency)} {/if} </div> <div class="text-secondary text-2xl"> - ⚡️{s(user.balance)} + ⚡️{s(balance)} </div> {/if} </div> diff --git a/src/components/Footer.svelte b/src/components/Footer.svelte index d70c013c..067e185e 100644 --- a/src/components/Footer.svelte +++ b/src/components/Footer.svelte @@ -4,7 +4,7 @@ import { t } from "$lib/translations"; const followLinks = [ - { url: "https://twitter.com/coinoswallet", titleID: "twitter" }, + { url: "nostr:nprofile1qqst4qyeqenw7zm0fwjsty68h6cnys5jre2xd8ngqpjv5a2j26s78fspz4mhxue69uhhyetvv9ujucm0d9hx7uewd9hsgwx239", titleID: "nostr" }, { url: "https://t.me/coinoswallet", titleID: "telegram" }, { url: "https://linktr.ee/coinos.io", titleID: "linktree" }, ]; diff --git a/src/components/Invoice.svelte b/src/components/Invoice.svelte index ba436649..563a2fee 100644 --- a/src/components/Invoice.svelte +++ b/src/components/Invoice.svelte @@ -27,7 +27,7 @@ </div> </div> <div class="mx-auto my-auto flex gap-2"> - <a href={`/${invoice.user.username}/invoice/${$invoice.id}/tip`}> + <a href={`/invoice/${$invoice.id}/tip`}> <button class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-32" > diff --git a/src/components/Items.svelte b/src/components/Items.svelte new file mode 100644 index 00000000..2916721d --- /dev/null +++ b/src/components/Items.svelte @@ -0,0 +1,246 @@ +<script> + import { t } from "$lib/translations"; + import { btc, f, sat, post } from "$lib/utils"; + import { browser } from "$app/environment"; + import { goto, invalidate } from "$app/navigation"; + import { fade } from "svelte/transition"; + import { cubicIn } from "svelte/easing"; + import { flip } from "svelte/animate"; + import { + dndzone, + SHADOW_ITEM_MARKER_PROPERTY_NAME, + SOURCES, + TRIGGERS, + } from "svelte-dnd-action"; + import DeleteItem from "$comp/DeleteItem.svelte"; + import Icon from "$comp/Icon.svelte"; + + export let subject, user, items, rate, currency, total; + + let flipDurationMs = 200; + let dropTargetStyle = {}; + + let dragDisabled = + subject?.id !== user?.id || (browser && window.innerWidth < 768); + + let deleting; + 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(`/invoice`, { + invoice, + user: { username: subject.username }, + }); + + if (invoice.memoPrompt) { + return goto(`/invoice/${id}/memo`); + } + goto(`/invoice/${id}`); + }; + + let reset = () => + (dragDisabled = + user?.username !== subject?.username || window.innerWidth < 768); +</script> + +<svelte:window on:resize={reset} /> + +{#if deleting} + <DeleteItem bind:item={deleting} /> +{/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={`/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={`/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> + +<div + class="fixed inset-x-0 mx-auto flex bottom-16" + class:static={!items.length} +> + {#if total > 0} + <button + class="rounded-2xl py-5 px-6 font-bold hover:bg-neutral-700 flex bg-black text-white mx-auto" + on:click={checkout} + > + <div class="mx-auto flex"> + <div class="my-auto text-2xl"> + {$t("user.dashboard.checkout")} + {f(total, currency)} + <span class="text-base"> + {sat(btc(total, rate), currency)} + </span> + </div> + </div> + </button> + {/if} +</div> + +<style> + .custom-shadow-item { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + visibility: visible; + background: #333; + opacity: 0.5; + margin: 0; + } +</style> diff --git a/src/components/LandingHeader.svelte b/src/components/LandingHeader.svelte index d1094a1d..72913999 100644 --- a/src/components/LandingHeader.svelte +++ b/src/components/LandingHeader.svelte @@ -19,22 +19,22 @@ </script> <header - class="w-full lg:w-5/6 py-5 mx-auto fixed md:sticky z-10 top-0 bg-white/90" + class="w-full lg:w-5/6 py-5 mx-auto fixed sticky z-10 top-0 bg-white/90" bind:this={header} > - <nav class="block xl:flex flex-wrap justify-between items-center"> + <nav class="block lg:flex justify-between items-center"> <div - class="flex justify-start md:justify-center items-center md:space-x-10" + class="flex justify-start lg:justify-center items-center lg:space-x-10" > <a href="/" on:click={() => scroll(header)}> - <Icon icon="logo" style="hidden md:block mb-5 xl:mb-0" /> - <Icon icon="logo" style="block md:hidden ml-5 w-48" /> + <Icon icon="logo" style="hidden lg:block mb-5 xl:mb-0" /> + <Icon icon="logo" style="block lg:hidden ml-5 w-48" /> </a> </div> <!-- desktop nav --> <div - class="hidden space-x-10 md:flex flex-wrap justify-center items-center font-bold" + class="hidden space-x-10 lg:flex justify-center items-center font-bold" > {#if $page.url.pathname === "/"} <button class="hover:opacity-80" on:click={() => scroll(howItWorks)} @@ -76,13 +76,13 @@ <!-- mobile nav --> <button - class="block md:hidden absolute top-[34.5px] right-10 z" + class="block lg:hidden absolute top-[34.5px] right-10 z" on:click={() => (showMobileMenu = !showMobileMenu)} ><Icon icon={!showMobileMenu ? "menu" : "close"} /> </button> <div - class="container w-full px-10 md:hidden absolute top-0 {showMobileMenu + class="container w-full px-10 lg:hidden absolute top-0 {showMobileMenu ? 'right-0' : 'right-[-100%]'} transition-all ease-in-out duration-300 h-[100vh] w-full bg-white" > diff --git a/src/components/LocationsMap.svelte b/src/components/LocationsMap.svelte index 79ea4bf3..bd51b125 100644 --- a/src/components/LocationsMap.svelte +++ b/src/components/LocationsMap.svelte @@ -3,7 +3,7 @@ import { browser } from "$app/environment"; import Icon from "$comp/Icon.svelte"; import Popup from "$comp/Popup.svelte"; - import { shuffleArray } from "$lib/utils"; + import { back, shuffleArray } from "$lib/utils"; import "maplibre-gl/dist/maplibre-gl.css"; @@ -220,8 +220,16 @@ class="mx-auto h-full w-full z-0" bind:this={mapContainer} /> + <div class="absolute flex top-2 left-2 gap-2"> + <button + class="rounded-full border-2 border-black bg-white w-10 h-10" + on:click={back} + > + <Icon icon="arrow-left" style="w-4 m-auto" /> + </button> + </div> <div class="relative"> - <div class="flex absolute flex top-2 right-2 gap-2"> + <div class="absolute flex top-2 right-2 gap-2"> <button class="rounded-full border-2 border-black bg-white w-10 h-10" on:click={toggle} diff --git a/src/components/Mnemonic.svelte b/src/components/Mnemonic.svelte new file mode 100644 index 00000000..5e834aed --- /dev/null +++ b/src/components/Mnemonic.svelte @@ -0,0 +1,14 @@ +<script> + export let mnemonic; +</script> + +<div + class="text-lg border p-4 rounded-2xl text-secondary grid grid-cols-3 gap-2 justify-center" +> + {#each mnemonic.split(" ") as w, i} + <div> + <span class="select-none text-sm text-black">{i + 1}.</span> + <span>{@html w === "-" ? "—" : w}</span> + </div> + {/each} +</div> diff --git a/src/components/Numpad.svelte b/src/components/Numpad.svelte index 88b5b899..78322ff5 100644 --- a/src/components/Numpad.svelte +++ b/src/components/Numpad.svelte @@ -1,7 +1,7 @@ <script> import Left from "$comp/Left.svelte"; import Icon from "$comp/Icon.svelte"; - import { f, s, focus, post, warning, sat, sats } from "$lib/utils"; + import { f, s, focus, warning, sat, sats } from "$lib/utils"; import { t } from "$lib/translations"; export let amount, diff --git a/src/components/Request.svelte b/src/components/Request.svelte deleted file mode 100644 index c085f9e2..00000000 --- a/src/components/Request.svelte +++ /dev/null @@ -1,39 +0,0 @@ -<script> - import { t } from "$lib/translations"; - import { fly } from "svelte/transition"; - - import Avatar from "$comp/Avatar.svelte"; - import Icon from "$comp/Icon.svelte"; - - import { request, requestRedirect } from "$lib/store"; - import { page } from "$app/stores"; - - export let user; - - $requestRedirect = $page.url.pathname; -</script> - -{#if $request} - <div class="fixed bottom-0 border-t w-full p-4 bg-white z-50" in:fly> - <div class="flex flex-wrap md:flex-nowrap max-w-xl mx-auto gap-2"> - <div class="my-auto mx-auto"> - <div class="flex"> - <Avatar user={$request.requester} size={12} /> - <h1 class="my-auto"> - <b>{$request.requester.username}</b> - {$t("payments.readyToPay")} - </h1> - </div> - </div> - <div class="mx-auto my-auto flex gap-2"> - <a href={`/${user.username}/receive/${$request.id}`}> - <button - class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-32" - > - {$t("payments.invoice")} - </button> - </a> - </div> - </div> - </div> -{/if} diff --git a/src/components/WalletPass.svelte b/src/components/WalletPass.svelte new file mode 100644 index 00000000..75c94f34 --- /dev/null +++ b/src/components/WalletPass.svelte @@ -0,0 +1,65 @@ +<script> + import { focus } from "$lib/utils"; + import { t } from "$lib/translations"; + import Icon from "$comp/Icon.svelte"; + export let cancel, password, submit; + let revealPassword; +</script> + +<div + class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-20" +> + <div + class="relative top-1/3 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white space-y-5" + > + <h1 class="text-center text-2xl font-semibold"> + {$t("payments.enterWalletPass")} + </h1> + <form on:submit|preventDefault={submit}> + <div class="relative mb-5"> + {#if revealPassword} + <input + name="password" + type="text" + required + bind:value={password} + autocapitalize="none" + use:focus + /> + {:else} + <input + name="password" + type="password" + required + bind:value={password} + autocapitalize="none" + use:focus + /> + {/if} + <button + type="button" + on:click={() => (revealPassword = !revealPassword)} + class="absolute right-5 top-1/2" + > + <Icon icon={revealPassword ? "eye" : "eye-off"} /> + </button> + </div> + <div class="w-full flex justify-center gap-2"> + <button + type="button" + class="border rounded-2xl font-semibold mx-auto py-5 px-6 hover:opacity-80 mx-auto grow" + on:click={cancel} + on:keydown={cancel} + > + <div class="my-auto">Cancel</div> + </button> + <button + type="submit" + class="border rounded-2xl font-semibold mx-auto py-5 px-6 hover:opacity-80 mx-auto grow" + > + <div class="my-auto">Submit</div> + </button> + </div> + </form> + </div> +</div> diff --git a/src/lib/english.ts b/src/lib/english.ts deleted file mode 100644 index 52a0da82..00000000 --- a/src/lib/english.ts +++ /dev/null @@ -1,2048 +0,0 @@ -export default `abandon -ability -able -about -above -absent -absorb -abstract -absurd -abuse -access -accident -account -accuse -achieve -acid -acoustic -acquire -across -act -action -actor -actress -actual -adapt -add -addict -address -adjust -admit -adult -advance -advice -aerobic -affair -afford -afraid -again -age -agent -agree -ahead -aim -air -airport -aisle -alarm -album -alcohol -alert -alien -all -alley -allow -almost -alone -alpha -already -also -alter -always -amateur -amazing -among -amount -amused -analyst -anchor -ancient -anger -angle -angry -animal -ankle -announce -annual -another -answer -antenna -antique -anxiety -any -apart -apology -appear -apple -approve -april -arch -arctic -area -arena -argue -arm -armed -armor -army -around -arrange -arrest -arrive -arrow -art -artefact -artist -artwork -ask -aspect -assault -asset -assist -assume -asthma -athlete -atom -attack -attend -attitude -attract -auction -audit -august -aunt -author -auto -autumn -average -avocado -avoid -awake -aware -away -awesome -awful -awkward -axis -baby -bachelor -bacon -badge -bag -balance -balcony -ball -bamboo -banana -banner -bar -barely -bargain -barrel -base -basic -basket -battle -beach -bean -beauty -because -become -beef -before -begin -behave -behind -believe -below -belt -bench -benefit -best -betray -better -between -beyond -bicycle -bid -bike -bind -biology -bird -birth -bitter -black -blade -blame -blanket -blast -bleak -bless -blind -blood -blossom -blouse -blue -blur -blush -board -boat -body -boil -bomb -bone -bonus -book -boost -border -boring -borrow -boss -bottom -bounce -box -boy -bracket -brain -brand -brass -brave -bread -breeze -brick -bridge -brief -bright -bring -brisk -broccoli -broken -bronze -broom -brother -brown -brush -bubble -buddy -budget -buffalo -build -bulb -bulk -bullet -bundle -bunker -burden -burger -burst -bus -business -busy -butter -buyer -buzz -cabbage -cabin -cable -cactus -cage -cake -call -calm -camera -camp -can -canal -cancel -candy -cannon -canoe -canvas -canyon -capable -capital -captain -car -carbon -card -cargo -carpet -carry -cart -case -cash -casino -castle -casual -cat -catalog -catch -category -cattle -caught -cause -caution -cave -ceiling -celery -cement -census -century -cereal -certain -chair -chalk -champion -change -chaos -chapter -charge -chase -chat -cheap -check -cheese -chef -cherry -chest -chicken -chief -child -chimney -choice -choose -chronic -chuckle -chunk -churn -cigar -cinnamon -circle -citizen -city -civil -claim -clap -clarify -claw -clay -clean -clerk -clever -click -client -cliff -climb -clinic -clip -clock -clog -close -cloth -cloud -clown -club -clump -cluster -clutch -coach -coast -coconut -code -coffee -coil -coin -collect -color -column -combine -come -comfort -comic -common -company -concert -conduct -confirm -congress -connect -consider -control -convince -cook -cool -copper -copy -coral -core -corn -correct -cost -cotton -couch -country -couple -course -cousin -cover -coyote -crack -cradle -craft -cram -crane -crash -crater -crawl -crazy -cream -credit -creek -crew -cricket -crime -crisp -critic -crop -cross -crouch -crowd -crucial -cruel -cruise -crumble -crunch -crush -cry -crystal -cube -culture -cup -cupboard -curious -current -curtain -curve -cushion -custom -cute -cycle -dad -damage -damp -dance -danger -daring -dash -daughter -dawn -day -deal -debate -debris -decade -december -decide -decline -decorate -decrease -deer -defense -define -defy -degree -delay -deliver -demand -demise -denial -dentist -deny -depart -depend -deposit -depth -deputy -derive -describe -desert -design -desk -despair -destroy -detail -detect -develop -device -devote -diagram -dial -diamond -diary -dice -diesel -diet -differ -digital -dignity -dilemma -dinner -dinosaur -direct -dirt -disagree -discover -disease -dish -dismiss -disorder -display -distance -divert -divide -divorce -dizzy -doctor -document -dog -doll -dolphin -domain -donate -donkey -donor -door -dose -double -dove -draft -dragon -drama -drastic -draw -dream -dress -drift -drill -drink -drip -drive -drop -drum -dry -duck -dumb -dune -during -dust -dutch -duty -dwarf -dynamic -eager -eagle -early -earn -earth -easily -east -easy -echo -ecology -economy -edge -edit -educate -effort -egg -eight -either -elbow -elder -electric -elegant -element -elephant -elevator -elite -else -embark -embody -embrace -emerge -emotion -employ -empower -empty -enable -enact -end -endless -endorse -enemy -energy -enforce -engage -engine -enhance -enjoy -enlist -enough -enrich -enroll -ensure -enter -entire -entry -envelope -episode -equal -equip -era -erase -erode -erosion -error -erupt -escape -essay -essence -estate -eternal -ethics -evidence -evil -evoke -evolve -exact -example -excess -exchange -excite -exclude -excuse -execute -exercise -exhaust -exhibit -exile -exist -exit -exotic -expand -expect -expire -explain -expose -express -extend -extra -eye -eyebrow -fabric -face -faculty -fade -faint -faith -fall -false -fame -family -famous -fan -fancy -fantasy -farm -fashion -fat -fatal -father -fatigue -fault -favorite -feature -february -federal -fee -feed -feel -female -fence -festival -fetch -fever -few -fiber -fiction -field -figure -file -film -filter -final -find -fine -finger -finish -fire -firm -first -fiscal -fish -fit -fitness -fix -flag -flame -flash -flat -flavor -flee -flight -flip -float -flock -floor -flower -fluid -flush -fly -foam -focus -fog -foil -fold -follow -food -foot -force -forest -forget -fork -fortune -forum -forward -fossil -foster -found -fox -fragile -frame -frequent -fresh -friend -fringe -frog -front -frost -frown -frozen -fruit -fuel -fun -funny -furnace -fury -future -gadget -gain -galaxy -gallery -game -gap -garage -garbage -garden -garlic -garment -gas -gasp -gate -gather -gauge -gaze -general -genius -genre -gentle -genuine -gesture -ghost -giant -gift -giggle -ginger -giraffe -girl -give -glad -glance -glare -glass -glide -glimpse -globe -gloom -glory -glove -glow -glue -goat -goddess -gold -good -goose -gorilla -gospel -gossip -govern -gown -grab -grace -grain -grant -grape -grass -gravity -great -green -grid -grief -grit -grocery -group -grow -grunt -guard -guess -guide -guilt -guitar -gun -gym -habit -hair -half -hammer -hamster -hand -happy -harbor -hard -harsh -harvest -hat -have -hawk -hazard -head -health -heart -heavy -hedgehog -height -hello -helmet -help -hen -hero -hidden -high -hill -hint -hip -hire -history -hobby -hockey -hold -hole -holiday -hollow -home -honey -hood -hope -horn -horror -horse -hospital -host -hotel -hour -hover -hub -huge -human -humble -humor -hundred -hungry -hunt -hurdle -hurry -hurt -husband -hybrid -ice -icon -idea -identify -idle -ignore -ill -illegal -illness -image -imitate -immense -immune -impact -impose -improve -impulse -inch -include -income -increase -index -indicate -indoor -industry -infant -inflict -inform -inhale -inherit -initial -inject -injury -inmate -inner -innocent -input -inquiry -insane -insect -inside -inspire -install -intact -interest -into -invest -invite -involve -iron -island -isolate -issue -item -ivory -jacket -jaguar -jar -jazz -jealous -jeans -jelly -jewel -job -join -joke -journey -joy -judge -juice -jump -jungle -junior -junk -just -kangaroo -keen -keep -ketchup -key -kick -kid -kidney -kind -kingdom -kiss -kit -kitchen -kite -kitten -kiwi -knee -knife -knock -know -lab -label -labor -ladder -lady -lake -lamp -language -laptop -large -later -latin -laugh -laundry -lava -law -lawn -lawsuit -layer -lazy -leader -leaf -learn -leave -lecture -left -leg -legal -legend -leisure -lemon -lend -length -lens -leopard -lesson -letter -level -liar -liberty -library -license -life -lift -light -like -limb -limit -link -lion -liquid -list -little -live -lizard -load -loan -lobster -local -lock -logic -lonely -long -loop -lottery -loud -lounge -love -loyal -lucky -luggage -lumber -lunar -lunch -luxury -lyrics -machine -mad -magic -magnet -maid -mail -main -major -make -mammal -man -manage -mandate -mango -mansion -manual -maple -marble -march -margin -marine -market -marriage -mask -mass -master -match -material -math -matrix -matter -maximum -maze -meadow -mean -measure -meat -mechanic -medal -media -melody -melt -member -memory -mention -menu -mercy -merge -merit -merry -mesh -message -metal -method -middle -midnight -milk -million -mimic -mind -minimum -minor -minute -miracle -mirror -misery -miss -mistake -mix -mixed -mixture -mobile -model -modify -mom -moment -monitor -monkey -monster -month -moon -moral -more -morning -mosquito -mother -motion -motor -mountain -mouse -move -movie -much -muffin -mule -multiply -muscle -museum -mushroom -music -must -mutual -myself -mystery -myth -naive -name -napkin -narrow -nasty -nation -nature -near -neck -need -negative -neglect -neither -nephew -nerve -nest -net -network -neutral -never -news -next -nice -night -noble -noise -nominee -noodle -normal -north -nose -notable -note -nothing -notice -novel -now -nuclear -number -nurse -nut -oak -obey -object -oblige -obscure -observe -obtain -obvious -occur -ocean -october -odor -off -offer -office -often -oil -okay -old -olive -olympic -omit -once -one -onion -online -only -open -opera -opinion -oppose -option -orange -orbit -orchard -order -ordinary -organ -orient -original -orphan -ostrich -other -outdoor -outer -output -outside -oval -oven -over -own -owner -oxygen -oyster -ozone -pact -paddle -page -pair -palace -palm -panda -panel -panic -panther -paper -parade -parent -park -parrot -party -pass -patch -path -patient -patrol -pattern -pause -pave -payment -peace -peanut -pear -peasant -pelican -pen -penalty -pencil -people -pepper -perfect -permit -person -pet -phone -photo -phrase -physical -piano -picnic -picture -piece -pig -pigeon -pill -pilot -pink -pioneer -pipe -pistol -pitch -pizza -place -planet -plastic -plate -play -please -pledge -pluck -plug -plunge -poem -poet -point -polar -pole -police -pond -pony -pool -popular -portion -position -possible -post -potato -pottery -poverty -powder -power -practice -praise -predict -prefer -prepare -present -pretty -prevent -price -pride -primary -print -priority -prison -private -prize -problem -process -produce -profit -program -project -promote -proof -property -prosper -protect -proud -provide -public -pudding -pull -pulp -pulse -pumpkin -punch -pupil -puppy -purchase -purity -purpose -purse -push -put -puzzle -pyramid -quality -quantum -quarter -question -quick -quit -quiz -quote -rabbit -raccoon -race -rack -radar -radio -rail -rain -raise -rally -ramp -ranch -random -range -rapid -rare -rate -rather -raven -raw -razor -ready -real -reason -rebel -rebuild -recall -receive -recipe -record -recycle -reduce -reflect -reform -refuse -region -regret -regular -reject -relax -release -relief -rely -remain -remember -remind -remove -render -renew -rent -reopen -repair -repeat -replace -report -require -rescue -resemble -resist -resource -response -result -retire -retreat -return -reunion -reveal -review -reward -rhythm -rib -ribbon -rice -rich -ride -ridge -rifle -right -rigid -ring -riot -ripple -risk -ritual -rival -river -road -roast -robot -robust -rocket -romance -roof -rookie -room -rose -rotate -rough -round -route -royal -rubber -rude -rug -rule -run -runway -rural -sad -saddle -sadness -safe -sail -salad -salmon -salon -salt -salute -same -sample -sand -satisfy -satoshi -sauce -sausage -save -say -scale -scan -scare -scatter -scene -scheme -school -science -scissors -scorpion -scout -scrap -screen -script -scrub -sea -search -season -seat -second -secret -section -security -seed -seek -segment -select -sell -seminar -senior -sense -sentence -series -service -session -settle -setup -seven -shadow -shaft -shallow -share -shed -shell -sheriff -shield -shift -shine -ship -shiver -shock -shoe -shoot -shop -short -shoulder -shove -shrimp -shrug -shuffle -shy -sibling -sick -side -siege -sight -sign -silent -silk -silly -silver -similar -simple -since -sing -siren -sister -situate -six -size -skate -sketch -ski -skill -skin -skirt -skull -slab -slam -sleep -slender -slice -slide -slight -slim -slogan -slot -slow -slush -small -smart -smile -smoke -smooth -snack -snake -snap -sniff -snow -soap -soccer -social -sock -soda -soft -solar -soldier -solid -solution -solve -someone -song -soon -sorry -sort -soul -sound -soup -source -south -space -spare -spatial -spawn -speak -special -speed -spell -spend -sphere -spice -spider -spike -spin -spirit -split -spoil -sponsor -spoon -sport -spot -spray -spread -spring -spy -square -squeeze -squirrel -stable -stadium -staff -stage -stairs -stamp -stand -start -state -stay -steak -steel -stem -step -stereo -stick -still -sting -stock -stomach -stone -stool -story -stove -strategy -street -strike -strong -struggle -student -stuff -stumble -style -subject -submit -subway -success -such -sudden -suffer -sugar -suggest -suit -summer -sun -sunny -sunset -super -supply -supreme -sure -surface -surge -surprise -surround -survey -suspect -sustain -swallow -swamp -swap -swarm -swear -sweet -swift -swim -swing -switch -sword -symbol -symptom -syrup -system -table -tackle -tag -tail -talent -talk -tank -tape -target -task -taste -tattoo -taxi -teach -team -tell -ten -tenant -tennis -tent -term -test -text -thank -that -theme -then -theory -there -they -thing -this -thought -three -thrive -throw -thumb -thunder -ticket -tide -tiger -tilt -timber -time -tiny -tip -tired -tissue -title -toast -tobacco -today -toddler -toe -together -toilet -token -tomato -tomorrow -tone -tongue -tonight -tool -tooth -top -topic -topple -torch -tornado -tortoise -toss -total -tourist -toward -tower -town -toy -track -trade -traffic -tragic -train -transfer -trap -trash -travel -tray -treat -tree -trend -trial -tribe -trick -trigger -trim -trip -trophy -trouble -truck -true -truly -trumpet -trust -truth -try -tube -tuition -tumble -tuna -tunnel -turkey -turn -turtle -twelve -twenty -twice -twin -twist -two -type -typical -ugly -umbrella -unable -unaware -uncle -uncover -under -undo -unfair -unfold -unhappy -uniform -unique -unit -universe -unknown -unlock -until -unusual -unveil -update -upgrade -uphold -upon -upper -upset -urban -urge -usage -use -used -useful -useless -usual -utility -vacant -vacuum -vague -valid -valley -valve -van -vanish -vapor -various -vast -vault -vehicle -velvet -vendor -venture -venue -verb -verify -version -very -vessel -veteran -viable -vibrant -vicious -victory -video -view -village -vintage -violin -virtual -virus -visa -visit -visual -vital -vivid -vocal -voice -void -volcano -volume -vote -voyage -wage -wagon -wait -walk -wall -walnut -want -warfare -warm -warrior -wash -wasp -waste -water -wave -way -wealth -weapon -wear -weasel -weather -web -wedding -weekend -weird -welcome -west -wet -whale -what -wheat -wheel -when -where -whip -whisper -wide -width -wife -wild -will -win -window -wine -wing -wink -winner -winter -wire -wisdom -wise -wish -witness -wolf -woman -wonder -wood -wool -word -work -world -worry -worth -wrap -wreck -wrestle -wrist -write -wrong -yard -year -yellow -you -young -youth -zebra -zero -zone -zoo`.split("\n"); diff --git a/src/lib/invoice.ts b/src/lib/invoice.ts index 7897dadc..fa2f7786 100644 --- a/src/lib/invoice.ts +++ b/src/lib/invoice.ts @@ -2,12 +2,14 @@ import { auth, get, post } from "$lib/utils"; import { redirect } from "@sveltejs/kit"; export default async ({ cookies, request }) => { + let aid = cookies.get("aid"); let form = await request.formData(); let rates = await get("/rates"); let amount = parseInt(form.get("amount")); let invoice = { + aid, amount, items: JSON.parse(form.get("items")), memo: form.get("memo"), @@ -16,7 +18,7 @@ export default async ({ cookies, request }) => { type: form.get("type"), prompt: form.get("prompt") === "true", rate: parseFloat(form.get("rate")) || rates[form.get("currency")], - id: undefined + id: undefined, }; let user = { username: form.get("username"), currency: form.get("currency") }; @@ -25,9 +27,9 @@ export default async ({ cookies, request }) => { let { id } = invoice; if (invoice.prompt && invoice.tip === null) - redirect(307, `/${user.username}/invoice/${id}/tip`); + redirect(307, `/invoice/${id}/tip`); if (invoice.memoPrompt && !invoice.memo) { - redirect(307, `/${user.username}/invoice/${id}/memo`); - } else redirect(307, `/${user.username}/invoice/${id}`); + redirect(307, `/invoice/${id}/memo`); + } else redirect(307, `/invoice/${id}`); }; diff --git a/src/lib/nostr.ts b/src/lib/nostr.ts index 1a02114d..62853f09 100644 --- a/src/lib/nostr.ts +++ b/src/lib/nostr.ts @@ -18,8 +18,8 @@ import { } from "nostr-tools/nip49"; import { mnemonicToEntropy, entropyToMnemonic } from "@scure/bip39"; -import wordlist from "$lib/english"; import { bech32m } from "@scure/base"; +import { wordlist } from "@scure/bip39/wordlists/english"; import { generateSeedWords, privateKeyFromSeedWords } from "nostr-tools/nip06"; diff --git a/src/lib/parse.ts b/src/lib/parse.ts index 6995f207..0ab3e809 100644 --- a/src/lib/parse.ts +++ b/src/lib/parse.ts @@ -10,7 +10,7 @@ export default async (t, host) => { if (t.startsWith("http")) redirect(307, t); if (t.startsWith(host)) redirect(307, `http://${t}`); - let amount, id, user; + let amount, user; t = t.trim(); t.toLowerCase().startsWith("bitcoin:") && @@ -37,7 +37,7 @@ export default async (t, host) => { if (t.toLowerCase().startsWith("ln")) { try { - ({ id } = await get(`/invoice/${t}`)); + await get(`/invoice/${t}`); } catch (e) { redirect(307, `/send/lightning/${t}`); } @@ -45,16 +45,9 @@ export default async (t, host) => { // bitcoin if (validate(t) || isLiquid(t)) { - try { - ({ id, user } = await get(`/invoice/${t}`)); - } catch (e) { - let r = `/send/bitcoin/${t}`; - if (amount) r += "/" + amount; - redirect(307, r); - } - - if (user) redirect(307, `/pay/${user.username}`); - else if (id) redirect(307, `/send/${id}`); + let r = `/send/bitcoin/${t}`; + if (amount) r += "/" + amount; + redirect(307, r); } if (t.startsWith("cashu")) { diff --git a/src/lib/store.ts b/src/lib/store.ts index 7609a67c..42b41127 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -38,6 +38,7 @@ let persistLocal = (key, defaultValue: any = undefined) => { return s; }; +export let account = writable(); export let amountPrompt = persistLocal("amountPrompt"); export let avatar = writable(); export let banner = writable(); @@ -48,6 +49,7 @@ export let invoice = writable(); export let last = writable(); export let loginRedirect = writable(); export let decrypted = persistLocal("decrypted", {}); +export let mnemonic = persistSession("mnemonic"); export let newPayment = persistLocal("newPayment"); export let password = writable<string | undefined>(); export let passwordPrompt = writable(); diff --git a/src/lib/types.ts b/src/lib/types.ts index 33f78c85..827f96aa 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -14,9 +14,10 @@ export interface User { } export interface Invoice { + aid: string; id?: string; memoPrompt?: boolean; memo?: string; rate: number; - type: string; + type?: string; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 69e0f949..0997eeb9 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,7 +1,7 @@ import { browser } from "$app/environment"; import { toast } from "@zerodevx/svelte-toast"; import { goto } from "$app/navigation"; -import { PUBLIC_COINOS_URL } from "$env/static/public"; +import { PUBLIC_COINOS_NETWORK, PUBLIC_COINOS_URL } from "$env/static/public"; import { page } from "$app/stores"; import { get as getStore } from "svelte/store"; @@ -41,7 +41,11 @@ export let get = (url: string, headers = {}) => { }); }; -export let post = async (url: string, body: object, headers: HeadersInit | undefined = undefined) => { +export let post = async ( + url: string, + body: object, + headers: HeadersInit | undefined = undefined +) => { headers = { "content-type": "application/json", accept: "application/json", @@ -242,7 +246,11 @@ export let select = (el: HTMLInputElement): any => export let sleep = (n: number): Promise<void> => new Promise((r) => setTimeout(r, n)); -export let wait = async (f: () => boolean | Promise<boolean>, n: number = 100, s: number = 300): Promise<boolean> => { +export let wait = async ( + f: () => boolean | Promise<boolean>, + n: number = 100, + s: number = 300 +): Promise<boolean> => { let i = 0; while (!(await f()) && i < s) { await sleep(n); @@ -279,6 +287,7 @@ export let types = { internal: "internal", fund: "fund", ecash: "ecash", + reconcile: "reconcile", }; export let ease = (t: number): number => @@ -327,3 +336,30 @@ export function setCookie(name: string, value: string, seconds: number): void { const expires = "expires=" + now.toUTCString(); document.cookie = name + "=" + value + ";" + expires + ";path=/"; } + +export let network = { + bitcoin: { + bech32: "bc", + pubKeyHash: 0x00, + scriptHash: 0x05, + wif: 0x80, + }, + regtest: { + bech32: "bcrt", + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, + }, +}[PUBLIC_COINOS_NETWORK]; + +export let versions = { + bitcoin: { + private: 0x0488ade4, + public: 0x0488b21e, + }, + regtest: { + private: 0x04358394, + public: 0x043587cf, + }, +}[PUBLIC_COINOS_NETWORK]; + diff --git a/src/locales/de.json b/src/locales/de.json index 906d85bb..8aa46733 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -20,7 +20,8 @@ }, "locations": { "header": "Standorte", - "description": "Hier finden Sie einige Unternehmen, welche bereits Zahlungen mit Bitcoin über Coinos akzeptieren. Wir hoffen, dass Ihr Unternehmen das nächste sein wird!" + "description": "Hier finden Sie einige Unternehmen, welche bereits Zahlungen mit Bitcoin über Coinos akzeptieren. Wir hoffen, dass Ihr Unternehmen das nächste sein wird!", + "viewMap": "Karte anzeigen" } }, "error": { @@ -78,7 +79,8 @@ "twitter": "Twitter", "news": "Neuigkeiten", "linktree": "Linktree", - "documentation": "Dokumentation" + "documentation": "Dokumentation", + "nostr": "Nostr" }, "howItWorks": { "header": "So funktioniert's", @@ -149,7 +151,9 @@ "forgot": "Passwort vergessen?", "reset": "Passwort zurücksetzen", "submit": "Einreichen", - "newPassword": "Neues Kennwort" + "newPassword": "Neues Kennwort", + "createAccount": "Benutzerkonto erstellen", + "signUp": "Melden Sie sich an" }, "nav": { "profile": "Profil", @@ -199,7 +203,8 @@ "deposit": "Einzahlung", "voucher": "Beleg", "checkout": "Kasse", - "payNow": "Zahlen Sie jetzt" + "payNow": "Zahlen Sie jetzt", + "setup": "Selbstverwahrung einrichten" }, "receive": { "lessThan1BTCWarning": "Bitte geben Sie einen Betrag unter 1 Bitcoin ein.", @@ -314,7 +319,9 @@ "details": "Einzelheiten", "noMessages": "Sie haben noch keine Nachrichten gesendet oder empfangen", "install": "App installieren", - "moreDetails": "Links und Details" + "moreDetails": "Links und Details", + "url": "Öffentliche URL", + "nwc": "Nostr-Wallet verbinden" }, "payments": { "header": "Transaktionen", @@ -409,7 +416,8 @@ "showText": "Text anzeigen", "showQr": "QR anzeigen", "copyPr": "Lohnanforderung kopieren", - "amountPrompt": "Immer nach dem Betrag fragen" + "amountPrompt": "Immer nach dem Betrag fragen", + "enterWalletPass": "Wallet-Passwort" }, "items": { "add": "Artikel hinzufügen", @@ -418,5 +426,28 @@ "price": "Preis", "submit": "Einreichen", "image": "Bild" + }, + "accounts": { + "back": "Zurück", + "next": "Nächste", + "seedPhrase": "Seed-Phrase", + "save": "Speichern", + "copy": "Kopie", + "revealMnemonic": "Seed-Phrase anzeigen", + "regenerate": "Regenerieren", + "deleteAccount": "Konto löschen", + "addAccount": "Konto hinzufügen", + "create": "Erstellen", + "import": "Import", + "accountSetup": "Kontoeinrichtung", + "enterSeed": "Geben Sie eine Seed-Phrase ein", + "passwordMismatch": "Passwörter stimmen nicht überein", + "generateSeed": "Generieren Sie eine neue Seed-Phrase in Ihrem Browser", + "accountPassword": "Kontokennwort", + "importSeed": "Geben Sie eine vorhandene Seed-Phrase ein", + "passwordWarning": "Wenn Sie dieses Passwort verlieren, können Sie weder Geld vom Konto abheben noch Ihre Seed-Phrase wiederherstellen. Wir können es nicht für Sie wiederherstellen.", + "savings": "Ersparnisse", + "password": "Passwort", + "confirmPassword": "Passwort bestätigen" } } diff --git a/src/locales/el.json b/src/locales/el.json index d876b26b..c2a2296d 100644 --- a/src/locales/el.json +++ b/src/locales/el.json @@ -20,7 +20,8 @@ }, "locations": { "header": "Τοποθεσίες", - "description": "Ακολουθούν ορισμένες επιχειρήσεις που δέχονται bitcoin με Coinos. Ελπίζουμε ότι η δική σας θα είναι η επόμενη!" + "description": "Ακολουθούν ορισμένες επιχειρήσεις που δέχονται bitcoin με Coinos. Ελπίζουμε ότι η δική σας θα είναι η επόμενη!", + "viewMap": "Προβολή χάρτη" } }, "error": { @@ -77,7 +78,8 @@ "support": "Υποστήριξη", "telegram": "Telegram", "twitter": "Twitter", - "documentation": "Τεκμηρίωση" + "documentation": "Τεκμηρίωση", + "nostr": "Nostr" }, "howItWorks": { "header": "Πως δουλεύει", @@ -148,7 +150,9 @@ "forgot": "Ξεχάσατε τον κωδικό?", "reset": "ΕΠΑΝΑΦΟΡΑ ΚΩΔΙΚΟΥ", "submit": "υποβάλλουν", - "newPassword": "Νέος Κωδικός" + "newPassword": "Νέος Κωδικός", + "createAccount": "Δημιουργία λογαριασμού", + "signUp": "Εγγραφείτε" }, "nav": { "profile": "Προφίλ", @@ -201,7 +205,8 @@ "voucher": "Κουπόν", "fetchingRate": "ποσοστό ανάκτησης...", "checkout": "Ολοκλήρωση αγοράς", - "payNow": "Πλήρωσε τώρα" + "payNow": "Πλήρωσε τώρα", + "setup": "Ρύθμιση αυτοεπιμέλειας" }, "receive": { "lessThan1BTCWarning": "Παρακαλώ εισάγετε ένα ποσό μικρότερο από 1 bitcoin ", @@ -308,7 +313,9 @@ "details": "Λεπτομέριες", "noMessages": "Δεν έχετε στείλει ή λάβει ακόμη μηνύματα", "install": "Εγκαταστήστε την εφαρμογή", - "moreDetails": "Σύνδεσμοι και λεπτομέρειες" + "moreDetails": "Σύνδεσμοι και λεπτομέρειες", + "url": "Δημόσια διεύθυνση URL", + "nwc": "Σύνδεση πορτοφολιού Nostr" }, "payments": { "amount": "Ποσό", @@ -400,7 +407,8 @@ "showText": "Εμφάνιση κειμένου", "showQr": "Εμφάνιση QR", "copyPr": "Αντιγραφή αιτήματος πληρωμής", - "amountPrompt": "Πάντα προτροπή για ποσό" + "amountPrompt": "Πάντα προτροπή για ποσό", + "enterWalletPass": "Κωδικός πρόσβασης πορτοφολιού" }, "items": { "add": "Πρόσθεσε είδος", @@ -409,5 +417,28 @@ "price": "Τιμή", "submit": "υποβάλλουν", "image": "Εικόνα" + }, + "accounts": { + "back": "Πίσω", + "next": "Επόμενος", + "seedPhrase": "Φράση σπόρων", + "save": "Εκτός", + "copy": "Αντίγραφο", + "revealMnemonic": "Αποκαλύψτε τη φράση σπόρων", + "regenerate": "Αναγεννηθείς", + "deleteAccount": "Διαγραφή λογαριασμού", + "addAccount": "Προσθήκη λογαριασμού", + "create": "Δημιουργώ", + "import": "Εισαγωγή", + "accountSetup": "Ρύθμιση λογαριασμού", + "enterSeed": "Εισαγάγετε μια αρχική φράση", + "passwordMismatch": "Οι κωδικοί πρόσβασης δεν ταιριάζουν", + "generateSeed": "Δημιουργήστε μια νέα αρχική φράση στο πρόγραμμα περιήγησής σας", + "accountPassword": "Κωδικός πρόσβασης λογαριασμού", + "importSeed": "Εισαγάγετε μια υπάρχουσα αρχική φράση", + "passwordWarning": "Εάν χάσετε αυτόν τον κωδικό πρόσβασης, δεν θα μπορείτε να κάνετε ανάληψη χρημάτων από τον λογαριασμό ή να επαναφέρετε τη φράση αρχικής σας. Δεν μπορούμε να σας το ανακτήσουμε.", + "savings": "Οικονομίες", + "password": "Σύνθημα", + "confirmPassword": "Επιβεβαίωση κωδικού πρόσβασης" } } diff --git a/src/locales/en.json b/src/locales/en.json index 29929c6b..35a75db2 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,4 +1,27 @@ { + "accounts": { + "back": "Back", + "next": "Next", + "seedPhrase": "Seed phrase", + "save": "Save", + "copy": "Copy", + "revealMnemonic": "Reveal seed phrase", + "regenerate": "Regenerate", + "deleteAccount": "Delete account", + "addAccount": "Add account", + "create": "Create", + "import": "Import", + "accountSetup": "Account setup", + "enterSeed": "Enter a seed phrase", + "passwordMismatch": "Passwords don't match", + "generateSeed": "Generate a new seed phrase in your browser", + "accountPassword": "Account password", + "importSeed": "Enter an existing seed phrase", + "passwordWarning": "If you lose this password you won't be able to withdraw funds from the account or restore your seed phrase. We can't recover it for you.", + "savings": "Savings", + "password": "Password", + "confirmPassword": "Confirm password" + }, "404": { "404": "404", "pageNotFound": "Page not found." @@ -19,6 +42,7 @@ } }, "locations": { + "viewMap": "View map", "header": "Locations", "description": "Over 500 businesses worldwide are accepting bitcoin with Coinos. We hope that yours will be next!" } @@ -61,6 +85,7 @@ "support": "Support", "telegram": "Telegram", "twitter": "Twitter", + "nostr": "Nostr", "documentation": "Documentation" }, "howItWorks": { @@ -131,7 +156,9 @@ "haveAccount": "Already have an account?", "noAccount": "Don't have an account?", "register": "Register", + "createAccount": "Create account", "signIn": "Sign In", + "signUp": "Sign Up", "failed": "Login failed" }, "nav": { @@ -140,7 +167,7 @@ "messages": "Messages", "funds": "Funds", "map": "Map", - "merch": "Swag", + "merch": "Shop", "profile": "Profile", "startInSeconds": "Start in Seconds", "account": "Account", @@ -155,6 +182,7 @@ "subheader": "Latest Updates" }, "user": { + "url": "Public URL", "install": "Install app", "moreDetails": "Profile details", "noMessages": "You have not sent or received any messages", @@ -182,6 +210,7 @@ "this": "This", "lightningAddress": "Lightning address", "dashboard": { + "setup": "Setup self-custody", "payNow": "Pay now", "checkout": "Checkout", "go": "Go", @@ -294,6 +323,7 @@ } }, "payments": { + "enterWalletPass": "Wallet password", "legacyVersion": "Legacy version", "newVersion": "New version", "income": "Income", @@ -303,8 +333,8 @@ "mint": "Mint", "ecash": "Ecash", "memo": "Notes", - "voucherText": "Enter an ecash or lnurlw code", - "createEcash": "Create ecash", + "voucherText": "Enter an ecash token or lnurlw code", + "createEcash": "Mint ecash", "tips": "Tips", "options": "options", "bump": "Bump fee", @@ -325,15 +355,14 @@ "voucher": "Payment code", "qr": "QR", "copy": "Copy", - "copyPr": "Copy pay request", - "amountPrompt": "Always prompt for amount", + "amountPrompt": "Prompt for amount", "shareLink": "Share link", "copyLink": "Copy link", "copyText": "Copy text", "openLink": "Open wallet", "setAmount": "Set amount", "moreOptions": "More options", - "redeemVoucher": "Ecash or LNURLw", + "redeemVoucher": "Ecash", "total": "Total", "gainLoss": "Gain/Loss", "presentValue": "Present Value", diff --git a/src/locales/es.json b/src/locales/es.json index 9782bf8a..9b5e436f 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -20,7 +20,8 @@ }, "locations": { "header": "Ubicaciones", - "description": "Aquí hay algunos negocios que aceptan bitcoin con Coinos. ¡Esperamos que el tuyo sea el siguiente!" + "description": "Aquí hay algunos negocios que aceptan bitcoin con Coinos. ¡Esperamos que el tuyo sea el siguiente!", + "viewMap": "Ver mapa" } }, "error": { @@ -61,7 +62,8 @@ "support": "Soporte", "telegram": "Telegram", "twitter": "Twitter", - "documentation": "Documentación" + "documentation": "Documentación", + "nostr": "Nostálgico" }, "howItWorks": { "header": "Cómo Funciona", @@ -132,7 +134,9 @@ "noAccount": "¿No tienes una cuenta?", "register": "Registrarse", "signIn": "Iniciar Sesión", - "failed": "Inicio de sesión fallido" + "failed": "Inicio de sesión fallido", + "createAccount": "Crear una cuenta", + "signUp": "Inscribirse" }, "nav": { "about": "Acerca de", @@ -189,7 +193,8 @@ "go": "Ir", "dashboard": "Panel", "checkout": "Verificar", - "payNow": "Pagar ahora" + "payNow": "Pagar ahora", + "setup": "Configurar la autocustodia" }, "receive": { "lessThan1BTCWarning": "Por favor, introduzca una cantidad menor a 1 bitcoin.", @@ -291,7 +296,9 @@ "details": "Detalles", "noMessages": "Aún no has enviado ni recibido ningún mensaje", "install": "Instalar aplicación", - "moreDetails": "Enlaces y detalles" + "moreDetails": "Enlaces y detalles", + "url": "URL pública", + "nwc": "Conexión de billetera Nostr" }, "payments": { "networkFee": "Tarifa de Red", @@ -382,7 +389,8 @@ "showText": "Mostrar texto", "showQr": "Mostrar código QR", "copyPr": "Copiar solicitud de pago", - "amountPrompt": "Siempre solicite la cantidad" + "amountPrompt": "Siempre solicite la cantidad", + "enterWalletPass": "Contraseña de la billetera" }, "items": { "add": "Añadir artículo", @@ -391,5 +399,28 @@ "price": "Precio", "submit": "Entregar", "image": "Imagen" + }, + "accounts": { + "back": "Atrás", + "next": "Próximo", + "seedPhrase": "Frase semilla", + "save": "Ahorrar", + "copy": "Copiar", + "revealMnemonic": "Revelar frase semilla", + "regenerate": "Regenerado", + "deleteAccount": "Eliminar cuenta", + "addAccount": "Agregar cuenta", + "create": "Crear", + "import": "Importar", + "accountSetup": "Configuración de cuenta", + "enterSeed": "Introduzca una frase inicial", + "passwordMismatch": "Las contraseñas no coinciden", + "generateSeed": "Genera una nueva frase semilla en tu navegador", + "accountPassword": "Contraseña de la cuenta", + "importSeed": "Introduzca una frase semilla existente", + "passwordWarning": "Si pierde esta contraseña, no podrá retirar fondos de la cuenta ni recuperar su frase inicial. No podemos recuperarla por usted.", + "savings": "Ahorros", + "password": "Contraseña", + "confirmPassword": "Confirmar Contraseña" } } diff --git a/src/locales/fa.json b/src/locales/fa.json index d091c028..b40f581c 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -20,7 +20,8 @@ }, "locations": { "header": "مکان ها", - "description": "در اینجا چند کسب و کار وجود دارد که بیت کوین را با کوینوس می پذیرند. امیدواریم مال بعدی شما باشد!" + "description": "در اینجا چند کسب و کار وجود دارد که بیت کوین را با کوینوس می پذیرند. امیدواریم مال بعدی شما باشد!", + "viewMap": "مشاهده نقشه" } }, "error": { @@ -61,7 +62,8 @@ "support": "حمایت کردن", "telegram": "تلگرام", "twitter": "توییتر", - "documentation": "مستندات" + "documentation": "مستندات", + "nostr": "Nostr" }, "howItWorks": { "header": "چگونه کار می کند", @@ -132,7 +134,9 @@ "noAccount": "حساب کاربری ندارید؟", "register": "ثبت نام", "signIn": "ورود", - "failed": "ورود ناموفق بود" + "failed": "ورود ناموفق بود", + "createAccount": "ایجاد حساب کاربری", + "signUp": "ثبت نام کنید" }, "nav": { "about": "در باره", @@ -189,7 +193,8 @@ "deposit": "سپرده", "voucher": "کوپن", "checkout": "وارسی", - "payNow": "الان پرداخت کن" + "payNow": "الان پرداخت کن", + "setup": "خود سرپرستی را تنظیم کنید" }, "receive": { "lessThan1BTCWarning": "لطفا مقدار کمتر از 1 بیت کوین را وارد کنید.", @@ -291,7 +296,9 @@ "details": "جزئیات", "noMessages": "شما هنوز هیچ پیامی ارسال یا دریافت نکرده اید", "install": "برنامه را نصب کنید", - "moreDetails": "لینک ها و جزئیات" + "moreDetails": "لینک ها و جزئیات", + "url": "URL عمومی", + "nwc": "اتصال کیف پول Nostr" }, "payments": { "networkFee": "هزینه شبکه", @@ -382,7 +389,8 @@ "showText": "نمایش متن", "showQr": "نمایش QR", "copyPr": "کپی درخواست پرداخت", - "amountPrompt": "همیشه مقدار را درخواست کنید" + "amountPrompt": "همیشه مقدار را درخواست کنید", + "enterWalletPass": "رمز کیف پول" }, "items": { "add": "این مورد را اضافه کنید", @@ -391,5 +399,28 @@ "price": "قیمت", "submit": "ارسال", "image": "تصویر" + }, + "accounts": { + "back": "برگشت", + "next": "بعدی", + "seedPhrase": "عبارت بذر", + "save": "ذخیره کنید", + "copy": "کپی کنید", + "revealMnemonic": "عبارت بذر را آشکار کنید", + "regenerate": "بازسازی کنید", + "deleteAccount": "حذف اکانت", + "addAccount": "اضافه کردن حساب", + "create": "ایجاد کنید", + "import": "واردات", + "accountSetup": "تنظیم حساب", + "enterSeed": "یک عبارت بذری وارد کنید", + "passwordMismatch": "گذرواژه ها مطابقت ندارند", + "generateSeed": "یک عبارت seed جدید در مرورگر خود ایجاد کنید", + "accountPassword": "رمز عبور حساب", + "importSeed": "عبارت seed موجود را وارد کنید", + "passwordWarning": "اگر این رمز عبور را گم کنید، نمی توانید وجوهی را از حساب برداشت کنید یا عبارت اولیه خود را بازیابی کنید. ما نمی توانیم آن را برای شما بازیابی کنیم.", + "savings": "پس انداز", + "password": "رمز عبور", + "confirmPassword": "رمز عبور را تایید کنید" } } diff --git a/src/locales/fr.json b/src/locales/fr.json index 08eedd0f..43f7ce4d 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -7,7 +7,8 @@ "header": "À Propos", "locations": { "header": "Établissements", - "description": "Il y a des entreprises qui acceptent bitcoin utilisant le point de vente coinos. Si vous utilisez notre appli, s'il vous plait <a href=\"mailto:[email protected]\" class=\"underline underline-offset-4\">fais-le-nous savoir!</a>" + "description": "Il y a des entreprises qui acceptent bitcoin utilisant le point de vente coinos. Si vous utilisez notre appli, s'il vous plait <a href=\"mailto:[email protected]\" class=\"underline underline-offset-4\">fais-le-nous savoir!</a>", + "viewMap": "Voir la carte" }, "team": { "header": "L'Équipe", @@ -61,7 +62,8 @@ "support": "Assistance", "jobs": "Jobs", "linktree": "Linktree", - "documentation": "Documentation" + "documentation": "Documentation", + "nostr": "Noster" }, "howItWorks": { "header": "Comment cela Fonctionne", @@ -114,7 +116,9 @@ "forgot": "Mot de passe oublié?", "reset": "Reinitialiser le mot de passe", "submit": "Soumettre", - "newPassword": "Nouveau mot de passe" + "newPassword": "Nouveau mot de passe", + "createAccount": "Créer un compte", + "signUp": "S'inscrire" }, "nav": { "startInSeconds": "Commence en Secondes", @@ -166,7 +170,8 @@ "deposit": "Déposer", "voucher": "Bon", "checkout": "Vérifier", - "payNow": "Payez maintenant" + "payNow": "Payez maintenant", + "setup": "Mettre en place l'auto-garde" }, "receive": { "lessThan1BTCWarning": "S'il vous plaît utilisez moins d'un bitcoin.", @@ -287,7 +292,9 @@ "details": "Détails", "noMessages": "Vous n'avez pas encore envoyé ou reçu de messages", "install": "Installer l'application", - "moreDetails": "Liens et détails" + "moreDetails": "Liens et détails", + "url": "URL publique", + "nwc": "Connexion au portefeuille Nostr" }, "news": { "header": "Nouvelles", @@ -382,7 +389,8 @@ "showText": "Afficher le texte", "showQr": "Afficher le QR", "copyPr": "Copie de la demande de paiement", - "amountPrompt": "Toujours demander le montant" + "amountPrompt": "Toujours demander le montant", + "enterWalletPass": "Mot de passe du portefeuille" }, "items": { "add": "Ajouter un item", @@ -391,5 +399,28 @@ "price": "Prix", "submit": "Soumettre", "image": "Image" + }, + "accounts": { + "back": "Dos", + "next": "Suivant", + "seedPhrase": "Phrase de départ", + "save": "Sauvegarder", + "copy": "Copie", + "revealMnemonic": "Révéler la phrase de départ", + "regenerate": "Régénérer", + "deleteAccount": "Supprimer le compte", + "addAccount": "Ajouter un compte", + "create": "Créer", + "import": "Importer", + "accountSetup": "Configuration du compte", + "enterSeed": "Entrez une phrase de départ", + "passwordMismatch": "Les mots de passe ne correspondent pas", + "generateSeed": "Générez une nouvelle phrase de départ dans votre navigateur", + "accountPassword": "Mot de passe du compte", + "importSeed": "Entrez une phrase de départ existante", + "passwordWarning": "Si vous perdez ce mot de passe, vous ne pourrez pas retirer de fonds du compte ni restaurer votre phrase de départ. Nous ne pouvons pas le récupérer pour vous.", + "savings": "Économies", + "password": "Mot de passe", + "confirmPassword": "Confirmez le mot de passe" } } diff --git a/src/locales/ja.json b/src/locales/ja.json index 3d9f539b..c5a0e275 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -20,7 +20,8 @@ }, "locations": { "header": "所在地", - "description": "Coinosでビットコインを受け入れている企業をいくつか紹介します。次はあなたのものになることを願っています!" + "description": "Coinosでビットコインを受け入れている企業をいくつか紹介します。次はあなたのものになることを願っています!", + "viewMap": "地図を見る" } }, "error": { @@ -61,7 +62,8 @@ "support": "サポート", "telegram": "電報", "twitter": "ツイッター", - "documentation": "ドキュメンテーション" + "documentation": "ドキュメンテーション", + "nostr": "ノストル" }, "howItWorks": { "header": "使い方", @@ -132,7 +134,9 @@ "noAccount": "アカウントをお持ちでない場合は、", "register": "登録する", "signIn": "サインイン", - "failed": "ログインに失敗しました" + "failed": "ログインに失敗しました", + "createAccount": "アカウントを作成する", + "signUp": "サインアップ" }, "nav": { "about": "について", @@ -189,7 +193,8 @@ "deposit": "デポジット", "voucher": "バウチャー", "checkout": "チェックアウト", - "payNow": "今払う" + "payNow": "今払う", + "setup": "自己管理の設定" }, "receive": { "lessThan1BTCWarning": "1 ビットコイン未満の金額を入力してください。", @@ -291,7 +296,9 @@ "details": "詳細", "noMessages": "まだメッセージを送信または受信していません", "install": "アプリをインストールする", - "moreDetails": "リンクと詳細" + "moreDetails": "リンクと詳細", + "url": "公開URL", + "nwc": "Nostrウォレットコネクト" }, "payments": { "networkFee": "ネットワーク料金", @@ -382,7 +389,8 @@ "showText": "テキストを表示", "showQr": "QRコードを表示", "copyPr": "支払い請求のコピー", - "amountPrompt": "常に金額の入力を求める" + "amountPrompt": "常に金額の入力を求める", + "enterWalletPass": "ウォレットパスワード" }, "items": { "add": "アイテムの追加", @@ -391,5 +399,28 @@ "price": "価格", "submit": "提出する", "image": "画像" + }, + "accounts": { + "back": "戻る", + "next": "次", + "seedPhrase": "シードフレーズ", + "save": "保存", + "copy": "コピー", + "revealMnemonic": "シードフレーズを表示", + "regenerate": "再生する", + "deleteAccount": "アカウントを削除", + "addAccount": "アカウントを追加", + "create": "作成する", + "import": "輸入", + "accountSetup": "アカウント設定", + "enterSeed": "シードフレーズを入力してください", + "passwordMismatch": "パスワードが一致しません", + "generateSeed": "ブラウザで新しいシードフレーズを生成する", + "accountPassword": "アカウントパスワード", + "importSeed": "既存のシードフレーズを入力してください", + "passwordWarning": "このパスワードを紛失した場合、アカウントから資金を引き出したり、シードフレーズを復元したりできなくなります。弊社では回復することはできません。", + "savings": "貯蓄", + "password": "パスワード", + "confirmPassword": "パスワードを認証する" } } diff --git a/src/locales/pt.json b/src/locales/pt.json index 327167c8..9618447b 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -20,7 +20,8 @@ }, "locations": { "header": "Locais", - "description": "Aqui estão algumas empresas que estão aceitando bitcoin com a Coinos. Esperamos que o seu seja o próximo!" + "description": "Aqui estão algumas empresas que estão aceitando bitcoin com a Coinos. Esperamos que o seu seja o próximo!", + "viewMap": "Ver mapa" } }, "error": { @@ -77,7 +78,8 @@ "support": "Suporte", "telegram": "Telegram", "twitter": "Twitter", - "documentation": "Documentação" + "documentation": "Documentação", + "nostr": "Nostr" }, "howItWorks": { "header": "Como funciona", @@ -148,7 +150,9 @@ "noAccount": "Don't have an account?", "register": "Register", "signIn": "Sign In", - "failed": "Login failed" + "failed": "Login failed", + "createAccount": "Criar uma conta", + "signUp": "Inscrever-se" }, "nav": { "about": "About", @@ -208,7 +212,8 @@ "deposit": "Deposit", "voucher": "Voucher", "checkout": "Confira", - "payNow": "Pague agora" + "payNow": "Pague agora", + "setup": "Configurar autocustódia" }, "receive": { "lessThan1BTCWarning": "Favor inserir uma quantidade inferior a 1 bitcoin.", @@ -312,7 +317,9 @@ "details": "Detalhes", "noMessages": "Você ainda não enviou ou recebeu nenhuma mensagem", "install": "Instalar aplicativo", - "moreDetails": "Links e detalhes" + "moreDetails": "Links e detalhes", + "url": "URL pública", + "nwc": "Nostr carteira conectar" }, "payments": { "amount": "Quantidade", @@ -404,7 +411,8 @@ "showText": "Mostrar texto", "showQr": "Mostrar QR", "copyPr": "Copiar solicitação de pagamento", - "amountPrompt": "Sempre solicite o valor" + "amountPrompt": "Sempre solicite o valor", + "enterWalletPass": "Senha da carteira" }, "items": { "add": "Adicionar Item", @@ -413,5 +421,28 @@ "price": "Preço", "submit": "Enviar", "image": "Imagem" + }, + "accounts": { + "back": "Voltar", + "next": "Próximo", + "seedPhrase": "Frase semente", + "save": "Salvar", + "copy": "Cópia", + "revealMnemonic": "Revelar frase semente", + "regenerate": "Regenerado", + "deleteAccount": "Excluir conta", + "addAccount": "Adicionar conta", + "create": "Criar", + "import": "Importar", + "accountSetup": "Configuração da conta", + "enterSeed": "Insira uma frase semente", + "passwordMismatch": "As senhas não correspondem", + "generateSeed": "Gere uma nova frase semente no seu navegador", + "accountPassword": "Senha da conta", + "importSeed": "Insira uma frase semente existente", + "passwordWarning": "Se você perder essa senha, não poderá sacar fundos da conta ou restaurar sua seed phrase. Não podemos recuperá-la para você.", + "savings": "Poupança", + "password": "Senha", + "confirmPassword": "Confirme sua senha" } } diff --git a/src/locales/ru.json b/src/locales/ru.json index 27a14836..11076733 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -20,7 +20,8 @@ }, "locations": { "header": "Локации", - "description": "Вот некоторые компании, которые принимают биткойны через Coinos. Надеемся, что ваш будет следующим!" + "description": "Вот некоторые компании, которые принимают биткойны через Coinos. Надеемся, что ваш будет следующим!", + "viewMap": "Посмотреть карту" } }, "error": { @@ -61,7 +62,8 @@ "support": "Поддерживать", "telegram": "Телеграмма", "twitter": "Твиттер", - "documentation": "Документация" + "documentation": "Документация", + "nostr": "Ностр" }, "howItWorks": { "header": "Как это работает", @@ -132,7 +134,9 @@ "noAccount": "У вас нет учетной записи?", "register": "регистр", "signIn": "Войти", - "failed": "Ошибка входа" + "failed": "Ошибка входа", + "createAccount": "Зарегистрироваться", + "signUp": "Зарегистрироваться" }, "nav": { "about": "О", @@ -189,7 +193,8 @@ "deposit": "Депозит", "voucher": "Ваучер", "checkout": "Проверить", - "payNow": "Заплатить сейчас" + "payNow": "Заплатить сейчас", + "setup": "Настройка самостоятельного хранения" }, "receive": { "lessThan1BTCWarning": "Пожалуйста, введите сумму менее 1 биткойна.", @@ -291,7 +296,9 @@ "details": "Подробности", "noMessages": "Вы еще не отправляли и не получали сообщений", "install": "Установить приложение", - "moreDetails": "Ссылки и подробности" + "moreDetails": "Ссылки и подробности", + "url": "Публичный URL-адрес", + "nwc": "Nostr кошелек подключить" }, "payments": { "networkFee": "Сетевая плата", @@ -382,7 +389,8 @@ "showText": "Показать текст", "showQr": "Показать QR-код", "copyPr": "Копировать запрос на оплату", - "amountPrompt": "Всегда запрашивать сумму" + "amountPrompt": "Всегда запрашивать сумму", + "enterWalletPass": "Пароль кошелька" }, "items": { "add": "Добавить элемент", @@ -391,5 +399,28 @@ "price": "Цена", "submit": "Представлять на рассмотрение", "image": "Изображение" + }, + "accounts": { + "back": "Назад", + "next": "Следующий", + "seedPhrase": "Исходная фраза", + "save": "Сохранять", + "copy": "Копировать", + "revealMnemonic": "Раскрыть начальную фразу", + "regenerate": "Регенерировать", + "deleteAccount": "Удалить аккаунт", + "addAccount": "Добавить аккаунт", + "create": "Создавать", + "import": "Импорт", + "accountSetup": "Настройка учетной записи", + "enterSeed": "Введите начальную фразу", + "passwordMismatch": "Пароли не совпадают", + "generateSeed": "Создайте новую исходную фразу в вашем браузере", + "accountPassword": "Пароль учетной записи", + "importSeed": "Введите существующую начальную фразу", + "passwordWarning": "Если вы потеряете этот пароль, вы не сможете вывести средства со счета или восстановить вашу seed-фразу. Мы не можем восстановить ее для вас.", + "savings": "Экономия", + "password": "Пароль", + "confirmPassword": "Подтвердите пароль" } } diff --git a/src/locales/zh.json b/src/locales/zh.json index fbf3dc01..e3ceabba 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -20,7 +20,8 @@ }, "locations": { "header": "地点", - "description": "以下是一些接受 Coinos 比特币支付平台的企业,希望您的公司可以加入我们!" + "description": "以下是一些接受 Coinos 比特币支付平台的企业,希望您的公司可以加入我们!", + "viewMap": "查看地图" } }, "error": { @@ -61,7 +62,8 @@ "support": "支持", "telegram": "电报", "twitter": "推特", - "documentation": "文档" + "documentation": "文档", + "nostr": "诺斯特" }, "howItWorks": { "header": "如何操作", @@ -132,7 +134,9 @@ "noAccount": "没有帐户?", "register": "登记", "signIn": "登入", - "failed": "登录失败" + "failed": "登录失败", + "createAccount": "创建账户", + "signUp": "报名" }, "nav": { "about": "关于", @@ -189,7 +193,8 @@ "deposit": "订金", "voucher": "代金券", "checkout": "查看", - "payNow": "现在付款" + "payNow": "现在付款", + "setup": "设置自我监护" }, "receive": { "lessThan1BTCWarning": "请输入小于 1 比特币的金额。", @@ -291,7 +296,9 @@ "details": "细节", "noMessages": "您尚未发送或接收任何消息", "install": "安装应用程序", - "moreDetails": "链接和详细信息" + "moreDetails": "链接和详细信息", + "url": "公共 URL", + "nwc": "Nostr 钱包连接" }, "payments": { "networkFee": "网络费", @@ -382,7 +389,8 @@ "showText": "显示文本", "showQr": "显示二维码", "copyPr": "复制付款请求", - "amountPrompt": "始终提示金额" + "amountPrompt": "始终提示金额", + "enterWalletPass": "钱包密码" }, "items": { "add": "新增项目", @@ -391,5 +399,28 @@ "price": "价格", "submit": "提交", "image": "图像" + }, + "accounts": { + "back": "后退", + "next": "下一个", + "seedPhrase": "种子短语", + "save": "节省", + "copy": "复制", + "revealMnemonic": "显示种子短语", + "regenerate": "再生", + "deleteAccount": "删除帐户", + "addAccount": "添加帐户", + "create": "创造", + "import": "进口", + "accountSetup": "帐户设置", + "enterSeed": "输入种子短语", + "passwordMismatch": "密码不匹配", + "generateSeed": "在浏览器中生成新的种子短语", + "accountPassword": "账户密码", + "importSeed": "输入现有的种子短语", + "passwordWarning": "如果您丢失了此密码,您将无法从账户中提取资金或恢复您的种子短语。我们无法为您恢复它。", + "savings": "储蓄", + "password": "密码", + "confirmPassword": "确认密码" } } 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, "/"); diff --git a/static/icons/balance.svg b/static/icons/balance.svg new file mode 100644 index 00000000..5c8ccaf6 --- /dev/null +++ b/static/icons/balance.svg @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg height="800px" width="800px" version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" + viewBox="0 0 512 512" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#000000;} +</style> +<g> + <path class="st0" d="M512,339.979h-0.447l-78.911-181.464c7.507,2.712,13.862,5.052,18.787,6.901l9.242-24.513 + c-26.263-9.878-90.312-32.634-160.397-53.254c0-10.817-4.432-20.665-11.522-27.755c-7.075-7.106-16.953-11.499-27.771-11.499 + c-10.817,0-20.718,4.394-27.786,11.499c-2.333,2.318-4.348,4.939-6.022,7.803C157.761,50.151,92.902,37.357,65.745,32.569 + l-4.552,25.794c5.749,1.015,13.317,2.416,22.332,4.159L0.454,253.545H0c0.06,0.197,0.129,0.394,0.189,0.591l-0.167,0.386 + l0.342,0.151c12.817,40.619,50.754,70.078,95.599,70.078c44.852,0,82.79-29.46,95.599-70.078l0.349-0.151l-0.167-0.386 + c0.061-0.197,0.129-0.394,0.19-0.591h-0.455L110.772,67.961c30.975,6.371,70.479,15.098,111.34,25.445 + c1.257,8.545,5.265,16.249,11.082,22.028c3.424,3.447,7.53,6.242,12.068,8.227v276.373c-17.734,0-31.543,0-31.543,0 + s-7.56,47.27-41.588,54.814v24.582h177.692v-24.582c-34.02-7.544-41.588-54.814-41.588-54.814s-13.809,0-31.543,0V123.661 + c4.53-1.985,8.628-4.78,12.06-8.227c0.963-0.97,1.909-1.984,2.772-3.06c41.732,12.264,81.547,25.369,112.325,35.997 + l-83.327,191.608h-0.454c0.06,0.204,0.128,0.394,0.197,0.591l-0.174,0.386l0.34,0.151c12.818,40.618,50.754,70.078,95.606,70.078 + c44.846,0,82.782-29.453,95.599-70.07l0.341-0.159l-0.167-0.386C511.871,340.373,511.947,340.183,512,339.979z M174.343,253.545 + H17.59L95.97,73.317L174.343,253.545z M275.783,102.481c-1.546,1.538-3.326,2.871-5.296,3.841 + c-2.825,1.492-6.045,2.311-9.507,2.311c-5.81,0-10.999-2.334-14.824-6.152c-1.068-1.106-2.046-2.28-2.879-3.59 + c-2.068-3.227-3.249-7.045-3.249-11.211c0-5.818,2.333-11.006,6.128-14.832c0.084-0.076,0.159-0.151,0.243-0.205 + c3.795-3.674,8.87-5.915,14.582-5.915c5.818,0,11.007,2.332,14.803,6.12c2.537,2.545,4.454,5.712,5.424,9.272 + c0.469,1.758,0.734,3.621,0.734,5.56C281.941,93.497,279.608,98.678,275.783,102.481z M337.657,339.979l78.38-180.23l78.373,180.23 + H337.657z"/> +</g> +</svg>
\ No newline at end of file diff --git a/static/icons/cash.svg b/static/icons/cash.svg new file mode 100644 index 00000000..966c00a5 --- /dev/null +++ b/static/icons/cash.svg @@ -0,0 +1,12 @@ +<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 30 30" id="Layer_1" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css"> + .st0{fill:#FD6A7E;} + .st1{fill:#17B978;} + .st2{fill:#8797EE;} + .st3{fill:#41A6F9;} + .st4{fill:#37E0FF;} + .st5{fill:#2FD9B9;} + .st6{fill:#F498BD;} + .st7{fill:#FFDF1D;} + .st8{fill:#C6C9CC;} +</style><path class="st1" d="M26,5H4C3.4,5,3,5.4,3,6v12c0,0.6,0.4,1,1,1h22c0.6,0,1-0.4,1-1V6C27,5.4,26.6,5,26,5z M5.5,14 c-0.2,0-0.4,0-0.5,0.1V9.9C5.1,10,5.3,10,5.5,10C6.9,10,8,8.9,8,7.5C8,7.3,8,7.1,7.9,7h14.1C22,7.1,22,7.3,22,7.5 c0,1.4,1.1,2.5,2.5,2.5c0.2,0,0.4,0,0.5-0.1v4.1c-0.1,0-0.3-0.1-0.5-0.1c-1.4,0-2.5,1.1-2.5,2.5c0,0.2,0,0.4,0.1,0.5H7.9 C8,16.9,8,16.7,8,16.5C8,15.1,6.9,14,5.5,14z M12,12c0-1.7,1.3-3,3-3s3,1.3,3,3s-1.3,3-3,3S12,13.7,12,12z M5,22v-1h20v1 c0,0.6-0.4,1-1,1H6C5.4,23,5,22.6,5,22z M7.4,26v-1h15v1c0,0.6-0.4,1-1,1h-13C7.8,27,7.4,26.6,7.4,26z"/></svg>
\ No newline at end of file diff --git a/static/icons/text.svg b/static/icons/text.svg index 2bf9998f..a8fe6e04 100644 --- a/static/icons/text.svg +++ b/static/icons/text.svg @@ -1,17 +1,4 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> -<svg width="800px" height="800px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"> - - <title>align-left</title> - <desc>Created with Sketch Beta.</desc> - <defs> - -</defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> - <g id="Icon-Set-Filled" sketch:type="MSLayerGroup" transform="translate(-310.000000, -208.000000)" fill="#000000"> - <path d="M328,232 L312,232 C310.896,232 310,232.896 310,234 C310,235.104 310.896,236 312,236 L328,236 C329.104,236 330,235.104 330,234 C330,232.896 329.104,232 328,232 L328,232 Z M336,224 L312,224 C310.896,224 310,224.896 310,226 C310,227.104 310.896,228 312,228 L336,228 C337.104,228 338,227.104 338,226 C338,224.896 337.104,224 336,224 L336,224 Z M312,220 L328,220 C329.104,220 330,219.104 330,218 C330,216.896 329.104,216 328,216 L312,216 C310.896,216 310,216.896 310,218 C310,219.104 310.896,220 312,220 L312,220 Z M312,212 L336,212 C337.104,212 338,211.104 338,210 C338,208.896 337.104,208 336,208 L312,208 C310.896,208 310,208.896 310,210 C310,211.104 310.896,212 312,212 L312,212 Z" id="align-left" sketch:type="MSShapeGroup"> - -</path> - </g> - </g> +<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12 3V21M9 21H15M19 6V3H5V6" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
\ No newline at end of file diff --git a/static/images/cash.png b/static/images/cash.png Binary files differnew file mode 100644 index 00000000..2f1063dd --- /dev/null +++ b/static/images/cash.png diff --git a/static/images/keys.png b/static/images/keys.png Binary files differnew file mode 100644 index 00000000..aef443d2 --- /dev/null +++ b/static/images/keys.png diff --git a/static/images/minibits.png b/static/images/minibits.png Binary files differnew file mode 100644 index 00000000..307ed7c8 --- /dev/null +++ b/static/images/minibits.png |
