diff options
| author | Adam Soltys <[email protected]> | 2024-08-18 13:09:41 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-08-18 22:15:05 -0700 |
| commit | d66cba2d111bec3a2bd57a56233577f3fd417b18 (patch) | |
| tree | 5cf0250363d11fb33414d8cae4f583e186247123 /src | |
| parent | 09d4867fe0ddc8cbc7bfa670911fe5c33a0a4f36 (diff) | |
| download | coinos-ui-d66cba2d111bec3a2bd57a56233577f3fd417b18.tar.gz coinos-ui-d66cba2d111bec3a2bd57a56233577f3fd417b18.zip | |
ecash
Diffstat (limited to 'src')
21 files changed, 719 insertions, 192 deletions
diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte index 6f68c41a..3b0303a8 100644 --- a/src/components/AppHeader.svelte +++ b/src/components/AppHeader.svelte @@ -66,7 +66,7 @@ .url.pathname === `/${user.username}/receive` ? 'opacity-100' : 'opacity-70 hover:opacity-80'}" - ><Icon icon="numpad" style="mx-auto w-6 sm:w-8" /> + ><Icon icon="receive" style="mx-auto w-6 sm:w-8" /> </button> </a> <a href={`/${user.username}/payments`}> diff --git a/src/lib/parse.js b/src/lib/parse.js new file mode 100644 index 00000000..fa4cb002 --- /dev/null +++ b/src/lib/parse.js @@ -0,0 +1,95 @@ +import { auth, get, isLiquid, post } from "$lib/utils"; +import bip21 from "bip21"; +import { validate } from "bitcoin-address-validation"; +import { PUBLIC_DOMAIN } from "$env/static/public"; +import { redirect } from "@sveltejs/kit"; + +export default async (t, host, cookies) => { + if (!t) return; + + if (t.startsWith("http")) redirect(307, t); + if (t.startsWith(host)) redirect(307, `http://${t}`); + + let amount, id, user; + + t = t.trim(); + t.toLowerCase().startsWith("bitcoin:") && + ({ + address: t, + options: { amount }, + } = bip21.decode(t)); + + if (t.startsWith("lightning:")) t = t.replace("lightning:", ""); + if (t.endsWith(`@${PUBLIC_DOMAIN}`)) t = t.split("@")[0]; + if (t.includes("@") && t.includes(".")) { + try { + t = await get(`/encode?address=${t}`); + } catch (e) {} + } + + if (t.includes("/fund")) redirect(307, t.substring(t.indexOf("/fund"))); + + // lightning + if (t.toLowerCase().startsWith("lightning:")) + t = t.toLowerCase().replace("lightning:", ""); + if (t.toLowerCase().startsWith("lnurl")) redirect(307, `/ln/${t}`); + if (t.includes(":")) t = t.split(":")[1]; + + if (t.toLowerCase().startsWith("ln")) { + try { + ({ id } = await get(`/invoice/${t}`)); + } catch (e) { + redirect(307, `/send/lightning/${t}`); + } + } + + // 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 claimed; + if (t.toLowerCase().startsWith("cashu")) { + try { + claimed = await post(`/claim`, { token: t }, auth(cookies)); + claimed = { ok: true }; + } catch (e) { + return e.message; + } + if (claimed.ok) { + redirect(307, `/${cookies.get("username")}/payments`); + } + } + + // user + try { + user = await get(`/users/${t.split("/")[0]}`); + if (user.anon) user = null; + } catch (e) {} + + if (user) redirect(307, `/pay/${t}`); + + let fund; + try { + fund = await get(`/fund/${t}`); + } catch (e) {} + + if (fund) redirect(307, `/send/fund/${t}`); + + // invoice + let invoice; + try { + invoice = await get(`/invoice/${t}`); + } catch (e) {} + + if (invoice) redirect(307, `/send/invoice/${invoice.id}`); +}; diff --git a/src/lib/socket.js b/src/lib/socket.js index f6a6893b..1b0c2649 100644 --- a/src/lib/socket.js +++ b/src/lib/socket.js @@ -32,9 +32,13 @@ export const messages = (data) => ({ async payment() { let { amount, confirmed } = data; - invalidate("app:user"); - invalidate("app:invoice"); - invalidate("app:payments"); + + // without setTimeout, invalidate seems to break redirections from actions + setTimeout(() => { + invalidate("app:user"); + invalidate("app:invoice"); + invalidate("app:payments"); + }, 500); if (amount > 0) { success(`${confirmed ? "Received" : "Detected"} ${sat(amount)}!`); diff --git a/src/lib/store.js b/src/lib/store.js index 747afc4e..71d4aa40 100644 --- a/src/lib/store.js +++ b/src/lib/store.js @@ -56,4 +56,4 @@ export let request = writable(); export let requestRedirect = writable(); export let token = persistSession("token"); export let ndef = writable(); -export let showQr = persistLocal("showQr"); +export let showQr = persistLocal("showQr", true); diff --git a/src/lib/utils.js b/src/lib/utils.js index a897bed8..ac6e9216 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -288,6 +288,7 @@ export let types = { lightning: "lightning", internal: "internal", fund: "fund", + ecash: "ecash" }; export let ease = (t) => diff --git a/src/locales/en.json b/src/locales/en.json index 6477dacd..b98ea54c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -157,7 +157,7 @@ "user": { "install": "Install app", "moreDetails": "Profile details", - "noMessages": "You have not sent or received any messages yet", + "noMessages": "You have not sent or received any messages", "profilePage": "Profile page", "nostrPubkey": "Nostr pubkey", "requestPayment": "Request Payment", @@ -293,6 +293,10 @@ } }, "payments": { + "ecash": "Ecash", + "memo": "Notes", + "voucherText": "Enter an ecash or lnurlw code", + "createEcash": "Create ecash", "tips": "Tips", "options": "options", "bump": "Bump fee", @@ -308,11 +312,15 @@ "takeFunds": "Take Funds", "show": "Show", "hide": "Hide", + "voucher": "Redeem voucher", "qr": "QR", + "copy": "Copy", "copyLink": "Copy Link", "copyText": "Copy Text", "openLink": "Open wallet", + "setAmount": "Set amount", "moreOptions": "More options", + "redeemVoucher": "Ecash/lnurlw voucher", "total": "Total", "gainLoss": "Gain/Loss", "presentValue": "Present Value", @@ -360,7 +368,7 @@ "fundsClaimed": "Funds claimed!", "print": "Print", "onlyLbtc": "We only support", - "dontDeposit": "don't deposit other assets yet" + "dontDeposit": "don't deposit other assets" }, "items": { "add": "Add item", diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte index 1dd8f573..2dfd4979 100644 --- a/src/routes/(app)/[username]/(profile)/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/+page.svelte @@ -147,7 +147,7 @@ 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="numpad" style="w-8" /> + <Icon icon="receive" style="w-8" /> <div class="my-auto">{$t("user.dashboard.receive")}</div> </div> </button> diff --git a/src/routes/(app)/[username]/ecash/[...token]/+page.server.js b/src/routes/(app)/[username]/ecash/[...token]/+page.server.js new file mode 100644 index 00000000..01b5521c --- /dev/null +++ b/src/routes/(app)/[username]/ecash/[...token]/+page.server.js @@ -0,0 +1,5 @@ +export let load = async ({ cookies, params, parent }) => { + let { rates, user } = await parent(); + let token = params.token || cookies.get("ecash"); + return { rates, token, user }; +}; diff --git a/src/routes/(app)/[username]/ecash/[...token]/+page.svelte b/src/routes/(app)/[username]/ecash/[...token]/+page.svelte new file mode 100644 index 00000000..d02102a6 --- /dev/null +++ b/src/routes/(app)/[username]/ecash/[...token]/+page.svelte @@ -0,0 +1,61 @@ +<script> + import { getDecodedToken } from "@cashu/cashu-ts"; + import { copy, sats, f, s } from "$lib/utils"; + import { t } from "$lib/translations"; + import Icon from "$comp/Icon.svelte"; + export let data; + let { rates, user, token } = data; + let { currency } = user; + let rate = rates[currency]; + let { proofs } = getDecodedToken(token).token[0]; + let amount = proofs.reduce((a, b) => a + b.amount, 0); + $: amountFiat = parseFloat(((amount * rate) / sats).toFixed(2)); +</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"> + {$t("payments.ecash")} + </h1> + + {#if amount > 0} + <div class="text-center font-bold text-2xl"> + <div> + {f(amountFiat, currency)} + </div> + <div> + <span class="text-secondary font-normal text-xl" + >⚡️{`${s(amount)}`}</span + > + </div> + </div> + {/if} + + <div class="flex flex-wrap gap-2 text-xl"> + <a href={`/qr/${encodeURIComponent(token)}`} class="contents"> + <button + type="button" + class="flex border rounded-2xl px-6 py-5 font-bold hover:opacity-80 w-full justify-center gap-2" + > + <Icon icon="qr" style="w-8 my-auto invert" /> + <div class="my-auto">{$t("payments.qr")}</div> + </button> + </a> + + <a href={`/voucher/${encodeURIComponent(token)}`} class="contents"> + <button + type="button" + class="flex border rounded-2xl px-6 py-5 font-bold hover:opacity-80 w-full justify-center gap-2" + > + <Icon icon="receive" style="w-8 my-auto" /> + <div class="my-auto">{$t("payments.redeem")}</div> + </button> + </a> + </div> + + <button + class="border rounded p-6 w-full break-all text-xl hover:bg-slate-100" + on:click={() => copy(token)} + > + {token} + </button> +</div> diff --git a/src/routes/(app)/[username]/invoice/[id]/+page.svelte b/src/routes/(app)/[username]/invoice/[id]/+page.svelte index 1a569039..f28faf99 100644 --- a/src/routes/(app)/[username]/invoice/[id]/+page.svelte +++ b/src/routes/(app)/[username]/invoice/[id]/+page.svelte @@ -1,5 +1,5 @@ <script> - import { enhance } from "$app/forms"; + import { enhance } from "$app/forms"; import { send } from "$lib/socket"; import { btc, @@ -21,6 +21,7 @@ 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"; @@ -117,6 +118,22 @@ goto(`./${id}?options=true`, { invalidateAll: true, noScroll: true }); }; + + let setAmount = async () => { + settingAmount = false; + invoice.amount = amount; + ({ id } = await post(`/${username}/invoice`, { + invoice, + user: { username, currency }, + })); + + goto(`./${id}?options=true`, { invalidateAll: true, noScroll: true }); + }; + + 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"> @@ -177,50 +194,50 @@ </div> {/if} - <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 whitespace-nowrap" - on:click={() => copy(txt)} - > - <Icon icon="copy" style="w-8 my-auto" /> - <div class="my-auto"> - {txt.substr(0, 11)}..{txt.substr(-10)} - </div> - </button> + <div class="text-secondary space-y-1"> + <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 whitespace-nowrap" + on:click={() => copy(txt)} + > + <Icon icon="copy" style="w-8 my-auto" /> + <div class="my-auto"> + {$t("payments.copy")} + {txt.substr(0, 11)}...{txt.substr(-6)} + </div> + </button> - <div - class="w-full flex justify-center gap-2 flex-wrap text-secondary text-xl" - > - <a href={link} class="w-full"> + {#if user.id === invoice.user.id} <button - class="w-full flex justify-center rounded-2xl border py-5 px-6 hover:opacity-80" + class="w-full flex justify-center rounded-2xl border py-5 px-6 hover:opacity-80 text-xl" + on:click={toggleAmount} > - <Icon icon="mobile" style="mr-1 w-8" /> - <div class="my-auto">{$t("payments.openLink")}</div> + <Icon icon="edit" style="mr-1 w-8" /> + <div class="my-auto">{$t("payments.setAmount")}</div> </button> - </a> - - <button - class="w-full flex justify-center rounded-2xl border py-5 px-6 hover:opacity-80" - on:click={() => ($showQr = !$showQr)} - > - <Icon icon="qr" style="w-8 mr-1 invert my-auto" /> - <div class="my-auto"> - {$showQr ? $t("payments.hide") : $t("payments.show")} - {$t("payments.qr")} - </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} - <!-- <button --> - <!-- class="w-full flex justify-center rounded-2xl border py-3 px-5 hover:opacity-80 text-lg" --> - <!-- on:click={() => (showOptions = !showOptions)} --> - <!-- > --> - <!-- <Icon icon="settings" style="w-8 mr-1 my-auto" /> --> - <!-- <div class="my-auto"> --> - <!-- {showOptions ? $t("payments.hide") : $t("payments.show")} --> - <!-- {$t("payments.options")} --> - <!-- </div></button --> - <!-- > --> + <div class="w-full flex justify-center gap-2 flex-wrap text-xl"> + <button + class="w-full flex justify-center rounded-2xl border py-5 px-6 hover:opacity-80" + on:click={() => ($showQr = !$showQr)} + > + <Icon icon="qr" style="w-8 mr-1 invert my-auto" /> + <div class="my-auto"> + {$showQr ? $t("payments.hide") : $t("payments.show")} + {$t("payments.qr")} + </div></button + > + </div> </div> <div class="flex justify-around text-secondary"> @@ -266,8 +283,53 @@ </div> </div> {/if} + + {#if user.id === invoice.user.id} + <a href={`/voucher`}> + <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} </div> +{#if settingAmount} + <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-12 border w-96 shadow-lg rounded-md bg-white space-y-5" + > + <form submit={setAmount}> + <Numpad bind:amount bind:currency bind:rate bind:fiat bind:submit /> + <div class="w-full flex flex-wrap gap-2"> + <button + 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 + 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} + on:keydown={toggleAmount} + > + <div class="my-auto">Cancel</div> + </button> + </div> + </form> + </div> + </div> +{/if} + <style> .invoice { view-transition-name: invoice; diff --git a/src/routes/(app)/[username]/payments/[...page]/+page.svelte b/src/routes/(app)/[username]/payments/[...page]/+page.svelte index 7e2bac9f..9f8dd5df 100644 --- a/src/routes/(app)/[username]/payments/[...page]/+page.svelte +++ b/src/routes/(app)/[username]/payments/[...page]/+page.svelte @@ -203,7 +203,7 @@ </div> {:else} <div class="text-secondary flex"> - {#if p.type === types.lightning} + {#if [types.ecash, types.lightning].includes(p.type)} <div class="text-3xl">⚡️</div> {:else if p.type === types.bitcoin} <div class="my-auto mr-1"> diff --git a/src/routes/(app)/[username]/receive/[...param]/+page.server.js b/src/routes/(app)/[username]/receive/[...param]/+page.server.js index a6e59175..c3b6ca64 100644 --- a/src/routes/(app)/[username]/receive/[...param]/+page.server.js +++ b/src/routes/(app)/[username]/receive/[...param]/+page.server.js @@ -1,15 +1,19 @@ -import { auth, get } from "$lib/utils"; +import { redirect } from "@sveltejs/kit"; +import { auth, post } from "$lib/utils"; import invoice from "$lib/invoice"; -export let load = async ({ cookies, params }) => { - let id = params.param; +export let load = async ({ cookies, parent }) => { + let { user, rates } = await parent(); - let request; - if (id) ({ request } = await get(`/request/${id}`, auth(cookies))); + let invoice = { + type: "lightning", + rate: rates[user.currency], + }; - return { ...params, request }; -}; + invoice = await post("/invoice", { invoice, user }, auth(cookies)); + let { id } = invoice; -export const actions = { - default: 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 index d643d181..f9267105 100644 --- a/src/routes/(app)/[username]/receive/[...param]/+page.svelte +++ b/src/routes/(app)/[username]/receive/[...param]/+page.svelte @@ -1,52 +1,116 @@ <script> - import Icon from "$comp/Icon.svelte"; - import Numpad from "$comp/Numpad.svelte"; - import Toggle from "$comp/Toggle.svelte"; - import { t } from "$lib/translations"; - import { enhance } from "$app/forms"; import { page } from "$app/stores"; - import { requestRedirect } from "$lib/store"; + 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; - let type = "lightning"; - - if ($page.params.param === "bitcoin") type = "bitcoin"; + export let form; + data.subject = data.user; - let { request } = data; + let { contacts } = data; + let w; - let amount; + let el, text; - let lightning = true; - let { currency, username, prompt } = data.subject; - let { rates, user } = data; - let submit; + let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click()); - $: rate = rates[currency]; - $: type = lightning ? "lightning" : "bitcoin"; + let paste = async () => { + text = await navigator.clipboard.readText(); + }; </script> -<form - method="POST" - class="flex justify-center items-center mb-3 px-3" - use:enhance -> - <input type="hidden" name="amount" value={amount} /> - <input type="hidden" name="currency" value={currency} /> - <input type="hidden" name="username" value={username} /> - <input type="hidden" name="type" value={type} /> - <input type="hidden" name="rate" value={rate} /> - <input type="hidden" name="prompt" value={prompt} /> +<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> - <div class="space-y-3 w-[300px] mx-auto"> - <Numpad bind:amount bind:currency bind:rate {submit} /> <button - bind:this={submit} + bind:this={el} type="submit" - class="bg-black text-white rounded-xl w-full h-[48px] flex justify-center items-center font-semibold - opacity-100 hover:opacity-80" + class="bg-black text-white border rounded-2xl px-6 py-5 w-full font-bold" > - {$t("payments.next")} + {$t("user.send.next")} </button> - </div> -</form> + </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)/qr/[text]/+page.svelte b/src/routes/(app)/qr/[text]/+page.svelte index da164b50..c9562582 100644 --- a/src/routes/(app)/qr/[text]/+page.svelte +++ b/src/routes/(app)/qr/[text]/+page.svelte @@ -1,7 +1,7 @@ <script> import Icon from "$comp/Icon.svelte"; import Qr from "$comp/Qr.svelte"; - import { back } from "$lib/utils"; + import { back, copy } from "$lib/utils"; export let data; let { src, text } = data; </script> @@ -17,9 +17,15 @@ > <Qr {src} /> - <div class="text-center w-full text-lg break-all"> - {text} - </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)} + > + <div class="my-auto"> + {text} + </div> + </button> </div> </div> </div> diff --git a/src/routes/(app)/send/[...text]/+page.server.js b/src/routes/(app)/send/[...text]/+page.server.js index 3e8623eb..c1ba5945 100644 --- a/src/routes/(app)/send/[...text]/+page.server.js +++ b/src/routes/(app)/send/[...text]/+page.server.js @@ -1,98 +1,19 @@ -import { validate } from "bitcoin-address-validation"; -import bip21 from "bip21"; -import { auth, get, isLiquid, post } from "$lib/utils"; +import { auth, get } from "$lib/utils"; import { fail, redirect } from "@sveltejs/kit"; -import { PUBLIC_DOMAIN } from "$env/static/public"; - -let parse = async (t, host) => { - if (!t) return; - - if (t.startsWith("http")) redirect(307, t); - if (t.startsWith(host)) redirect(307, `http://${t}`); - - let amount, user, id; - - t = t.trim(); - t.toLowerCase().startsWith("bitcoin:") && - ({ - address: t, - options: { amount }, - } = bip21.decode(t)); - - if (t.startsWith("lightning:")) t = t.replace("lightning:", ""); - if (t.endsWith(`@${PUBLIC_DOMAIN}`)) t = t.split("@")[0]; - if (t.includes("@") && t.includes(".")) { - try { - t = await get(`/encode?address=${t}`); - } catch (e) {} - } - - if (t.includes("/fund")) redirect(307, t.substring(t.indexOf("/fund"))); - - // lightning - if (t.toLowerCase().startsWith("lightning:")) - t = t.toLowerCase().replace("lightning:", ""); - if (t.toLowerCase().startsWith("lnurl")) redirect(307, `/ln/${t}`); - if (t.includes(":")) t = t.split(":")[1]; - - if (t.toLowerCase().startsWith("ln")) { - try { - ({ id } = await get(`/invoice/${t}`)); - } catch (e) { - redirect(307, `/send/lightning/${t}`); - } - } - - // 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}`); - } - - // user - try { - user = await get(`/users/${t.split("/")[0]}`); - if (user.anon) user = null; - } catch (e) {} - - if (user) redirect(307, `/pay/${t}`); - - let fund; - try { - fund = await get(`/fund/${t}`); - } catch (e) {} - - if (fund) redirect(307, `/send/fund/${t}`); - - // invoice - let invoice; - try { - invoice = await get(`/invoice/${t}`); - } catch (e) {} - - if (invoice) redirect(307, `/send/invoice/${invoice.id}`); -}; +import parse from "$lib/parse"; export async function load({ cookies, params, request, url }) { let { text } = params; - await parse(text, url.host); + await parse(text, url.host, cookies); let contacts = await get("/contacts", auth(cookies)); return { contacts }; } export const actions = { - default: async ({ request, url }) => { + default: async ({ cookies, request, url }) => { const form = await request.formData(); let t = form.get("text"); - await parse(t, url.host); - return fail(400, { error: true }); + let msg = await parse(t, url.host, cookies); + return fail(400, { error: msg || "default" }); }, }; diff --git a/src/routes/(app)/send/[...text]/+page.svelte b/src/routes/(app)/send/[...text]/+page.svelte index f9267105..e677760f 100644 --- a/src/routes/(app)/send/[...text]/+page.svelte +++ b/src/routes/(app)/send/[...text]/+page.svelte @@ -16,15 +16,17 @@ data.subject = data.user; let { contacts } = data; - let w; - let el, text; + let el, text, pasted, w; let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click()); let paste = async () => { text = await navigator.clipboard.readText(); + pasted = true; }; + + $: if (browser && pasted && text) el.click() && (pasted = false); </script> <svelte:window bind:innerWidth={w} /> @@ -39,7 +41,9 @@ {#if form?.error} <div class="mb-5"> <div class="text-red-600"> - {$t("error.unrecognizedInput")} + {form.error === "default" + ? $t("error.unrecognizedInput") + : form.error} </div> </div> {/if} @@ -52,6 +56,7 @@ on:keypress={keypress} class="w-full p-4 border rounded-xl h-48 text-xl" bind:value={text} + on:paste={() => (pasted = true)} autocapitalize="none" /> </div> @@ -60,19 +65,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-6 py-5 font-bold hover:opacity-80 w-full bg-primary justify-center gap-2" > - <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-6 py-5 font-bold hover:opacity-80 w-full bg-primary justify-center gap-2" 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> @@ -80,12 +85,23 @@ <button bind:this={el} type="submit" - class="bg-black text-white border rounded-2xl px-6 py-5 w-full font-bold" + class="flex bg-black text-white border rounded-2xl px-6 py-5 w-full font-bold gap-2 justify-center" > - {$t("user.send.next")} + <Icon icon="send" style="w-8 my-auto invert" /> + <div class="my-auto">{$t("user.send.next")}</div> </button> </form> + <a href="/send/ecash"> + <button + type="button" + class="flex border rounded-2xl px-6 py-5 w-full font-bold gap-2 justify-center text-xl" + > + <Icon icon="voucher" style="w-8 my-auto" /> + <div class="my-auto">{$t("payments.createEcash")}</div> + </button> + </a> + {#if contacts.length} <div class="space-y-5"> <h1 class="px-3 md:px-0 text-2xl font-semibold mt-10"> diff --git a/src/routes/(app)/send/ecash/+page.server.js b/src/routes/(app)/send/ecash/+page.server.js new file mode 100644 index 00000000..2fb43a2f --- /dev/null +++ b/src/routes/(app)/send/ecash/+page.server.js @@ -0,0 +1,19 @@ +import { fail, redirect } from "@sveltejs/kit"; +import { auth, post, fd } from "$lib/utils"; + +export const actions = { + default: async ({ cookies, request }) => { + let token; + try { + let body = await fd(request); + ({ token } = await post("/mint", body, auth(cookies))); + } catch (e) { + console.log("problem creating ecash", e); + return fail(400, { message: e.message }); + } + + cookies.set("ecash", token, { path: "/" }); + + redirect(307, `/${cookies.get("username")}/ecash`); + }, +}; diff --git a/src/routes/(app)/send/ecash/+page.svelte b/src/routes/(app)/send/ecash/+page.svelte new file mode 100644 index 00000000..d8e2dd46 --- /dev/null +++ b/src/routes/(app)/send/ecash/+page.svelte @@ -0,0 +1,60 @@ +<script> + import { enhance } from "$app/forms"; + import { t } from "$lib/translations"; + import Icon from "$comp/Icon.svelte"; + import Numpad from "$comp/Numpad.svelte"; + import { page } from "$app/stores"; + import { rate } from "$lib/store"; + import { fail, s } from "$lib/utils"; + + export let data; + + let { rates, user } = data; + let { balance, currency, username } = user; + + let amount = 0; + let submit, fiat; + + $: update(data); + let update = () => ($rate = data.rates[currency]); + + let setMax = () => { + fiat = false; + amount = balance; + }; +</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 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"> + <button + type="button" + class="hover:opacity-80 bg-black text-white rounded-2xl py-3 px-4 mt-2 border font-bold" + on:click|preventDefault={setMax} + on:keydown={setMax}>Max ⚡️{s(balance)}</button + > + + <button + 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" + > + {$t("payments.next")} + </button> + </div> + </form> +</div> diff --git a/src/routes/(app)/voucher/+page.svelte b/src/routes/(app)/voucher/+page.svelte new file mode 100644 index 00000000..5e21b3be --- /dev/null +++ b/src/routes/(app)/voucher/+page.svelte @@ -0,0 +1,90 @@ +<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 el, text, pasted, w; + + let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click()); + + let paste = async () => { + text = await navigator.clipboard.readText(); + }; + + $: if (browser && pasted && text) el.click() && (pasted = false); +</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.voucher")} + </h1> + + <form method="POST" use:enhance class="space-y-2 text-xl"> + {#if form?.error} + <div class="mb-5"> + <div class="text-red-600"> + {form.error === "default" + ? $t("error.unrecognizedInput") + : form.error} + </div> + </div> + {/if} + + <div class="mb-2"> + <textarea + use:focus + name="text" + placeholder={$t("payments.voucherText")} + on:keypress={keypress} + class="w-full p-4 border rounded-xl h-48 text-xl" + bind:value={text} + on:paste={() => (pasted = true)} + 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> +</div> diff --git a/src/routes/(app)/voucher/[...token]/+page.server.js b/src/routes/(app)/voucher/[...token]/+page.server.js new file mode 100644 index 00000000..dcfb046e --- /dev/null +++ b/src/routes/(app)/voucher/[...token]/+page.server.js @@ -0,0 +1,20 @@ +import { validate } from "bitcoin-address-validation"; +import bip21 from "bip21"; +import { auth, get, isLiquid, post } from "$lib/utils"; +import { fail, redirect } from "@sveltejs/kit"; +import { PUBLIC_DOMAIN } from "$env/static/public"; +import parse from "$lib/parse"; +import { test } from "$lib/parse"; + +export async function load({ cookies, params, request, url }) { + await parse(params.token, url.host, cookies); +} + +export const actions = { + default: async ({ cookies, request, url }) => { + const form = await request.formData(); + let t = form.get("text"); + let msg = await parse(t, url.host, cookies); + return fail(400, { error: msg || "default" }); + }, +}; diff --git a/src/routes/(app)/voucher/[...token]/+page.svelte b/src/routes/(app)/voucher/[...token]/+page.svelte new file mode 100644 index 00000000..f5b7b882 --- /dev/null +++ b/src/routes/(app)/voucher/[...token]/+page.svelte @@ -0,0 +1,91 @@ +<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 el, text, pasted, w; + + let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click()); + + let paste = async () => { + text = await navigator.clipboard.readText(); + }; + + + $: if (browser && pasted && text) el.click() && (pasted = false); +</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.voucher")} + </h1> + + <form method="POST" use:enhance class="space-y-2 text-xl"> + {#if form?.error} + <div class="mb-5"> + <div class="text-red-600"> + {form.error === "default" + ? $t("error.unrecognizedInput") + : form.error} + </div> + </div> + {/if} + + <div class="mb-2"> + <textarea + use:focus + name="text" + placeholder={$t("payments.voucherText")} + on:keypress={keypress} + class="w-full p-4 border rounded-xl h-48 text-xl" + bind:value={text} + on:paste={() => (pasted = true)} + 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> +</div> |
