aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-08-18 13:09:41 -0700
committerAdam Soltys <[email protected]>2024-08-18 22:15:05 -0700
commitd66cba2d111bec3a2bd57a56233577f3fd417b18 (patch)
tree5cf0250363d11fb33414d8cae4f583e186247123 /src
parent09d4867fe0ddc8cbc7bfa670911fe5c33a0a4f36 (diff)
downloadcoinos-ui-d66cba2d111bec3a2bd57a56233577f3fd417b18.tar.gz
coinos-ui-d66cba2d111bec3a2bd57a56233577f3fd417b18.zip
ecash
Diffstat (limited to 'src')
-rw-r--r--src/components/AppHeader.svelte2
-rw-r--r--src/lib/parse.js95
-rw-r--r--src/lib/socket.js10
-rw-r--r--src/lib/store.js2
-rw-r--r--src/lib/utils.js1
-rw-r--r--src/locales/en.json12
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.svelte2
-rw-r--r--src/routes/(app)/[username]/ecash/[...token]/+page.server.js5
-rw-r--r--src/routes/(app)/[username]/ecash/[...token]/+page.svelte61
-rw-r--r--src/routes/(app)/[username]/invoice/[id]/+page.svelte142
-rw-r--r--src/routes/(app)/[username]/payments/[...page]/+page.svelte2
-rw-r--r--src/routes/(app)/[username]/receive/[...param]/+page.server.js22
-rw-r--r--src/routes/(app)/[username]/receive/[...param]/+page.svelte136
-rw-r--r--src/routes/(app)/qr/[text]/+page.svelte14
-rw-r--r--src/routes/(app)/send/[...text]/+page.server.js91
-rw-r--r--src/routes/(app)/send/[...text]/+page.svelte34
-rw-r--r--src/routes/(app)/send/ecash/+page.server.js19
-rw-r--r--src/routes/(app)/send/ecash/+page.svelte60
-rw-r--r--src/routes/(app)/voucher/+page.svelte90
-rw-r--r--src/routes/(app)/voucher/[...token]/+page.server.js20
-rw-r--r--src/routes/(app)/voucher/[...token]/+page.svelte91
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>