aboutsummaryrefslogtreecommitdiffstats
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
parent09d4867fe0ddc8cbc7bfa670911fe5c33a0a4f36 (diff)
downloadcoinos-ui-d66cba2d111bec3a2bd57a56233577f3fd417b18.tar.gz
coinos-ui-d66cba2d111bec3a2bd57a56233577f3fd417b18.zip
ecash
-rwxr-xr-xbun.lockbbin154608 -> 156730 bytes
-rw-r--r--package.json2
-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
-rw-r--r--static/icons/receive.svg19
-rw-r--r--static/icons/voucher.svg12
-rw-r--r--static/images/cashu.pngbin0 -> 5762 bytes
26 files changed, 736 insertions, 208 deletions
diff --git a/bun.lockb b/bun.lockb
index 23f34c1f..10a6badf 100755
--- a/bun.lockb
+++ b/bun.lockb
Binary files differ
diff --git a/package.json b/package.json
index e5d1e8ba..9c84fbb0 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"@scure/base": "^1.1.1",
"@scure/bip39": "^1.2.1",
"@sveltejs/adapter-node": "^2.0.0",
- "@sveltejs/kit": "^2.0.0",
+ "@sveltejs/kit": "^2.5.22",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/line-clamp": "^0.4.2",
"@zerodevx/svelte-toast": "^0.9.5",
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>
diff --git a/static/icons/receive.svg b/static/icons/receive.svg
index 143ae144..a376b40e 100644
--- a/static/icons/receive.svg
+++ b/static/icons/receive.svg
@@ -1,16 +1,5 @@
-<?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 48 48" xmlns="http://www.w3.org/2000/svg" >
- <path d="M0 0h48v48H0z" fill="none"/>
-<g id="Shopicon">
- <path d="M25.662,19.651C26.402,19.877,27.187,20,28,20c4.418,0,8-3.582,8-8s-3.582-8-8-8c-2.212,0-4.214,0.898-5.662,2.349
- C21.598,6.123,20.814,6,20,6c-4.418,0-8,3.582-8,8s3.582,8,8,8C22.212,22,24.214,21.102,25.662,19.651z M28,8c2.206,0,4,1.794,4,4
- s-1.794,4-4,4c-2.206,0-4-1.794-4-4S25.795,8,28,8z M16,14c0-2.206,1.794-4,4-4c0.088,0,0.169,0.02,0.256,0.026
- C20.095,10.658,20,11.317,20,12c0,2.079,0.8,3.967,2.1,5.389C21.488,17.77,20.773,18,20,18C17.795,18,16,16.206,16,14z"/>
- <path d="M30,24c-5.952,0-10.813,1.913-12.9,4.777c-1.05-0.914-1.862-1.813-2.3-2.379c-1.986-2.643-5.754-3.181-8.4-1.198
- c-2.646,1.984-3.185,5.753-1.199,8.4C5.519,34.025,13.131,44,24,44h20V32C44,27.439,37.981,24,30,24z M8.4,31.2
- c-0.662-0.882-0.482-2.139,0.397-2.8c0.36-0.27,0.783-0.399,1.201-0.399c0.608,0,1.207,0.274,1.596,0.791
- C11.647,28.864,17.026,36,24,36h8v-2l0,0v-2h-8c-1.229,0-2.46-0.385-3.61-0.962C21.518,29.573,25.025,28,30,28
- c6.104,0,10,2.369,10,4v8h-2H24C15.107,40,8.669,31.559,8.4,31.2z"/>
-</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 fill-rule="evenodd" clip-rule="evenodd" d="M17.4964 21.9284C17.844 21.7894 18.1491 21.6495 18.4116 21.5176C18.9328 22.4046 19.8969 23 21 23C22.6569 23 24 21.6568 24 20V14C24 12.3431 22.6569 11 21 11C19.5981 11 18.4208 11.9616 18.0917 13.2612C17.8059 13.3614 17.5176 13.4549 17.2253 13.5384C16.3793 13.7801 15.3603 13.9999 14.5 13.9999C13.2254 13.9999 10.942 13.5353 9.62034 13.2364C8.61831 13.0098 7.58908 13.5704 7.25848 14.5622L6.86313 15.7483C5.75472 15.335 4.41275 14.6642 3.47619 14.1674C2.42859 13.6117 1.09699 14.0649 0.644722 15.1956L0.329309 15.9841C0.0210913 16.7546 0.215635 17.6654 0.890813 18.2217C1.66307 18.8581 3.1914 20.0378 5.06434 21.063C6.91913 22.0782 9.21562 22.9999 11.5 22.9999C14.1367 22.9999 16.1374 22.472 17.4964 21.9284ZM20 20C20 20.5523 20.4477 21 21 21C21.5523 21 22 20.5523 22 20V14C22 13.4477 21.5523 13 21 13C20.4477 13 20 13.4477 20 14V20ZM14.5 15.9999C12.9615 15.9999 10.4534 15.4753 9.17918 15.1872C9.17918 15.1872 8.84483 16.1278 8.7959 16.2745L12.6465 17.2776C13.1084 17.3979 13.372 17.8839 13.2211 18.3367C13.0935 18.7194 12.7092 18.9536 12.3114 18.8865C11.0903 18.6805 8.55235 18.2299 7.25848 17.8365C5.51594 17.3066 3.71083 16.5559 2.53894 15.9342C2.53894 15.9342 2.22946 16.6189 2.19506 16.7049C2.92373 17.3031 4.32792 18.3799 6.0246 19.3086C7.76488 20.2611 9.70942 20.9999 11.5 20.9999C15.023 20.9999 17.1768 19.9555 18 19.465V15.3956C16.8681 15.7339 15.6865 15.9999 14.5 15.9999Z" fill="#0F0F0F"/>
+<path d="M12 1C11.4477 1 11 1.44772 11 2V7.58564L9.7071 6.29278C9.3166 5.9024 8.68342 5.9024 8.29292 6.29278C7.90235 6.68341 7.90235 7.31646 8.29292 7.70709L11.292 10.7063C11.6823 11.0965 12.3149 11.0968 12.7055 10.707L15.705 7.71368C16.0955 7.3233 16.0955 6.69 15.705 6.29962C15.3145 5.90899 14.6813 5.90899 14.2908 6.29962L13 7.59034V2C13 1.44772 12.5523 1 12 1Z" fill="#0F0F0F"/>
</svg> \ No newline at end of file
diff --git a/static/icons/voucher.svg b/static/icons/voucher.svg
new file mode 100644
index 00000000..48d38871
--- /dev/null
+++ b/static/icons/voucher.svg
@@ -0,0 +1,12 @@
+<?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 48 48" xmlns="http://www.w3.org/2000/svg" >
+ <path d="M0 0h48v48H0z" fill="none"/>
+<g id="Shopicon">
+ <path d="M4,16v24c0,2.2,1.8,4,4,4h32c2.2,0,4-1.8,4-4V16c0-2.2-1.8-4-4-4h-4.349C35.873,11.374,36,10.702,36,10
+ c0-3.314-2.686-6-6-6s-6,2.686-6,6c0,0.455,0.055,0.897,0.151,1.323L24,11.172l-0.151,0.151C23.945,10.897,24,10.455,24,10
+ c0-3.314-2.686-6-6-6s-6,2.686-6,6c0,0.702,0.127,1.374,0.349,2H8C5.8,12,4,13.8,4,16z M40,40H8v-5h32V40z M30,8
+ c1.103,0,2,0.897,2,2s-0.897,2-2,2s-2-0.897-2-2S28.897,8,30,8z M18,8c1.103,0,2,0.897,2,2s-0.897,2-2,2s-2-0.897-2-2
+ S16.897,8,18,8z M19.172,16l-6,6L16,24.828l8-8l8,8L34.828,22l-6-6H40v13H8V16H19.172z"/>
+</g>
+</svg> \ No newline at end of file
diff --git a/static/images/cashu.png b/static/images/cashu.png
new file mode 100644
index 00000000..19623e23
--- /dev/null
+++ b/static/images/cashu.png
Binary files differ