aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/(app)/invoice
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-09-24 20:12:43 -0700
committerAdam Soltys <[email protected]>2024-09-24 20:12:43 -0700
commitd49d4119811dee9270f0858f3f03e5d8cd0d95ba (patch)
tree42101a3ccf47f7b7290637700b90a843476e56f7 /src/routes/(app)/invoice
parent20c0c52f8b850bfbab490735b0b5cd5d08851488 (diff)
parent9531ccbdf01b07c392b1b5cb6d8ad4680ee29d2c (diff)
downloadcoinos-ui-d49d4119811dee9270f0858f3f03e5d8cd0d95ba.tar.gz
coinos-ui-d49d4119811dee9270f0858f3f03e5d8cd0d95ba.zip
Merge branch 'self-custody'
Diffstat (limited to 'src/routes/(app)/invoice')
-rw-r--r--src/routes/(app)/invoice/+page.server.ts28
-rw-r--r--src/routes/(app)/invoice/+page.svelte0
-rw-r--r--src/routes/(app)/invoice/+server.ts7
-rw-r--r--src/routes/(app)/invoice/[id]/+layout.server.ts38
-rw-r--r--src/routes/(app)/invoice/[id]/+layout.svelte1
-rw-r--r--src/routes/(app)/invoice/[id]/+page.server.ts5
-rw-r--r--src/routes/(app)/invoice/[id]/+page.svelte344
-rw-r--r--src/routes/(app)/invoice/[id]/memo/+page.server.ts13
-rw-r--r--src/routes/(app)/invoice/[id]/memo/+page.svelte95
-rw-r--r--src/routes/(app)/invoice/[id]/paid/+page.svelte85
-rw-r--r--src/routes/(app)/invoice/[id]/tip/+page.server.ts12
-rw-r--r--src/routes/(app)/invoice/[id]/tip/+page.svelte212
-rw-r--r--src/routes/(app)/invoice/sent/+page.svelte11
13 files changed, 851 insertions, 0 deletions
diff --git a/src/routes/(app)/invoice/+page.server.ts b/src/routes/(app)/invoice/+page.server.ts
new file mode 100644
index 00000000..b8a2fe06
--- /dev/null
+++ b/src/routes/(app)/invoice/+page.server.ts
@@ -0,0 +1,28 @@
+import { auth, post } from "$lib/utils";
+import { error, redirect } from "@sveltejs/kit";
+import type { Invoice } from "$lib/types";
+
+export let load = async ({ cookies, parent }) => {
+ let aid = cookies.get("aid");
+ let { subject, user, rates } = await parent();
+
+ let invoice: Invoice = {
+ aid,
+ rate: rates[user?.currency || subject?.currency],
+ };
+
+ if (!user) user = subject;
+
+ try {
+ invoice = await post("/invoice", { invoice, aid, user }, auth(cookies));
+ } catch (e) {
+ console.log(e);
+ error(500, "Failed to generate invoice");
+ }
+
+ let { id } = invoice;
+
+ if (invoice.memoPrompt && !invoice.memo) {
+ redirect(307, `/invoice/${id}/memo`);
+ } else redirect(307, `/invoice/${id}`);
+};
diff --git a/src/routes/(app)/invoice/+page.svelte b/src/routes/(app)/invoice/+page.svelte
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/routes/(app)/invoice/+page.svelte
diff --git a/src/routes/(app)/invoice/+server.ts b/src/routes/(app)/invoice/+server.ts
new file mode 100644
index 00000000..5e3d0300
--- /dev/null
+++ b/src/routes/(app)/invoice/+server.ts
@@ -0,0 +1,7 @@
+import { auth, post } from "$lib/utils";
+import { json } from "@sveltejs/kit";
+
+export let POST = async ({ cookies: c, request: r }) => {
+ let j = await r.json();
+ return json(await post("/invoice", j, auth(c)));
+};
diff --git a/src/routes/(app)/invoice/[id]/+layout.server.ts b/src/routes/(app)/invoice/[id]/+layout.server.ts
new file mode 100644
index 00000000..55c44f36
--- /dev/null
+++ b/src/routes/(app)/invoice/[id]/+layout.server.ts
@@ -0,0 +1,38 @@
+import * as Qr from "qrcode-base64";
+import { get } from "$lib/utils";
+import { redirect } from "@sveltejs/kit";
+
+export async function load({ depends, params, url, parent }) {
+ depends("app:invoice");
+
+ let { subject, user } = await parent();
+ let { id } = params;
+ let invoice;
+
+ if (id) {
+ invoice = await get(`/invoice/${id}`);
+ let options = !!url.searchParams.get("options");
+
+ if (
+ user &&
+ invoice.uid !== user.id &&
+ !(url.pathname.includes("tip") || url.pathname.includes("memo")) &&
+ !options
+ ) {
+ redirect(307, `/send/invoice/${id}`);
+ }
+
+ let { amount, pending, received } = invoice;
+ amount = parseInt(amount);
+
+ let paid =
+ (!amount && (pending || received)) ||
+ (amount > 0 && (pending >= amount || received >= amount));
+ if (paid && !url.pathname.endsWith("paid")) {
+ redirect(307, `/invoice/${id}/paid` + (options ? "?options=true" : ""));
+ }
+ }
+
+ let src = Qr.drawImg(invoice?.text || "", { size: 500 });
+ return { id, invoice, subject, src };
+}
diff --git a/src/routes/(app)/invoice/[id]/+layout.svelte b/src/routes/(app)/invoice/[id]/+layout.svelte
new file mode 100644
index 00000000..4fa864ce
--- /dev/null
+++ b/src/routes/(app)/invoice/[id]/+layout.svelte
@@ -0,0 +1 @@
+<slot />
diff --git a/src/routes/(app)/invoice/[id]/+page.server.ts b/src/routes/(app)/invoice/[id]/+page.server.ts
new file mode 100644
index 00000000..a57efa64
--- /dev/null
+++ b/src/routes/(app)/invoice/[id]/+page.server.ts
@@ -0,0 +1,5 @@
+import invoice from "$lib/invoice";
+
+export const actions = {
+ default: invoice,
+};
diff --git a/src/routes/(app)/invoice/[id]/+page.svelte b/src/routes/(app)/invoice/[id]/+page.svelte
new file mode 100644
index 00000000..5ef9e5e5
--- /dev/null
+++ b/src/routes/(app)/invoice/[id]/+page.svelte
@@ -0,0 +1,344 @@
+<script>
+ import { enhance } from "$app/forms";
+ import { send } from "$lib/socket";
+ import {
+ btc,
+ post,
+ back,
+ copy,
+ f,
+ get,
+ types,
+ sat,
+ reverseFormat,
+ s,
+ sats,
+ } from "$lib/utils";
+ import { tick, onMount, onDestroy } from "svelte";
+ import { browser } from "$app/environment";
+ import { last, showQr, amountPrompt } from "$lib/store";
+ import Avatar from "$comp/Avatar.svelte";
+ import Icon from "$comp/Icon.svelte";
+ import Heart from "$comp/Heart.svelte";
+ import Image from "$comp/Image.svelte";
+ import Numpad from "$comp/Numpad.svelte";
+ import { t } from "$lib/translations";
+ import { goto, invalidate } from "$app/navigation";
+ import Toggle from "$comp/Toggle.svelte";
+
+ export let data;
+
+ let showOptions;
+
+ let reading = async ({ message, serialNumber }) => {
+ let name = serialNumber.replace(/:/g, "-");
+ let result = await post(`/fund/${name}/withdraw`, { amount, hash, name });
+ };
+
+ let readingerror = (e) => console.log("nfc error", e);
+
+ $: refresh(data);
+ let { invoice, id, subject, user, src } = data;
+ let {
+ aid,
+ amount,
+ hash,
+ type,
+ rate,
+ text,
+ tip,
+ user: { username, currency },
+ } = invoice;
+
+ let qr;
+ let tipPercent = 0;
+
+ let refresh = async (data) => {
+ ({ invoice, id, user, src } = data);
+ ({
+ aid,
+ amount,
+ hash,
+ type,
+ rate,
+ text,
+ tip,
+ user: { username, currency },
+ } = invoice);
+
+ tipPercent = (tip / amount) * 100;
+ };
+
+ $: amountFiat = parseFloat(((amount * rate) / sats).toFixed(2));
+ $: tipAmount = ((tip * rate) / sats).toFixed(2);
+
+ let subbed;
+
+ onMount(async () => {
+ if (browser) {
+ last.subscribe((v) => {
+ if (!v || subbed) return;
+ subbed = true;
+ send("subscribe", invoice);
+ });
+
+ if ($amountPrompt && tip === null) toggleAmount();
+ }
+ });
+
+ $: link = [types.bitcoin, types.liquid].includes(type)
+ ? text
+ : `lightning:${text}`;
+ $: txt = [types.bitcoin, types.liquid].includes(type) ? hash : text;
+
+ let setType = async (type) => {
+ invoice.type = type;
+ ({ id } = await post(`/invoice`, {
+ invoice,
+ user: { username, currency },
+ }));
+
+ goto(`./${id}?options=true`, { invalidateAll: true, noScroll: true });
+ };
+
+ let setAmount = async () => {
+ if (typeof $amountPrompt === "undefined") $amountPrompt = true;
+ settingAmount = false;
+ amount = newAmount;
+ invoice.amount = newAmount;
+ invoice.tip = 0;
+ ({ id } = await post(`/invoice`, {
+ invoice,
+ user: { username, currency },
+ }));
+
+ let url = `./${id}`;
+ if (subject?.prompt) url += "/tip";
+ else url += "?options=true";
+
+ goto(url, { invalidateAll: true, noScroll: true });
+ };
+
+ let newAmount;
+ let settingAmount;
+ let toggleAmount = () => (settingAmount = !settingAmount);
+ let fiat;
+ let submit;
+</script>
+
+<div class="invoice container mx-auto max-w-xl px-4 space-y-2">
+ {#if $showQr}
+ <div>
+ <a
+ href={[types.bitcoin, types.liquid].includes(invoice.type)
+ ? invoice.text
+ : "lightning:" + invoice.text}
+ >
+ <img
+ {src}
+ class="mx-auto z-10 max-w-[360px]"
+ bind:this={qr}
+ alt={txt}
+ />
+ </a>
+ </div>
+ {:else}
+ <div class="break-all pb-5 text-center text-secondary text-xl">
+ {txt}
+ </div>
+ {/if}
+
+ {#each invoice.items as i}
+ <div class="grid grid-cols-12 text-xl">
+ <div class="col-span-1 my-auto">{i.quantity}</div>
+ <div class="mr-auto grow col-span-7 my-auto">
+ {i.name}
+ </div>
+ <div class="col-span-2 font-semibold text-right my-auto">
+ {f(i.price * i.quantity, currency)}
+ </div>
+ <div class="col-span-2 text-secondary text-right text-lg my-auto">
+ {sat(btc(i.price * i.quantity, rate))}
+ </div>
+ </div>
+ {/each}
+
+ {#if amount > 0}
+ <div class="text-center font-bold text-2xl">
+ <div>
+ {f(amountFiat, currency)}
+
+ {#if tip}
+ <span class="text-sm">
+ +{f(tip * (rate / sats), currency)}
+ </span>
+ {/if}
+ </div>
+ <div>
+ <span class="text-secondary font-normal text-xl"
+ >⚡️{`${s(amount)}`}</span
+ >
+
+ {#if tip}
+ <span class="text-sm text-secondary font-normal">
+ +⚡️{s(tip)}
+ </span>
+ {/if}
+ </div>
+ </div>
+ {/if}
+
+ <div class="text-secondary space-y-2 text-xl pt-2">
+ <button
+ type="button"
+ class="flex gap-2 text-center break-all rounded-2xl hover:opacity-80 py-5 px-6 w-full mx-auto justify-center border whitespace-nowrap mb-2"
+ on:click={() => copy(txt)}
+ >
+ <Icon icon="copy" style="w-8 my-auto" />
+ <div class="my-auto">
+ {$t("payments.copy")}
+ </div>
+ </button>
+
+ {#if user?.id === invoice?.user?.id}
+ <button
+ class="w-full flex justify-center rounded-2xl border py-5 px-6 hover:opacity-80"
+ on:click={toggleAmount}
+ >
+ <Icon icon="edit" style="mr-1 w-8" />
+ <div class="my-auto">{$t("payments.setAmount")}</div>
+ </button>
+ {:else}
+ <a href={link} class="w-full">
+ <button
+ class="w-full flex justify-center rounded-2xl border py-5 px-6 hover:opacity-80"
+ >
+ <Icon icon="mobile" style="mr-1 w-8" />
+ <div class="my-auto">{$t("payments.openLink")}</div>
+ </button>
+ </a>
+ {/if}
+
+ <div class="w-full flex justify-center gap-2 flex-wrap">
+ <button
+ class="w-full flex justify-center rounded-2xl border py-5 px-6 hover:opacity-80"
+ on:click={() => ($showQr = !$showQr)}
+ >
+ {#if $showQr}
+ <Icon icon="text" style="w-6 mr-2 my-auto" />
+ {:else}
+ <Icon icon="qr" style="w-8 mr-1 invert my-auto" />
+ {/if}
+ <div class="my-auto">
+ {$showQr ? $t("payments.showText") : $t("payments.showQr")}
+ </div></button
+ >
+ </div>
+ </div>
+
+ {#if aid === user.id}
+ <div class="grid grid-cols-2 justify-around text-secondary">
+ <button
+ class="hover:bg-primary my-auto flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center"
+ class:bg-primary={type === types.bitcoin}
+ class:text-black={type === types.bitcoin}
+ on:click={() => setType(types.bitcoin)}
+ >
+ <img src="/images/bitcoin.svg" class="w-8" alt="Bitcoin" />
+ <div class="my-auto text-lg">Bitcoin</div>
+ </button>
+
+ <button
+ class="hover:bg-primary my-auto flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center"
+ class:bg-primary={type === types.liquid}
+ class:text-black={type === types.liquid}
+ on:click={() => setType(types.liquid)}
+ >
+ <img src="/images/liquid.svg" class="w-8" alt="Liquid" />
+ <div class="my-auto text-lg">Liquid</div>
+ </button>
+
+ <button
+ class="hover:bg-primary flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center"
+ class:bg-primary={type === types.lightning}
+ class:text-black={type === types.lightning}
+ on:click={() => setType(types.lightning)}
+ >
+ <div class="bg-white rounded-full w-8 h-8 text-center flex">
+ <div class="m-auto">⚡️</div>
+ </div>
+ <div class="my-auto text-lg">Lightning</div>
+ </button>
+
+ {#if user?.id === invoice?.user?.id}
+ <a href={`/receive`} class="block contents">
+ <button
+ class="hover:bg-primary flex gap-1 sm:gap-2 py-3 px-2 sm:px-5 justify-center"
+ >
+ <img src="/images/cash.png" class="w-8 my-auto" />
+ <div class="my-auto text-lg">Ecash</div>
+ </button>
+ </a>
+ {/if}
+ </div>
+
+ {#if type === types.liquid}
+ <div class="flex justify-center">
+ <div class="my-auto text-xl text-center text-secondary">
+ {$t("payments.onlyLbtc")}
+ <span class="text-teal-500 font-bold">L-BTC</span>,
+ {$t("payments.dontDeposit")}
+ </div>
+ </div>
+ {/if}
+ {/if}
+</div>
+
+{#if settingAmount}
+ <div
+ class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"
+ >
+ <div
+ class="relative mx-auto p-12 border w-96 shadow-lg rounded-md bg-white space-y-5 top-10"
+ >
+ <form on:submit|preventDefault={setAmount}>
+ <Numpad
+ bind:amount={newAmount}
+ bind:currency
+ bind:rate
+ bind:fiat
+ bind:submit
+ />
+ <div class="w-full flex flex-wrap gap-2">
+ <button
+ bind:this={submit}
+ type="submit"
+ on:click={setAmount}
+ class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto bg-black text-white w-full"
+ >
+ <div class="my-auto">Ok</div>
+ </button>
+ <button
+ type="button"
+ class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto w-full"
+ on:click={toggleAmount}
+ on:keydown={toggleAmount}
+ >
+ <div class="my-auto">Cancel</div>
+ </button>
+ </div>
+ </form>
+
+ <div class="flex justify-center gap-3">
+ <div class="text-secondary">{$t("payments.amountPrompt")}</div>
+ <Toggle id="notify" bind:value={$amountPrompt} />
+ </div>
+ </div>
+ </div>
+{/if}
+
+<style>
+ .invoice {
+ view-transition-name: invoice;
+ }
+</style>
diff --git a/src/routes/(app)/invoice/[id]/memo/+page.server.ts b/src/routes/(app)/invoice/[id]/memo/+page.server.ts
new file mode 100644
index 00000000..24dec1af
--- /dev/null
+++ b/src/routes/(app)/invoice/[id]/memo/+page.server.ts
@@ -0,0 +1,13 @@
+import invoice from "$lib/invoice";
+import { redirect } from "@sveltejs/kit";
+
+export async function load({ parent, params }) {
+ let { invoice } = await parent();
+ if (!invoice.amount) {
+ redirect(307, `/invoice/${invoice.id}`);
+ }
+}
+
+export const actions = {
+ default: invoice,
+};
diff --git a/src/routes/(app)/invoice/[id]/memo/+page.svelte b/src/routes/(app)/invoice/[id]/memo/+page.svelte
new file mode 100644
index 00000000..4d23ff19
--- /dev/null
+++ b/src/routes/(app)/invoice/[id]/memo/+page.svelte
@@ -0,0 +1,95 @@
+<script>
+ import { onMount } from "svelte";
+ import { browser } from "$app/environment";
+ import { invoice as inv, request } from "$lib/store";
+ import {
+ copy,
+ select,
+ f,
+ sat,
+ get,
+ reverseFormat,
+ s,
+ sats,
+ } from "$lib/utils";
+ import Icon from "$comp/Icon.svelte";
+ import { t } from "$lib/translations";
+ import { enhance } from "$app/forms";
+
+ let submitting;
+ let submit;
+
+ export let data;
+ $: refresh(data);
+ let { invoice, id, user, rates } = data;
+ let {
+ amount,
+ hash,
+ items,
+ memoPrompt,
+ type,
+ rate,
+ received,
+ prompt,
+ text,
+ tip,
+ user: { username, currency },
+ } = invoice;
+
+ invoice.memo = user?.username;
+
+ let refresh = (data) => {
+ $inv = null;
+
+ ({ invoice, id } = data);
+ ({
+ amount,
+ hash,
+ items,
+ memoPrompt,
+ type,
+ received,
+ prompt,
+ text,
+ tip,
+ user: { username },
+ } = invoice);
+ };
+</script>
+
+<form
+ method="POST"
+ use:enhance
+ class:invisible={submitting}
+ class="container px-4 max-w-lg mx-auto space-y-5"
+>
+ <input type="hidden" name="amount" value={amount} />
+ <input type="hidden" name="tip" value={tip} />
+ <input type="hidden" name="username" value={username} />
+ <input type="hidden" name="rate" value={invoice.rate} />
+ <input type="hidden" name="memoPrompt" value={invoice.memoPrompt} />
+ <input type="hidden" name="prompt" value={invoice.prompt} />
+ <input type="hidden" name="type" value={type} />
+ <input type="hidden" name="hash" value={hash} />
+ <input type="hidden" name="items" value={JSON.stringify(items)} />
+
+ <h1 class="text-4xl font-semibold text-center">{$t("invoice.addMemo")}</h1>
+ <div class="space-y-2">
+ <input
+ use:select
+ name="memo"
+ class="w-full p-4 border rounded-xl text-xl"
+ bind:value={invoice.memo}
+ autocapitalize="none"
+ />
+ <div class="text-secondary">{invoice.memoPrompt}</div>
+ </div>
+
+ <button
+ type="submit"
+ bind:this={submit}
+ class="bg-black text-white border rounded-2xl px-6 py-5 w-full font-bold"
+ >
+ {$t("payments.next")}
+ </button>
+</form>
diff --git a/src/routes/(app)/invoice/[id]/paid/+page.svelte b/src/routes/(app)/invoice/[id]/paid/+page.svelte
new file mode 100644
index 00000000..e4279e48
--- /dev/null
+++ b/src/routes/(app)/invoice/[id]/paid/+page.svelte
@@ -0,0 +1,85 @@
+<script>
+ import { scale } from "svelte/transition";
+ import Icon from "$comp/Icon.svelte";
+ import { toast } from "@zerodevx/svelte-toast";
+ import { browser } from "$app/environment";
+ import { t } from "$lib/translations";
+ import { fiat, f, s, sat, sats } from "$lib/utils";
+
+ export let data;
+ let amount, currency, rate, received, pending, tip, user, id;
+ $: update(data);
+ let update = (data) => {
+ ({ amount, currency, rate, received, pending, tip, user, id } =
+ data.invoice);
+ };
+
+ toast.pop(0);
+</script>
+
+<div class="container px-4 text-center mx-auto">
+ {#if received}
+ <div
+ class="flex w-full py-5 max-w-[200px] mx-auto"
+ in:scale={{ start: 0.5 }}
+ >
+ <Icon icon="check" style="mx-auto" />
+ </div>
+
+ <h1 class="text-3xl md:text-4xl font-bold mb-6">
+ {$t("invoice.paymentSuccessful")}
+ </h1>
+
+ <h2 class="text-2xl md:text-3xl font-semibold">
+ {f(fiat(received - tip, rate), currency)}
+ {#if tip}
+ <span class="text-lg">
+ + {f(fiat(tip, rate), currency)}
+ </span>
+ {/if}
+ </h2>
+ <h3 class="text-secondary md:text-lg mb-6 mt-1">
+ ⚡️{s(received - tip)}
+
+ {#if tip}
+ <span class="text-lg">
+ + ⚡️{s(tip)}
+ </span>
+ {/if}
+ </h3>
+ {/if}
+
+ {#if pending}
+ <div
+ class="flex w-full py-5 max-w-[200px] mx-auto"
+ in:scale={{ start: 0.5 }}
+ >
+ <Icon icon="orangeclock" style="mx-auto" />
+ </div>
+
+ <h1 class="text-3xl md:text-4xl font-bold mb-6">Payment detected</h1>
+ <h2 class="text-2xl md:text-3xl font-semibold">
+ {f(fiat(pending - tip, rate), currency)}
+ {#if tip}
+ + {f(fiat(tip, rate), currency)}
+ {/if}
+ </h2>
+
+ <h3 class="text-secondary md:text-lg mb-6 mt-1">
+ ⚡️{s(pending - tip)}
+ {#if tip}
+ <span class="text-lg">
+ + ⚡️{s(tip)}
+ </span>
+ {/if}
+ </h3>
+ {/if}
+</div>
+
+<a href={`/${user.username}`}>
+ <div class="opacity-0 w-screen h-screen fixed top-0 left-0 z-50" />
+</a>
+
+<div class="fixed bottom-10 w-full text-center">
+ {$t("payments.tapAnywhere")}
+</div>
diff --git a/src/routes/(app)/invoice/[id]/tip/+page.server.ts b/src/routes/(app)/invoice/[id]/tip/+page.server.ts
new file mode 100644
index 00000000..48ebf970
--- /dev/null
+++ b/src/routes/(app)/invoice/[id]/tip/+page.server.ts
@@ -0,0 +1,12 @@
+import invoice from "$lib/invoice";
+import { redirect } from "@sveltejs/kit";
+
+export async function load({ parent }) {
+ let { invoice } = await parent();
+ if (!invoice.amount)
+ redirect(307, `/invoice/${invoice.id}`);
+}
+
+export const actions = {
+ default: invoice,
+};
diff --git a/src/routes/(app)/invoice/[id]/tip/+page.svelte b/src/routes/(app)/invoice/[id]/tip/+page.svelte
new file mode 100644
index 00000000..da2d041e
--- /dev/null
+++ b/src/routes/(app)/invoice/[id]/tip/+page.svelte
@@ -0,0 +1,212 @@
+<script>
+ import { onMount } from "svelte";
+ import { browser } from "$app/environment";
+ import { invoice as inv, request } from "$lib/store";
+ import { copy, focus, f, sat, get, reverseFormat, s, sats } from "$lib/utils";
+ import Icon from "$comp/Icon.svelte";
+ import Slider from "$comp/Slider.svelte";
+ import { goto } from "$app/navigation";
+ import { t } from "$lib/translations";
+ import { enhance } from "$app/forms";
+ import { tick } from "svelte";
+
+ const tipAmounts = ["No", "10%", "15%", "20%"];
+ let showCustomAmount;
+
+ let customInput;
+ let customTipAmount;
+
+ let apply = async () => {
+ if (customTipAmount > 0) {
+ tipPercent = (customTipAmount / amountFiat) * 100;
+ showCustomAmount = false;
+ }
+ };
+
+ let active = (amount) =>
+ !customTipAmount && Math.round(tipPercent) === parseInt(amount.slice(0, 2));
+
+ const handleCustomTipAmount = (e) => {
+ tipPercent = 0;
+ customTipAmount = e.target.value;
+ };
+
+ let timeout;
+ const handleSlide = (e) => {
+ customTipAmount = "";
+ if (customInput) {
+ customInput.value = "";
+ }
+ tipPercent = e.target.value;
+ };
+
+ let submitting;
+ const handleTipButtonClick = async (amount) => {
+ submitting = true;
+ if (amount === "No") {
+ tipPercent = 0;
+ } else {
+ tipPercent = parseInt(amount.slice(0, 2));
+ }
+
+ await tick();
+ submit.click();
+ };
+
+ let submit;
+
+ export let data;
+ $: refresh(data);
+ let { invoice, id, user, rates } = data;
+ let {
+ aid,
+ amount,
+ hash,
+ items,
+ type,
+ rate,
+ received,
+ prompt,
+ text,
+ tip,
+ user: { username, currency },
+ } = invoice;
+
+ let qr;
+ let tipPercent = 0;
+
+ let fullscreen;
+
+ let refresh = (data) => {
+ $inv = null;
+
+ ({ invoice, id } = data);
+ ({
+ aid,
+ amount,
+ hash,
+ items,
+ type,
+ received,
+ prompt,
+ text,
+ tip,
+ user: { username },
+ } = invoice);
+
+ tipPercent = (tip / amount) * 100;
+ };
+
+ $: currency = user?.currency || invoice.currency;
+ $: rate = invoice.rate * (rates[currency] / rates[invoice.currency]);
+
+ $: tip = Math.round((amount / 100) * tipPercent);
+
+ $: amountFiat = parseFloat(((amount * rate) / sats).toFixed(2));
+
+ $: tipAmount = ((tip * rate) / sats).toFixed(2);
+
+ $: invoiceAmountFiatFormatted = f(amountFiat, currency);
+
+ onMount(() => ($request = undefined));
+</script>
+
+<div class="container px-4 max-w-lg text-center mx-auto">
+ <form method="POST" use:enhance class:invisible={submitting}>
+ <input type="hidden" name="aid" value={aid} />
+ <input type="hidden" name="amount" value={amount} />
+ <input type="hidden" name="tip" value={tip} />
+ <input type="hidden" name="username" value={username} />
+ <input type="hidden" name="rate" value={invoice.rate} />
+ <input type="hidden" name="prompt" value="false" />
+ <input type="hidden" name="type" value={type} />
+ <input type="hidden" name="hash" value={hash} />
+ <input type="hidden" name="items" value={JSON.stringify(items)} />
+
+ {#if invoice.memoPrompt}
+ <input type="hidden" name="memoPrompt" value={invoice.memoPrompt} />
+ {/if}
+
+ <h1 class="text-4xl font-semibold my-8">{$t("invoice.addTipq")}</h1>
+ {#if !showCustomAmount}
+ <div class="my-2 text-xl">{Math.round(tipPercent)}%</div>
+
+ <Slider handle={handleSlide} bind:value={tipPercent} />
+
+ <div class="flex mb-8 text-lg">
+ <div>
+ <span class="mr-1">
+ {f(amountFiat, currency)}
+ <span class="font-semibold">+{f(tipAmount, currency)}</span>
+ </span>
+ </div>
+
+ <div class="ml-auto text-lg">
+ {sat(amount)}
+ <span class="font-semibold">+{sat(tip)}</span>
+ </div>
+ </div>
+
+ {#if tip}
+ <div>
+ <button type="button" on:click={() => submit.click()}>Next</button>
+ <button type="button" on:click={() => (tipPercent = 0)}>Reset</button>
+ </div>
+ {:else}
+ {#each tipAmounts as amount}
+ <button
+ type="button"
+ class:active={active(amount, tipPercent)}
+ on:click={() => handleTipButtonClick(amount)}>{amount}</button
+ >
+ {/each}
+ {/if}
+
+ <div>
+ <button type="button" on:click={() => (showCustomAmount = true)}
+ >Custom</button
+ >
+ </div>
+ {/if}
+
+ <button type="submit" bind:this={submit} />
+ </form>
+
+ {#if showCustomAmount}
+ <form on:submit|preventDefault={apply}>
+ <div class="relative">
+ <span
+ class="absolute top-[18px] left-5 font-semibold {!customTipAmount
+ ? 'opacity-50'
+ : 'opacity-100'}">$</span
+ >
+ <input
+ bind:this={customInput}
+ type="number"
+ step="0.01"
+ on:input={(e) => handleCustomTipAmount(e)}
+ class="pl-10"
+ placeholder="Custom amount"
+ use:focus
+ />
+ <button
+ type="submit"
+ class="w-full mt-2 bg-black text-white font-semibold py-3 px-7 rounded-2xl {!customTipAmount
+ ? 'opacity-50'
+ : 'opacity-100 hover:opacity-80'}"
+ disabled={!customTipAmount}>Apply</button
+ >
+ </div>
+ </form>
+ {/if}
+</div>
+
+<style>
+ .active {
+ @apply !bg-black !text-white;
+ }
+
+ .container button[type="button"] {
+ @apply w-full mt-2 bg-primary text-black hover:bg-black hover:text-white font-semibold py-5 px-7 rounded-2xl;
+ }
+</style>
diff --git a/src/routes/(app)/invoice/sent/+page.svelte b/src/routes/(app)/invoice/sent/+page.svelte
new file mode 100644
index 00000000..b95bc85e
--- /dev/null
+++ b/src/routes/(app)/invoice/sent/+page.svelte
@@ -0,0 +1,11 @@
+<script>
+ import Avatar from "$comp/Avatar.svelte";
+ import Icon from "$comp/Icon.svelte";
+ import { request } from "$lib/store";
+</script>
+
+Invoice sent to
+<div class="flex">
+ <Avatar user={$request.requester} size={12} />
+ <h1 class="my-auto"><b>{$request.requester.username}</b> is ready to pay</h1>
+</div>