aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes
diff options
context:
space:
mode:
authorJurjen de Vries <[email protected]>2025-09-11 00:48:15 +0200
committerGitHub <[email protected]>2025-09-11 00:48:15 +0200
commit0c69c9848c6b98c92609d209103efc60003c9723 (patch)
tree445121def885f13d0f80c06864ad2ff4d5ef95a1 /src/routes
parent2abcf6e435fca5d86960f3ba4228002bfb0a19e6 (diff)
parent9277b2921e09ee9dd5f39cf4af30a2330f4d95d1 (diff)
downloadcoinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.tar.gz
coinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.zip
Merge branch 'coinos:master' into master
Diffstat (limited to 'src/routes')
-rw-r--r--src/routes/(app)/+layout.svelte45
-rw-r--r--src/routes/(app)/[username]/(profile)/+layout.svelte87
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.svelte46
-rw-r--r--src/routes/(app)/[username]/accepted/[text]/+page.server.ts6
-rw-r--r--src/routes/(app)/[username]/accepted/[text]/+page.svelte57
-rw-r--r--src/routes/(app)/[username]/accepted/[text]/raw/+server.ts16
-rw-r--r--src/routes/(app)/[username]/accepted/create/+page.svelte64
-rw-r--r--src/routes/(app)/[username]/receive/+page.svelte4
-rw-r--r--src/routes/(app)/fund/[id]/+layout.server.ts12
-rw-r--r--src/routes/(app)/fund/[id]/+page.server.ts13
-rw-r--r--src/routes/(app)/fund/[id]/+page.svelte23
-rw-r--r--src/routes/(app)/fund/[id]/access/+page.server.ts3
-rw-r--r--src/routes/(app)/fund/[id]/access/+page.svelte16
-rw-r--r--src/routes/(app)/fund/[id]/authorize/[...amount]/+page.server.ts36
-rw-r--r--src/routes/(app)/fund/[id]/authorize/[...amount]/+page.svelte27
-rw-r--r--src/routes/(app)/fund/[id]/gift/+page.server.ts6
-rw-r--r--src/routes/(app)/fund/[id]/gift/+page.svelte40
-rw-r--r--src/routes/(app)/fund/[id]/sweep/+page.server.ts13
-rw-r--r--src/routes/(app)/fund/[id]/sweep/+page.svelte41
-rw-r--r--src/routes/(app)/fund/[id]/sweep/[...amount]/+page.server.ts51
-rw-r--r--src/routes/(app)/fund/[id]/sweep/[...amount]/+page.svelte36
-rw-r--r--src/routes/(app)/fund/[id]/withdraw/+page.svelte8
-rw-r--r--src/routes/(app)/funder/+page.svelte55
-rw-r--r--src/routes/(app)/invoice/[id]/+layout.server.ts26
-rw-r--r--src/routes/(app)/invoice/[id]/+page.svelte20
-rw-r--r--src/routes/(app)/invoice/[id]/tip/+page.server.ts48
-rw-r--r--src/routes/(app)/invoice/[id]/tip/+page.svelte24
-rw-r--r--src/routes/(app)/invoices/+page.svelte21
-rw-r--r--src/routes/(app)/ln/[lnurl]/+page.server.ts42
-rw-r--r--src/routes/(app)/pay/[username]/[...amount]/+page.server.ts8
-rw-r--r--src/routes/(app)/payment/[id]/+page.svelte4
-rw-r--r--src/routes/(app)/payments/[...page]/+page.svelte54
-rw-r--r--src/routes/(app)/qr/[text]/[icon]/+server.ts33
-rw-r--r--src/routes/(app)/qr/[text]/raw/+server.ts1
-rw-r--r--src/routes/(app)/scan/+page.svelte223
-rw-r--r--src/routes/(app)/send/[...text]/+page.svelte65
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/+page.svelte22
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts3
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte32
-rw-r--r--src/routes/(app)/send/ecash/+page.server.ts26
-rw-r--r--src/routes/(app)/send/ecash/+page.svelte63
-rw-r--r--src/routes/(app)/send/ecash/[request]/+page.server.ts26
-rw-r--r--src/routes/(app)/send/ecash/[request]/+page.svelte128
-rw-r--r--src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte34
-rw-r--r--src/routes/(app)/send/invoice/[id]/+page.server.ts36
-rw-r--r--src/routes/(app)/send/invoice/[id]/+page.svelte23
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts3
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte9
-rw-r--r--src/routes/(app)/settings/+layout.svelte30
-rw-r--r--src/routes/(app)/settings/account/+page.server.ts8
-rw-r--r--src/routes/(app)/settings/account/+page.svelte407
-rw-r--r--src/routes/(app)/settings/nostr/+page.svelte39
-rw-r--r--src/routes/(app)/settings/preferences/+page.server.ts11
-rw-r--r--src/routes/(app)/settings/preferences/+page.svelte291
-rw-r--r--src/routes/(app)/settings/profile/+page.server.ts5
-rw-r--r--src/routes/(app)/settings/profile/+page.svelte181
-rw-r--r--src/routes/(app)/settings/security/+page.svelte31
-rw-r--r--src/routes/(app)/settings/square/+page.server.ts18
-rw-r--r--src/routes/(app)/settings/square/+page.svelte0
-rw-r--r--src/routes/(static)/bounties/+page.server.ts18
-rw-r--r--src/routes/(static)/bounties/+page.svelte233
-rw-r--r--src/routes/(static)/docs/+page.svelte16
-rw-r--r--src/routes/(static)/login/+page.server.ts1
-rw-r--r--src/routes/(static)/login/+page.svelte2
-rw-r--r--src/routes/(static)/login/[token]/+page.server.ts13
-rw-r--r--src/routes/(static)/register/+page.server.ts39
-rw-r--r--src/routes/(static)/register/+page.svelte7
-rw-r--r--src/routes/+layout.svelte65
-rw-r--r--src/routes/+page.server.ts2
-rw-r--r--src/routes/phtest/+page.svelte1
70 files changed, 1766 insertions, 1331 deletions
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index af4dbf2c..830e5291 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -46,22 +46,35 @@
checkSocket();
$pin = getCookie("pin");
- if (window.NDEFReader && user.nfc) {
- try {
- $ndef = new NDEFReader();
- await $ndef.scan();
-
- $ndef.addEventListener("readingerror", (e) => {
- console.log("nfc error", e);
- });
-
- $ndef.addEventListener("reading", ({ message, serialNumber }) => {
- goto(`/send/${message}`);
- });
- } catch (error) {
- console.log("Argh! " + error);
- }
- }
+ // if (window.NDEFReader) {
+ // try {
+ // $ndef = new NDEFReader();
+ // await $ndef.scan();
+ //
+ // $ndef.addEventListener("readingerror", (e) => {
+ // console.log("nfc error", e);
+ // });
+ //
+ // $ndef.addEventListener("reading", ({ message, serialNumber }) => {
+ // console.log("MESSAGE", message);
+ //
+ // const record = message.records[0];
+ //
+ // if (record.recordType === "url") {
+ // const textDecoder = new TextDecoder();
+ // const url = textDecoder.decode(record.data);
+ // console.log("URL:", url);
+ //
+ // // Navigate to the URL or use it however you want
+ // goto(`/send/${encodeURIComponent(url)}`);
+ // } else {
+ // console.error("Unsupported record type:", record.recordType);
+ // }
+ // });
+ // } catch (error) {
+ // console.log("Argh! " + error);
+ // }
+ // }
}
});
diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte
index afd94ab0..b384344e 100644
--- a/src/routes/(app)/[username]/(profile)/+layout.svelte
+++ b/src/routes/(app)/[username]/(profile)/+layout.svelte
@@ -16,12 +16,13 @@
} from "$lib/utils";
import { t } from "$lib/translations";
import { sign, send } from "$lib/nostr";
- import { bech32m } from "@scure/base";
+ import { bech32 } from "@scure/base";
import { page } from "$app/stores";
+ import { PUBLIC_DOMAIN } from "$env/static/public";
let { data, children } = $props();
- let { encode, toWords } = bech32m;
+ let { encode, toWords } = bech32;
let {
events,
@@ -107,6 +108,17 @@
: `${stripped}@${$page.url.host}`,
);
let profile = $derived(`${$page.url.host}/${subject.anon ? npub : stripped}`);
+ let lnurl = $derived(
+ encode(
+ "lnurl",
+ toWords(
+ new TextEncoder().encode(
+ `https://${PUBLIC_DOMAIN}/p/${subject.anon ? npub : stripped}`,
+ ),
+ ),
+ 20000,
+ ),
+ );
$effect(() => followList.then((l) => (list = l)));
let following = $derived(list.some((t) => t.includes(subject.pubkey)));
</script>
@@ -116,19 +128,23 @@
<div
class="w-full lg:w-[280px] xl:w-[360px] lg:absolute space-y-2 left-20 mx-auto"
>
- <button
- type="button"
- class="flex text-3xl font-bold text-center mx-auto justify-center gap-2"
+ <div
+ class="flex text-3xl font-bold text-center mx-auto justify-center gap-1 items-center"
>
- <div class="flex gap-1" onclick={toggleDetails}>
- <div class="my-auto break-words">{display || username}</div>
- <iconify-icon noobserver icon="ph:qr-code-bold" width="32"
- ></iconify-icon>
- </div>
+ <button class="flex gap-1 items-center" onclick={toggleDetails}>
+ <div class="break-words">{display || username}</div>
+ <iconify-icon noobserver icon="ph:list-bold" width="32"></iconify-icon>
+ </button>
+ {#if subject.id === user?.id}
+ <a href="/settings/profile" class="btn contents">
+ <iconify-icon noobserver icon="ph:pencil-bold" width="32"
+ ></iconify-icon>
+ </a>
+ {/if}
<!-- <a href={`/${subject.pubkey}/notes`}> -->
<!-- <iconify-icon noobserver icon="ph:note-bold" width="32"></iconify-icon> -->
<!-- </a> -->
- </button>
+ </div>
{#if subject.about}
<div
@@ -169,7 +185,7 @@
<!-- </div> -->
{#if showDetails}
- <div class="space-y-5 pb-20">
+ <div class="space-y-5">
{#if lnaddr}
<div>
<div class="text-secondary">{$t("user.lightningAddress")}</div>
@@ -214,18 +230,18 @@
</div>
</div>
<div>
- <div class="text-secondary">{$t("user.nostrPubkey")}</div>
+ <div class="text-secondary">{$t("user.lnurl")}</div>
<div class="flex gap-4">
<div class="break-all grow text-xl">
- {subject.pubkey}
+ {lnurl}
</div>
- <div class="flex my-auto gap-1">
- <button class="my-auto" onclick={() => copy(subject.pubkey)}
+ <div class="flex mb-auto gap-1">
+ <button class="my-auto" onclick={() => copy(`lightning:${lnurl}`)}
><iconify-icon noobserver icon="ph:copy-bold" width="32"
></iconify-icon></button
>
<a
- href={`/qr/${encodeURIComponent(subject.pubkey)}`}
+ href={`/${n}/accepted/${encodeURIComponent(`lightning:${lnurl}`)}`}
class="my-auto"
>
<iconify-icon noobserver icon="ph:qr-code-bold" width="32"
@@ -234,24 +250,24 @@
</div>
</div>
</div>
- <div>
- <div class="text-secondary">{$t("user.nostrPubkey")}</div>
- <div class="flex gap-4">
- <div class="break-all grow text-xl">
- {npub}
- </div>
- <div class="flex my-auto gap-1">
- <button class="my-auto" onclick={() => copy(npub)}
- ><iconify-icon noobserver icon="ph:copy-bold" width="32"
- ></iconify-icon></button
- >
- <a href={`/qr/${encodeURIComponent(npub)}`} class="my-auto">
- <iconify-icon noobserver icon="ph:qr-code-bold" width="32"
- ></iconify-icon>
- </a>
- </div>
- </div>
- </div>
+ <!-- <div> -->
+ <!-- <div class="text-secondary">{$t("user.nostrPubkey")}</div> -->
+ <!-- <div class="flex gap-4"> -->
+ <!-- <div class="break-all grow text-xl"> -->
+ <!-- {npub} -->
+ <!-- </div> -->
+ <!-- <div class="flex my-auto gap-1"> -->
+ <!-- <button class="my-auto" onclick={() => copy(npub)} -->
+ <!-- ><iconify-icon noobserver icon="ph:copy-bold" width="32" -->
+ <!-- ></iconify-icon></button -->
+ <!-- > -->
+ <!-- <a href={`/qr/${encodeURIComponent(npub)}`} class="my-auto"> -->
+ <!-- <iconify-icon noobserver icon="ph:qr-code-bold" width="32" -->
+ <!-- ></iconify-icon> -->
+ <!-- </a> -->
+ <!-- </div> -->
+ <!-- </div> -->
+ <!-- </div> -->
</div>
{/if}
@@ -314,6 +330,7 @@
{#if currency}
<div
class="flex fixed w-full px-4 bg-base-100 py-2 pb-3 bottom-0 bg-opacity-90 tabular-nums"
+ style="bottom: calc(var(--safe-area-inset-bottom) + 8px);"
>
<div class="text-secondary flex mr-auto">
<div class="flex mr-1">
diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte
index bd421545..7d342994 100644
--- a/src/routes/(app)/[username]/(profile)/+page.svelte
+++ b/src/routes/(app)/[username]/(profile)/+page.svelte
@@ -6,7 +6,7 @@
import Account from "$comp/Account.svelte";
import Balance from "$comp/Balance.svelte";
import { t } from "$lib/translations";
- import { installPrompt } from "$lib/store";
+ import { installPrompt, password } from "$lib/store";
import { afterNavigate, preloadData } from "$app/navigation";
import { page } from "$app/stores";
@@ -18,6 +18,7 @@
});
let { accounts, subject, rate, user } = $derived(data);
+ let { locked } = $derived(user);
let install = async () => {
if (!$installPrompt) return;
@@ -31,6 +32,35 @@
</script>
<div class="space-y-2">
+ {#if user?.destination && !user?.autowithdraw}
+ <div class="rounded-2xl space-y-2 p-4 shadow-lg">
+ <h1 class="text-3xl font-bold">
+ {$t("user.settings.confirmAutoWithdrawal")}
+ </h1>
+ <div>
+ {$t("user.settings.confirmAutoWithdrawalDesc")}
+ </div>
+
+ <a href="/settings/account" class="btn">
+ <iconify-icon icon="ph:gear-bold" width="32"></iconify-icon>
+ Settings</a
+ >
+ </div>
+ {/if}
+ {#if user?.fresh}
+ <div>
+ {$t("user.settings.yourUsername")} <b>{user?.username}</b>
+ {#if $password}
+ <br />
+ {$t("user.settings.yourPassword")}
+ <b>{$password}</b>{/if}
+ </div>
+
+ <a href="/settings/profile" class="btn">
+ <iconify-icon icon="ph:gear-bold" width="32"></iconify-icon>
+ {$t("user.settings.continueSettingUp")}</a
+ >
+ {/if}
{#if user?.id && user.id === subject.id}
<div class="space-y-5" data-sveltekit-preload-data="false">
{#each accounts as account, i}
@@ -38,13 +68,11 @@
{/each}
</div>
- <a href={`/account/savings`} class="contents">
- <button class="btn btn-lg w-full rounded-2xl whitespace-nowrap">
- <iconify-icon noobserver icon="ph:plus-circle-bold" width="32"
- ></iconify-icon>
- {$t("accounts.addAccount")}
- </button>
- </a>
+ {#if locked}
+ <div class="text-sm">
+ {$t("incident")}
+ </div>
+ {/if}
{#if $installPrompt}
<button class="btn btn-accent lg:hidden" onclick={install}>
@@ -56,6 +84,8 @@
{/if}
</div>
+<!-- <a href="/funder" class="btn">Funder</a> -->
+
<div class="fixed inset-x-0 mx-auto flex bottom-16 px-4">
{#if user?.username !== subject.username && (!subject.anon || subject.lud16)}
<a
diff --git a/src/routes/(app)/[username]/accepted/[text]/+page.server.ts b/src/routes/(app)/[username]/accepted/[text]/+page.server.ts
new file mode 100644
index 00000000..aa25ce7f
--- /dev/null
+++ b/src/routes/(app)/[username]/accepted/[text]/+page.server.ts
@@ -0,0 +1,6 @@
+export async function load({ params, url }) {
+ const { text } = params;
+ const query = new URLSearchParams(url.search);
+ const nfc = !!query.get("nfc");
+ return { nfc, text };
+}
diff --git a/src/routes/(app)/[username]/accepted/[text]/+page.svelte b/src/routes/(app)/[username]/accepted/[text]/+page.svelte
new file mode 100644
index 00000000..1483a294
--- /dev/null
+++ b/src/routes/(app)/[username]/accepted/[text]/+page.svelte
@@ -0,0 +1,57 @@
+<script>
+ import Icon from "$comp/Icon.svelte";
+ import Qr from "$comp/Qr.svelte";
+ import { back, copy } from "$lib/utils";
+ import { bech32 } from "@scure/base";
+ import { page } from "$app/stores";
+ import { PUBLIC_DOMAIN } from "$env/static/public";
+
+ let { encode, toWords } = bech32;
+ let { data } = $props();
+ let { nfc, text } = $derived(data);
+ let { username } = $derived(data.subject);
+ let url = `https://coinos.io/p/${username.toLowerCase()}`;
+
+ let lnurl = $derived(
+ `https://${PUBLIC_DOMAIN}/ln/${encode(
+ "lnurl",
+ toWords(
+ new TextEncoder().encode(`https://${PUBLIC_DOMAIN}/p/${username}`),
+ ),
+ 20000,
+ )}`,
+ );
+</script>
+
+<div class="px-2">
+ <div
+ class="border-8 border-black p-8 max-w-[580px] mx-auto mt-20 space-y-2 rounded-3xl relative"
+ >
+ <div class="flex gap-2 mx-auto justify-center">
+ <!-- <img src="/images/bitcoin.png" class="w-16 h-16 my-auto" /> -->
+ <img src="/images/bitcoin-logo.png" class="my-auto w-[400px]" />
+ <!-- <h3 class="text-5xl font-bold text-center my-auto"> -->
+ <!-- bitcoin -->
+ <!-- </h3> -->
+ </div>
+ {#if nfc}
+ <img
+ src="/images/tap.png"
+ class="w-24 absolute top-52 left-4 rotate-[-10deg]"
+ />
+ <img
+ src="/images/arrow.png"
+ class="w-24 scale-x-[-1] rotate-[120deg] absolute top-52 left-32 w-20 h-52"
+ />
+ {/if}
+ <Qr {text} icon="icon.png" />
+ <div class="text-center text-4xl font-bold break-all">
+ coinos.io/{username.toLowerCase()}
+ </div>
+ </div>
+
+ <button onclick={() => copy(lnurl)} class="flex gap-1 m-auto my-8">
+ <iconify-icon icon="ph:copy-bold" width={32}></iconify-icon>
+ LNURL</button
+ >
+</div>
diff --git a/src/routes/(app)/[username]/accepted/[text]/raw/+server.ts b/src/routes/(app)/[username]/accepted/[text]/raw/+server.ts
new file mode 100644
index 00000000..a24d3a3c
--- /dev/null
+++ b/src/routes/(app)/[username]/accepted/[text]/raw/+server.ts
@@ -0,0 +1,16 @@
+import Qr from "qrcode-base64";
+
+export async function GET({ params }) {
+ const { text } = params;
+
+ const dataUri = Qr.drawImg(text, { size: 600 });
+ const base64Data = dataUri.split(",")[1];
+ const binaryData = Buffer.from(base64Data, "base64");
+
+ return new Response(binaryData, {
+ headers: {
+ "Content-Type": "image/gif",
+ "Cache-Control": "public, max-age=31536000, immutable",
+ },
+ });
+}
diff --git a/src/routes/(app)/[username]/accepted/create/+page.svelte b/src/routes/(app)/[username]/accepted/create/+page.svelte
new file mode 100644
index 00000000..74316ba9
--- /dev/null
+++ b/src/routes/(app)/[username]/accepted/create/+page.svelte
@@ -0,0 +1,64 @@
+<script>
+ import { run, preventDefault } from 'svelte/legacy';
+
+ import { goto } from "$app/navigation";
+ import { browser } from "$app/environment";
+ import { t } from "$lib/translations";
+ import Icon from "$comp/Icon.svelte";
+ let el = $state(), text = $state(), pasted = $state();
+
+ let keypress = (e) =>
+ e.key === "Enter" ? e.preventDefault() || el.click() : (pasted = false);
+
+ let paste = async () => {
+ text = await navigator.clipboard.readText();
+ pasted = true;
+ };
+
+ let submit = () => goto(`/qr/${encodeURIComponent(text)}`);
+
+ run(() => {
+ if (browser && pasted && text) el.click() && (pasted = false);
+ });
+</script>
+
+<div class="container px-4 max-w-lg mx-auto space-y-5 mt-20">
+ <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold">
+ Create QR
+ </h1>
+
+ <form onsubmit={preventDefault(submit)} class="space-y-2 text-xl">
+ <div class="mb-2">
+ <textarea
+ use:focus
+ name="text"
+ placeholder="Enter some text or a URL to put in a QR"
+ onkeypress={keypress}
+ class="w-full p-4 border rounded-xl h-32 text-xl"
+ bind:value={text}
+ onpaste={() => (pasted = true)}
+ autocapitalize="none"
+></textarea>
+ </div>
+
+ <div class="flex gap-2">
+ <button
+ type="button"
+ class="flex border rounded-2xl px-6 py-5 font-bold hover:opacity-80 w-full bg-primary justify-center gap-2"
+ onclick={paste}
+ >
+ <Icon icon="paste" style="w-8 my-auto" />
+ <div class="my-auto">{$t("user.send.paste")}</div>
+ </button>
+
+ <button
+ bind:this={el}
+ type="submit"
+ class="flex bg-black text-white border rounded-2xl px-6 py-5 w-full font-bold gap-2 justify-center"
+ >
+ <Icon icon="send" style="w-8 my-auto invert" />
+ <div class="my-auto">{$t("user.dashboard.go")}</div>
+ </button>
+ </div>
+ </form>
+</div>
diff --git a/src/routes/(app)/[username]/receive/+page.svelte b/src/routes/(app)/[username]/receive/+page.svelte
index b5b3153b..cd692abf 100644
--- a/src/routes/(app)/[username]/receive/+page.svelte
+++ b/src/routes/(app)/[username]/receive/+page.svelte
@@ -94,12 +94,12 @@
if (typeof $amountPrompt === "undefined") $amountPrompt = true;
amount = newAmount;
invoice.amount = newAmount;
- invoice.tip = 0;
+ invoice.tip = null;
await update();
};
- let newAmount = $state();
+ let newAmount = $state(0);
let settingAmount = $state();
let fiat = $state(true);
let toggleAmount = () => (settingAmount = !settingAmount);
diff --git a/src/routes/(app)/fund/[id]/+layout.server.ts b/src/routes/(app)/fund/[id]/+layout.server.ts
new file mode 100644
index 00000000..3669eb7d
--- /dev/null
+++ b/src/routes/(app)/fund/[id]/+layout.server.ts
@@ -0,0 +1,12 @@
+import getRates from "$lib/rates";
+import { get } from "$lib/utils";
+
+export const load = async ({ depends, params: { id }, parent }) => {
+ depends("app:payments");
+ const data = await get(`/fund/${id}`);
+ data.managers = await get(`/fund/${id}/managers`);
+ const rates = await getRates();
+ const { user } = await parent();
+ data.rate = rates[user?.currency || "USD"];
+ return data;
+};
diff --git a/src/routes/(app)/fund/[id]/+page.server.ts b/src/routes/(app)/fund/[id]/+page.server.ts
index a4f402af..cd2bd979 100644
--- a/src/routes/(app)/fund/[id]/+page.server.ts
+++ b/src/routes/(app)/fund/[id]/+page.server.ts
@@ -1,17 +1,6 @@
-import getRates from "$lib/rates";
-import { auth, get, post } from "$lib/utils";
+import { auth, post } from "$lib/utils";
import { redirect } from "@sveltejs/kit";
-export const load = async ({ depends, params: { id }, parent }) => {
- depends("app:payments");
- const data = await get(`/fund/${id}`);
- data.managers = await get(`/fund/${id}/managers`);
- const rates = await getRates();
- const { user } = await parent();
- data.rate = rates[user?.currency || "USD"];
- return data;
-};
-
export const actions = {
default: async ({ cookies, locals, params }) => {
const { user } = locals;
diff --git a/src/routes/(app)/fund/[id]/+page.svelte b/src/routes/(app)/fund/[id]/+page.svelte
index 6cb3b05d..da869fed 100644
--- a/src/routes/(app)/fund/[id]/+page.svelte
+++ b/src/routes/(app)/fund/[id]/+page.svelte
@@ -69,13 +69,15 @@
{/if}
</div>
- <a
- href={`/fund/${id}/access`}
- class="btn !w-auto ml-auto text-secondary grow"
- >
- <iconify-icon icon="ph:gear-bold" width={32}></iconify-icon>
- {$t("funds.manage")}
- </a>
+ {#if (user && !managers.length) || managers.some((m) => m.id === user?.id)}
+ <a
+ href={`/fund/${id}/access`}
+ class="btn !w-auto ml-auto text-secondary grow"
+ >
+ <iconify-icon icon="ph:gear-bold" width={32}></iconify-icon>
+ {$t("funds.manage")}
+ </a>
+ {/if}
</div>
<div class="flex flex-wrap gap-2" data-sveltekit-prefetch="off">
<div class="grow">
@@ -87,7 +89,7 @@
</button>
</a>
</div>
- {#if !managers.length || managers.some((m) => m.id === user.id)}
+ {#if !managers.length || managers.some((m) => m.id === user?.id)}
<div class="grow">
<a href={`/fund/${id}/withdraw`}>
<button class="btn">
@@ -109,6 +111,11 @@
></iconify-icon>
<div class="my-auto">{$t("payments.shareLink")}</div>
</a>
+ <a href={`${$page.url.pathname}/gift`} class="btn !w-auto grow">
+ <iconify-icon noobserver icon="ph:gift-bold" width="32"
+ ></iconify-icon>
+ <div class="my-auto">{$t("payments.giftLink")}</div>
+ </a>
<!-- <a href={`/qr/${lnurlw}`} class="btn !w-auto grow"> -->
<!-- <iconify-icon -->
<!-- noobserver -->
diff --git a/src/routes/(app)/fund/[id]/access/+page.server.ts b/src/routes/(app)/fund/[id]/access/+page.server.ts
index c38cb29b..36191643 100644
--- a/src/routes/(app)/fund/[id]/access/+page.server.ts
+++ b/src/routes/(app)/fund/[id]/access/+page.server.ts
@@ -11,11 +11,8 @@ export const actions = {
const form = await fd(request);
try {
- console.log("SUBMITTING", form);
const managers = await post("/fund/managers", form, auth(cookies));
- console.log(managers);
-
return { managers };
} catch (e) {
const { message } = e as Error;
diff --git a/src/routes/(app)/fund/[id]/access/+page.svelte b/src/routes/(app)/fund/[id]/access/+page.svelte
index f18d6357..cc2921c5 100644
--- a/src/routes/(app)/fund/[id]/access/+page.svelte
+++ b/src/routes/(app)/fund/[id]/access/+page.svelte
@@ -1,15 +1,20 @@
<script>
- import { fail, post } from "$lib/utils";
+ import { focus, fail, post } from "$lib/utils";
import { enhance } from "$app/forms";
import { t } from "$lib/translations";
import Avatar from "$comp/Avatar.svelte";
let { data } = $props();
let { id, managers } = $derived(data);
+ let m = $state(data.managers);
+ $effect(() => (m = data.managers));
let username = $state();
- let del = async ({ id }) => {
+ let del = async (e, user) => {
+ e.preventDefault();
try {
- await post("/post/fund/access/delete", { id });
+ m = await post(`/post/fund/${id}/managers/delete`, {
+ id: user.id,
+ });
} catch (e) {
fail(e.message);
}
@@ -25,14 +30,14 @@
Fund managers
</h1>
- {#each managers as c}
+ {#each m as c}
<a href={`/${c.username}`} class="contents">
<div class="flex hover:bg-base-200 p-2 items-center">
<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>
- <button onclick={() => del(c)} class="ml-auto">
+ <button onclick={(e) => del(e, c)} class="ml-auto">
<iconify-icon icon="ph:trash-bold" width="32"></iconify-icon>
</button>
</div>
@@ -42,6 +47,7 @@
<form method="post" use:enhance class="space-y-2">
<input type="hidden" name="id" value={id} />
<input
+ use:focus
name="username"
bind:value={username}
placeholder={$t("funds.username")}
diff --git a/src/routes/(app)/fund/[id]/authorize/[...amount]/+page.server.ts b/src/routes/(app)/fund/[id]/authorize/[...amount]/+page.server.ts
new file mode 100644
index 00000000..e5683ae6
--- /dev/null
+++ b/src/routes/(app)/fund/[id]/authorize/[...amount]/+page.server.ts
@@ -0,0 +1,36 @@
+import getRates from "$lib/rates";
+import { auth, get, post, sats } from "$lib/utils";
+import { error, redirect } from "@sveltejs/kit";
+
+export async function load({ cookies, params, parent }) {
+ const { user } = await parent();
+ const { id } = params;
+ let [amount, currency] = params.amount.split("/");
+ let fiat;
+
+ if (!currency) currency = user?.currency;
+ if (currency) currency = currency.toUpperCase();
+
+ const rates = await getRates();
+ const rate = rates[currency || "USD"];
+ if (currency && !rate) error(500, "Invalid currency symbol");
+
+ if (!amount) {
+ const balance = await get(`/fund/${id}`);
+ amount = balance.amount;
+ } else if (currency) {
+ fiat = amount;
+ amount = Math.round((fiat * sats) / rate);
+ }
+
+ if (!amount) redirect(307, `/fund/${id}`);
+
+ try {
+ await post("/authorize", { amount, currency, fiat, id }, auth(cookies));
+ } catch (e) {
+ console.log("caught", e);
+ redirect(307, `/fund/${id}`);
+ }
+
+ return { amount, currency, id, rate, rates };
+}
diff --git a/src/routes/(app)/fund/[id]/authorize/[...amount]/+page.svelte b/src/routes/(app)/fund/[id]/authorize/[...amount]/+page.svelte
new file mode 100644
index 00000000..b94c1ce9
--- /dev/null
+++ b/src/routes/(app)/fund/[id]/authorize/[...amount]/+page.svelte
@@ -0,0 +1,27 @@
+<script>
+ import { onMount } from "svelte";
+ import Amount from "$comp/Amount.svelte";
+ import Success from "$comp/Success.svelte";
+ import { scale } from "svelte/transition";
+ import { toFiat, f, s } from "$lib/utils";
+ import Icon from "$comp/Icon.svelte";
+ import { t } from "$lib/translations";
+ import { loc } from "$lib/utils";
+
+ let { data } = $props();
+
+ let { amount, currency, id, user, rate, rates } = data;
+ let locale = $derived(loc(user));
+</script>
+
+<div class="container px-4 text-center mx-auto">
+ <Success {amount} {rate} {currency} {locale} title="Authorized" />
+</div>
+
+<a href={`/fund/${id}`} aria-label="Continue">
+ <div class="opacity-0 w-screen h-screen fixed top-24 left-0 z-50"></div>
+</a>
+
+<div class="flex justify-center mt-4">
+ {$t("payments.tapAnywhere")}
+</div>
diff --git a/src/routes/(app)/fund/[id]/gift/+page.server.ts b/src/routes/(app)/fund/[id]/gift/+page.server.ts
new file mode 100644
index 00000000..bd39d82b
--- /dev/null
+++ b/src/routes/(app)/fund/[id]/gift/+page.server.ts
@@ -0,0 +1,6 @@
+export const load = async ({ params, parent }) => {
+ const p = await parent();
+ const { amount, rate } = p;
+ const { id } = params;
+ return { amount, id, rate };
+};
diff --git a/src/routes/(app)/fund/[id]/gift/+page.svelte b/src/routes/(app)/fund/[id]/gift/+page.svelte
new file mode 100644
index 00000000..7c65d314
--- /dev/null
+++ b/src/routes/(app)/fund/[id]/gift/+page.svelte
@@ -0,0 +1,40 @@
+<script>
+ import Amount from "$comp/Amount.svelte";
+ import { t } from "$lib/translations";
+ import { page } from "$app/stores";
+ import Icon from "$comp/Icon.svelte";
+ import Qr from "$comp/Qr.svelte";
+ import { loc, copy } from "$lib/utils";
+
+ let { data } = $props();
+ let { id, user, amount, rate } = $derived(data);
+ let text = $page.url.href.replace("gift", "sweep");
+ let currency = user ? user.currency : "CAD";
+ let locale = loc(user);
+</script>
+
+<div class="container px-4 max-w-4xl mx-auto space-y-5 pb-10">
+ <h1 class="text-center text-2xl font-semibold">{$t("payments.gift")}</h1>
+
+ <Amount {amount} {currency} {rate} {locale} />
+
+ <div class="text-secondary shadow-lg p-8 mx-auto text-center space-y-2">
+ <p>{$t("payments.giftDesc1")}</p>
+ <p>{$t("payments.giftDesc2")}</p>
+ </div>
+
+ <div class="flex gap-1 min-w-0 items-center">
+ <button
+ type="button"
+ class="btn break-all !h-auto flex-nowrap p-4 font-normal shrink !w-auto max-w-full"
+ onclick={() => copy(text)}
+ >
+ <iconify-icon noobserver icon="ph:copy-bold" width="32"></iconify-icon>
+ <div class="my-auto break-all min-w-0 !w-auto max-w-full">{text}</div>
+ </button>
+
+ <a href={`/qr/${encodeURIComponent(text)}`} class="btn !w-auto">
+ <iconify-icon icon="ph:qr-code-bold" width={32}></iconify-icon>
+ </a>
+ </div>
+</div>
diff --git a/src/routes/(app)/fund/[id]/sweep/+page.server.ts b/src/routes/(app)/fund/[id]/sweep/+page.server.ts
deleted file mode 100644
index 5bb42fa4..00000000
--- a/src/routes/(app)/fund/[id]/sweep/+page.server.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { get, auth, post } from "$lib/utils";
-import { redirect } from "@sveltejs/kit";
-
-export async function load({ cookies, params, parent }) {
- const { user } = await parent();
- const { id } = params;
- const { amount } = await get(`/fund/${id}`);
-
- if (!amount) redirect(307, `/fund/${id}`);
- if (!user) redirect(307, `/register?redirect=/fund/${id}/sweep`);
- await post("/take", { amount, id }, auth(cookies));
- return { amount, id };
-}
diff --git a/src/routes/(app)/fund/[id]/sweep/+page.svelte b/src/routes/(app)/fund/[id]/sweep/+page.svelte
deleted file mode 100644
index cd89f6ae..00000000
--- a/src/routes/(app)/fund/[id]/sweep/+page.svelte
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
- import { scale } from "svelte/transition";
- import { toFiat, f, s } from "$lib/utils";
- import Icon from "$comp/Icon.svelte";
- import { t } from "$lib/translations";
-
- let { data } = $props();
-
- let { amount, id, user, rates } = data;
- let { currency } = user;
- let rate = $derived(rates[currency]);
-</script>
-
-<div class="container px-4 max-w-xl mx-auto mt-10 space-y-5 text-center">
- <div class="w-full flex">
- <a href="/" class="mx-auto">
- <Icon icon="logo" />
- </a>
- </div>
-
- <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("payments.fundsClaimed")}
- </h1>
- <h2 class="text-2xl md:text-3xl font-semibold">
- {f(toFiat(amount, rate), currency)}
- </h2>
- <h3 class="text-secondary md:text-lg mb-6 mt-1">
- ⚡️{s(amount)}
- </h3>
-</div>
-
-<a href={`/payments`}>
- <div class="opacity-0 w-screen h-screen fixed top-0 left-0 z-50"></div>
-</a>
-
-<div class="flex justify-center">
- {$t("payments.tapAnywhere")}
-</div>
diff --git a/src/routes/(app)/fund/[id]/sweep/[...amount]/+page.server.ts b/src/routes/(app)/fund/[id]/sweep/[...amount]/+page.server.ts
new file mode 100644
index 00000000..78df3461
--- /dev/null
+++ b/src/routes/(app)/fund/[id]/sweep/[...amount]/+page.server.ts
@@ -0,0 +1,51 @@
+import { randomName, randomPassword } from "$lib/random";
+import getRates from "$lib/rates";
+import { auth, get, post, register, sats } from "$lib/utils";
+import { error, redirect } from "@sveltejs/kit";
+
+export async function load({ cookies, request, params, parent }) {
+ let { user } = await parent();
+ const { id } = params;
+ let [amount, currency] = params.amount.split("/");
+
+ if (!currency) currency = user?.currency;
+ if (currency) currency = currency.toUpperCase();
+ const rates = await getRates();
+ const rate = rates[currency || "USD"];
+ if (currency && !rate) error(500, "Invalid currency symbol");
+
+ if (!amount) {
+ const balance = await get(`/fund/${id}`);
+ amount = balance.authorization || balance.amount;
+ } else if (currency) {
+ amount = Math.round((amount * sats) / rate);
+ }
+
+ const username = randomName() + Math.floor(Math.random() * 99) + 1;
+ const password = randomPassword();
+
+ if (!amount) redirect(307, `/fund/${id}`);
+ if (!user) {
+ const ip = request.headers.get("cf-connecting-ip");
+
+ user = {
+ username,
+ password,
+ fresh: true,
+ };
+
+ let redirectUrl = `/fund/${id}/sweep`;
+ if (params.amount && !params.currency) redirectUrl += `/${params.amount}`;
+ if (params.currency) redirectUrl += `/${params.amount}/params.currency}`;
+
+ await register(user, ip, cookies, redirectUrl);
+ }
+
+ try {
+ ({ amount } = await post("/take", { amount, id }, auth(cookies)));
+ } catch (e) {
+ redirect(307, `/fund/${id}`);
+ }
+
+ return { amount, currency, id, rate, rates, password };
+}
diff --git a/src/routes/(app)/fund/[id]/sweep/[...amount]/+page.svelte b/src/routes/(app)/fund/[id]/sweep/[...amount]/+page.svelte
new file mode 100644
index 00000000..7a7d5402
--- /dev/null
+++ b/src/routes/(app)/fund/[id]/sweep/[...amount]/+page.svelte
@@ -0,0 +1,36 @@
+<script>
+ import { onMount } from "svelte";
+ import { password } from "$lib/store";
+ import Amount from "$comp/Amount.svelte";
+ import Success from "$comp/Success.svelte";
+ import { scale } from "svelte/transition";
+ import { toFiat, f, s } from "$lib/utils";
+ import Icon from "$comp/Icon.svelte";
+ import { t } from "$lib/translations";
+ import { loc } from "$lib/utils";
+
+ let { data } = $props();
+
+ let { amount, currency, id, user, rate, rates, password: pw } = data;
+ let locale = $derived(loc(user));
+
+ onMount(() => ($password = pw));
+</script>
+
+<div class="container px-4 text-center mx-auto">
+ <Success
+ {amount}
+ {rate}
+ {currency}
+ {locale}
+ title={$t("payments.received")}
+ />
+</div>
+
+<a href={`/${user.username}`} aria-label="Continue">
+ <div class="opacity-0 w-screen h-screen fixed top-24 left-0 z-50"></div>
+</a>
+
+<div class="flex justify-center mt-4">
+ {$t("payments.tapAnywhere")}
+</div>
diff --git a/src/routes/(app)/fund/[id]/withdraw/+page.svelte b/src/routes/(app)/fund/[id]/withdraw/+page.svelte
index 78f9b16c..69a74a3f 100644
--- a/src/routes/(app)/fund/[id]/withdraw/+page.svelte
+++ b/src/routes/(app)/fund/[id]/withdraw/+page.svelte
@@ -6,15 +6,17 @@
import Numpad from "$comp/Numpad.svelte";
import Spinner from "$comp/Spinner.svelte";
import { page } from "$app/stores";
- import { s } from "$lib/utils";
+ import { loc, s } from "$lib/utils";
import { fiat, pin } from "$lib/store";
let { data, form } = $props();
- let { currency } = $derived(data.user);
+ let { user } = $derived(data);
+ let { currency } = $derived(user);
let loading = $state();
let { id, balance, rate } = $derived(data);
let { amount } = $state(data);
+ let locale = loc(user);
let submit = $state();
let submitting = $state();
@@ -39,7 +41,7 @@
{/if}
<div class="container px-4 mt-20 max-w-xl mx-auto">
- <Numpad bind:amount {currency} {fiat} {rate} {submit} />
+ <Numpad bind:amount {currency} {fiat} {rate} {submit} {locale} />
<form use:enhance={handler(toggle)} method="POST" class="space-y-5">
<input name="id" value={id} type="hidden" />
diff --git a/src/routes/(app)/funder/+page.svelte b/src/routes/(app)/funder/+page.svelte
new file mode 100644
index 00000000..e6549360
--- /dev/null
+++ b/src/routes/(app)/funder/+page.svelte
@@ -0,0 +1,55 @@
+<script lang="ts">
+ import { onMount } from 'svelte';
+let here = false;
+ let ready = false;
+ let success = false;
+ let controller: AbortController | null = null;
+ let NfcWriter: any;
+
+ async function write() {
+ NfcWriter = window.Capacitor?.Plugins?.NfcWriter;
+ ready = true;
+ console.log(window.Capacitor);
+ if (!NfcWriter) return;
+ here = true;
+
+ try {
+ await NfcWriter.enableExclusiveNfcMode();
+
+ if (controller) controller.abort();
+ controller = new AbortController();
+
+ ready = true;
+
+ const ndef = new NDEFReader();
+ await ndef.write(
+ {
+ records: [
+ {
+ recordType: 'url',
+ data: 'https://coinos.io',
+ },
+ ],
+ },
+ { signal: controller.signal }
+ );
+
+ success = true;
+ } catch (err) {
+ if (err.name === 'AbortError') {
+ console.warn('Previous write aborted');
+ } else {
+ console.error('Write failed', err);
+ alert('❌ Write failed: ' + err.message);
+ }
+ } finally {
+ await NfcWriter.disableExclusiveNfcMode();
+ }
+ }
+</script>
+
+HI
+<button class="p-4 btn" on:click={write}>write</button>
+{#if here}HERE{/if}
+{#if ready}go{/if}
+{#if success}done{/if}
diff --git a/src/routes/(app)/invoice/[id]/+layout.server.ts b/src/routes/(app)/invoice/[id]/+layout.server.ts
index 590e5d4d..ab41dddc 100644
--- a/src/routes/(app)/invoice/[id]/+layout.server.ts
+++ b/src/routes/(app)/invoice/[id]/+layout.server.ts
@@ -12,19 +12,8 @@ export async function load({ cookies, depends, params, url, parent }) {
if (id) {
invoice = await get(`/invoice/${id}`);
const 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);
- subject = invoice.user;
const paid =
(!amount && (pending || received)) ||
@@ -32,6 +21,21 @@ export async function load({ cookies, depends, params, url, parent }) {
if (paid && !url.pathname.endsWith("paid")) {
redirect(307, `/invoice/${id}/paid` + (options ? "?options=true" : ""));
}
+
+ subject = invoice.user;
+
+ if (
+ user &&
+ invoice.uid !== user?.id &&
+ !(url.pathname.includes("tip") || url.pathname.includes("memo")) &&
+ !options
+ ) {
+ if (invoice.prompt && invoice.tip === null)
+ redirect(307, `/invoice/${id}/tip`);
+ else {
+ redirect(307, `/send/invoice/${id}`);
+ }
+ }
}
const theme = cookies.get("theme") || "light";
diff --git a/src/routes/(app)/invoice/[id]/+page.svelte b/src/routes/(app)/invoice/[id]/+page.svelte
index 0cb5aeac..29dd815d 100644
--- a/src/routes/(app)/invoice/[id]/+page.svelte
+++ b/src/routes/(app)/invoice/[id]/+page.svelte
@@ -56,16 +56,18 @@
let subbed = false;
- onMount(async () => {
- if (browser) {
- last.subscribe((v) => {
- if (!v || subbed) return;
- subbed = true;
- send("subscribe", invoice);
- });
+ $effect(() => {
+ if (!browser) return;
+ last.subscribe((v) => {
+ if (!v || subbed) return;
+ subbed = true;
+ send("subscribe", invoice);
+ });
+ });
- if ($amountPrompt && !amount) toggleAmount();
- }
+ onMount(async () => {
+ if (!browser) return;
+ if ($amountPrompt && !amount) toggleAmount();
});
let update = async () => {
diff --git a/src/routes/(app)/invoice/[id]/tip/+page.server.ts b/src/routes/(app)/invoice/[id]/tip/+page.server.ts
index c3be68f0..851d3f1b 100644
--- a/src/routes/(app)/invoice/[id]/tip/+page.server.ts
+++ b/src/routes/(app)/invoice/[id]/tip/+page.server.ts
@@ -1,11 +1,36 @@
-import invoice from "$lib/invoice";
import getRates from "$lib/rates";
-import { redirect } from "@sveltejs/kit";
+import { auth, fd, get, post } from "$lib/utils";
+import { fail, redirect } from "@sveltejs/kit";
-export async function load({ parent }) {
+export async function load({ cookies, params, parent }) {
+ const { id } = params;
const rates = await getRates();
- const { invoice, subject } = await parent();
+ let { invoice, subject, user } = await parent();
if (!invoice.amount) redirect(307, `/invoice/${invoice.id}`);
+ const pin = cookies.get("pin");
+
+ if (user) {
+ const trust = await get("/trust", auth(cookies));
+ const trusted = trust.includes(invoice.uid);
+ if (trusted && (pin || !user.haspin)) {
+ let p;
+ try {
+ if (invoice.tip === null && invoice.user.prompt) {
+ if (user.tip > 0) {
+ invoice.tip = Math.round(invoice.amount * (user.tip / 100));
+ invoice = await post(`/invoice/${id}`, { invoice }, auth(cookies));
+ p = await post("/payments", { ...invoice, pin }, auth(cookies));
+ }
+ } else {
+ p = await post("/payments", { ...invoice, pin }, auth(cookies));
+ }
+ } catch (e) {
+ const { message } = e as Error;
+ fail(400, { message });
+ }
+ if (p) redirect(307, `/sent/${p.id}`);
+ }
+ }
const rate = rates[subject?.currency];
const invoiceRate = rates[invoice.currency];
@@ -13,5 +38,18 @@ export async function load({ parent }) {
}
export const actions = {
- default: invoice,
+ default: async ({ cookies, request }) => {
+ const form = await fd(request);
+ const invoice = {
+ tip: form.tip ?? null,
+ };
+
+ const { id } = await post(
+ `/invoice/${form.id}`,
+ { invoice },
+ auth(cookies),
+ );
+
+ redirect(307, `/invoice/${id}`);
+ },
};
diff --git a/src/routes/(app)/invoice/[id]/tip/+page.svelte b/src/routes/(app)/invoice/[id]/tip/+page.svelte
index d609d86a..a1ea3586 100644
--- a/src/routes/(app)/invoice/[id]/tip/+page.svelte
+++ b/src/routes/(app)/invoice/[id]/tip/+page.svelte
@@ -97,19 +97,8 @@
<div class="container px-4 max-w-lg text-center mx-auto">
<form method="POST" use:enhance class="space-y-5">
- <input type="hidden" name="aid" value={aid} />
- <input type="hidden" name="amount" value={amount} />
+ <input type="hidden" name="id" value={id} />
<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}
@@ -127,7 +116,10 @@
<div class="ml-auto text-lg flex gap-2">
<div class="flex items-center">
- <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300"
+ <iconify-icon
+ noobserver
+ icon="ph:lightning-fill"
+ class="text-yellow-300"
></iconify-icon>
<div>{s(amount, locale)}</div>
</div>
@@ -170,12 +162,14 @@
{/if}
</div>
- <div>
+ <div class="space-y-2">
<button
type="button"
class="btn"
- onclick={() => (showCustomAmount = true)}>Custom</button
+ onclick={() => (showCustomAmount = true)}
+ >{$t("payments.custom")}</button
>
+ <a href={`/invoice/${id}`} class="btn">{$t("payments.cancel")}</a>
</div>
{/if}
diff --git a/src/routes/(app)/invoices/+page.svelte b/src/routes/(app)/invoices/+page.svelte
index 8de240c2..fbacb2df 100644
--- a/src/routes/(app)/invoices/+page.svelte
+++ b/src/routes/(app)/invoices/+page.svelte
@@ -7,17 +7,28 @@
<div class="container w-full mx-auto text-lg px-2 space-y-2">
<div class="mx-auto flex justify-center w-full gap-1">
- <div class="grid grid-cols-12 w-full text-center text-lg break-all gap-2">
+ <div
+ class="grid md:grid-cols-12 w-full text-center text-lg break-all gap-x-2"
+ >
{#each invoices as i}
- <div class="col-span-2 whitespace-nowrap">{i.type}</div>
+ <div class="md:col-span-2 whitespace-nowrap mt-8 md:mt-0">{i.type}</div>
{#if show[i.id]}
- <div class="col-span-8">{i.hash}</div>
+ <div class="md:col-span-8">{i.hash}</div>
{:else}
- <div class="cursor-pointer grow col-span-8" onclick={() => display(i.id)}>
+ <div
+ class="cursor-pointer grow md:col-span-6"
+ onclick={() => display(i.id)}
+ >
{i.hash.substr(0, 12)}
+ {#if !show[i.id]}...{/if}
</div>
{/if}
- <div class="col-span-2 whitespace-nowrap">{i.received} / {i.amount}</div>
+ <div class="md:col-span-2 whitespace-nowrap">
+ {i.received} / {i.amount}
+ </div>
+ <div class="whitespace-nowrap">
+ {i.memo?.startsWith("[") ? JSON.parse(i.memo)[0][1] : i.memo}
+ </div>
{/each}
</div>
</div>
diff --git a/src/routes/(app)/ln/[lnurl]/+page.server.ts b/src/routes/(app)/ln/[lnurl]/+page.server.ts
index c113e03f..7348f96c 100644
--- a/src/routes/(app)/ln/[lnurl]/+page.server.ts
+++ b/src/routes/(app)/ln/[lnurl]/+page.server.ts
@@ -1,25 +1,60 @@
+import { PUBLIC_DOMAIN } from "$env/static/public";
import getRates from "$lib/rates";
import { auth, fd, get, post } from "$lib/utils";
+import { bech32 } from "@scure/base";
import { error, fail, redirect } from "@sveltejs/kit";
+const { decode, fromWords } = bech32;
export async function load({ params, parent }) {
const { user } = await parent();
- if (!user) redirect(307, "/register");
const rates = await getRates();
let data;
+ const { lnurl } = params;
+
+ const url = Buffer.from(fromWords(decode(lnurl, 20000).words)).toString();
+
try {
- const { lnurl } = params;
data = await get(`/decode?text=${lnurl}`);
} catch (e) {
const { message } = e as Error;
error(500, message);
}
+ let { callback, minSendable, maxSendable, comment, tag } = data;
+ if (tag === "payRequest" && minSendable === maxSendable) {
+ minSendable = Math.round(minSendable / 1000);
+ maxSendable = Math.round(maxSendable / 1000);
+ const amount = minSendable;
+
+ const urlObj = new URL(callback);
+ urlObj.searchParams.set("amount", (amount * 1000).toString());
+ if (comment) {
+ urlObj.searchParams.set("comment", comment);
+ }
+ const url = urlObj.toString();
+
+ const { pr } = await fetch(url).then((r) => (r as Response).json());
+
+ let invoice;
+ try {
+ invoice = await get(`/invoice/${pr}`);
+ } catch (e) {}
+
+ if (invoice) redirect(307, `/invoice/${invoice.id}`);
+
+ let path = `/send/lightning/${pr}`;
+ if (comment) path += `/${encodeURIComponent(comment)}`;
+ redirect(307, path);
+ } else if (callback.includes(PUBLIC_DOMAIN)) {
+ const username = url.split(`https://${PUBLIC_DOMAIN}/p/`)[1];
+ redirect(307, `/pay/${username}`);
+ }
+
if (!["payRequest", "withdrawRequest"].includes(data.tag))
error(500, "We only support LNURLp and LNURLw at this time");
- data.rate = rates[user.currency];
+ data.rate = rates[user?.currency || "USD"];
return data;
}
@@ -42,6 +77,7 @@ export const actions = {
if (comment) url += `&comment=${comment}`;
const { pr } = await fetch(url).then((r) => r.json());
+
let path = `/send/lightning/${pr}`;
if (comment) path += `/${encodeURIComponent(comment)}`;
redirect(307, path);
diff --git a/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts b/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts
index b0f824ed..9c2b1e3b 100644
--- a/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts
+++ b/src/routes/(app)/pay/[username]/[...amount]/+page.server.ts
@@ -13,7 +13,7 @@ export async function load({ cookies, params, parent }) {
let [amount, currency] = params.amount.split("/");
const rate = rates[currency ? currency.toUpperCase() : subject.currency];
- if (!rate) error(500, "Invalid currency symbol");
+ if (currency && !rate) error(500, "Invalid currency symbol");
if (amount) {
if (currency) amount = (amount * sats) / rate;
@@ -33,8 +33,6 @@ export async function load({ cookies, params, parent }) {
auth(cookies),
);
- const { balance } = await get(`/account/${user.id}`, auth(cookies));
-
redirect(307, `/invoice/${id}`);
}
@@ -66,8 +64,8 @@ export const actions = {
return fail(400, { message });
}
- if (parseInt(amount) > 0 && prompt) redirect(307, `/invoice/${id}/tip`);
- else if (parseInt(amount) <= 0 ) redirect(307, `/${username}/receive`);
+ if (parseInt(amount) > 0 && prompt) redirect(307, `/invoice/${id}/tip`);
+ else if (parseInt(amount) <= 0) redirect(307, `/${username}/receive`);
redirect(307, `/invoice/${id}`);
},
};
diff --git a/src/routes/(app)/payment/[id]/+page.svelte b/src/routes/(app)/payment/[id]/+page.svelte
index fcde487b..0667291e 100644
--- a/src/routes/(app)/payment/[id]/+page.svelte
+++ b/src/routes/(app)/payment/[id]/+page.svelte
@@ -67,7 +67,9 @@
let direction = $state("");
onMount(() => {
- direction = amount > 0 ? $t("payments.from") : $t("payments.to");
+ let a = amount;
+ if (type === types.fund) a = 0 - a;
+ direction = a > 0 ? $t("payments.from") : $t("payments.to");
if (direction)
direction =
diff --git a/src/routes/(app)/payments/[...page]/+page.svelte b/src/routes/(app)/payments/[...page]/+page.svelte
index 28cd29f4..57316f4a 100644
--- a/src/routes/(app)/payments/[...page]/+page.svelte
+++ b/src/routes/(app)/payments/[...page]/+page.svelte
@@ -215,7 +215,7 @@
<!-- >{$t("payments.subtotal")}</span -->
<!-- > -->
<span class="text-base text-secondary">
- {#if tipsIn || tipsOut}{$t("payments.tips")}{/if}
+ {#if tipsIn > 0 || tipsOut > 0}{$t("payments.tips")}{/if}
</span>
<span class="text-base text-secondary text-right"
>{$t("payments.total")}</span
@@ -229,32 +229,38 @@
{@const tipsOut = outgoing[c]?.fiatTips || 0}
{@const subtotalOut = totalOut - tipsOut}
- <span class="text-left text-base text-secondary"
- >{$t("payments.income")}</span
- >
- <!-- <span><b>{subtotalIn ? f(subtotalIn, c) : "-"}</b></span> -->
- <span><b>{tipsIn > 0 ? f(tipsIn, c, locale) : "-"}</b></span>
- <span class="text-right"
- ><b>{totalIn > 0 ? f(totalIn, c, locale) : "-"}</b></span
- >
+ {#if totalIn > 0}
+ <span class="text-left text-base text-secondary"
+ >{$t("payments.income")}</span
+ >
+ <!-- <span><b>{subtotalIn ? f(subtotalIn, c) : "-"}</b></span> -->
+ <span><b>{tipsIn > 0 ? f(tipsIn, c, locale) : ""}</b></span>
+ <span class="text-right"
+ ><b>{totalIn > 0 ? f(totalIn, c, locale) : "-"}</b></span
+ >
+ {/if}
- <span class="text-left text-base text-secondary"
- >{$t("payments.expenditure")}</span
- >
- <!-- <span><b>{subtotalOut ? f(subtotalOut, c) : "-"}</b></span> -->
- <span><b>{tipsOut > 0 ? f(tipsOut, c, locale) : "-"}</b></span>
- <span class="text-right"
- ><b>{totalOut > 0 ? f(totalOut, c, locale) : "-"}</b></span
- >
+ {#if totalOut > 0}
+ <span class="text-left text-base text-secondary"
+ >{$t("payments.expenditure")}</span
+ >
+ <!-- <span><b>{subtotalOut ? f(subtotalOut, c) : "-"}</b></span> -->
+ <span><b>{tipsOut > 0 ? f(tipsOut, c, locale) : "-"}</b></span>
+ <span class="text-right"
+ ><b>{totalOut > 0 ? f(totalOut, c, locale) : "-"}</b></span
+ >
+ {/if}
{/each}
</div>
</div>
- <div class="flex justify-center">
- <button class="btn !w-auto" onclick={csv}>
- <iconify-icon noobserver icon="ph:floppy-disk-bold" width="32"
- ></iconify-icon>
- <div class="my-auto">{$t("payments.export")}</div>
- </button>
- </div>
+ {#if payments.length}
+ <div class="flex justify-center">
+ <button class="btn !w-auto" onclick={csv}>
+ <iconify-icon noobserver icon="ph:floppy-disk-bold" width="32"
+ ></iconify-icon>
+ <div class="my-auto">{$t("payments.export")}</div>
+ </button>
+ </div>
+ {/if}
</div>
diff --git a/src/routes/(app)/qr/[text]/[icon]/+server.ts b/src/routes/(app)/qr/[text]/[icon]/+server.ts
new file mode 100644
index 00000000..c2825877
--- /dev/null
+++ b/src/routes/(app)/qr/[text]/[icon]/+server.ts
@@ -0,0 +1,33 @@
+import { createCanvas, loadImage } from "canvas";
+import QRCode from "qrcode";
+
+export async function GET({ params }) {
+ const { icon, text } = params;
+ const size = 600;
+
+ const canvas = createCanvas(size, size);
+ await QRCode.toCanvas(canvas, text, {
+ errorCorrectionLevel: "H",
+ width: size,
+ });
+
+ const ctx = canvas.getContext("2d");
+
+ const logo = await loadImage(`static/images/${icon}`);
+ const logoSize = size * 0.3;
+ ctx.drawImage(
+ logo,
+ (size - logoSize) / 2,
+ (size - logoSize) / 2,
+ logoSize,
+ logoSize,
+ );
+
+ const buffer = canvas.toBuffer("image/png");
+ return new Response(buffer, {
+ headers: {
+ "Content-Type": "image/png",
+ "Cache-Control": "public, max-age=31536000, immutable",
+ },
+ });
+}
diff --git a/src/routes/(app)/qr/[text]/raw/+server.ts b/src/routes/(app)/qr/[text]/raw/+server.ts
index a24d3a3c..a2626c4d 100644
--- a/src/routes/(app)/qr/[text]/raw/+server.ts
+++ b/src/routes/(app)/qr/[text]/raw/+server.ts
@@ -14,3 +14,4 @@ export async function GET({ params }) {
},
});
}
+
diff --git a/src/routes/(app)/scan/+page.svelte b/src/routes/(app)/scan/+page.svelte
index 88a76fbb..b21a7458 100644
--- a/src/routes/(app)/scan/+page.svelte
+++ b/src/routes/(app)/scan/+page.svelte
@@ -1,222 +1,61 @@
<script>
+ import { tick } from "svelte";
+ import Icon from "$comp/Icon.svelte";
+ import QrScanner from "qr-scanner";
+ import { onMount, onDestroy } from "svelte";
import { back } from "$lib/utils";
import { goto } from "$app/navigation";
- import { onMount, onDestroy } from "svelte";
import { BarcodeDetector } from "barcode-detector/ponyfill";
- import { camera } from "$lib/store";
+
+ let scanner,
+ vid = $state(),
+ resizing;
let resize = () => {
if (resizing) clearTimeout(resizing);
resizing = setTimeout(initialize, 1000);
};
- let videoEl;
- let canvasEl;
- let barcodeDetector;
- let scanningFrameId;
- let cameras = [];
- let detectionResult = null; // will hold the detected QR code value
-
- // New variables to handle iOS behavior.
- let isIOS = false;
- let scanningStarted = false;
-
- // Enumerate available video input devices
- async function getCameraDevices() {
- try {
- const devices = await navigator.mediaDevices.enumerateDevices();
- cameras = devices.filter((device) => device.kind === "videoinput");
-
- // Simply choose the first available camera.
- if (!$camera && cameras.length > 0) {
- $camera = cameras[0].deviceId;
- }
- } catch (err) {
- console.error("Error enumerating devices:", err);
- }
- }
-
- // Start the camera stream for a given deviceId (used for non-iOS or programmatic restarts)
- async function startCamera(deviceId) {
- stopCameraStream();
- detectionResult = null; // reset detection on restart
- try {
- const constraints = deviceId
- ? { video: { deviceId: { exact: deviceId } } }
- : { video: { facingMode: "environment" } };
- const stream = await navigator.mediaDevices.getUserMedia(constraints);
- videoEl.srcObject = stream;
- await videoEl.play();
- } catch (err) {
- console.error("Error accessing camera:", err);
- }
- }
-
- // Stop any active video stream and cancel the scanning loop
- function stopCameraStream() {
- if (videoEl && videoEl.srcObject) {
- const stream = videoEl.srcObject;
- if (stream.getTracks) {
- stream.getTracks().forEach((track) => track.stop());
- }
- }
- if (scanningFrameId) {
- cancelAnimationFrame(scanningFrameId);
- }
- }
-
- function handleDetection(barcode) {
- detectionResult = barcode.rawValue;
- stopCameraStream();
- goto(`/send/${encodeURIComponent(detectionResult)}`);
- }
-
- // The scanning loop: draw the video frame onto a hidden canvas and detect QR codes.
- function scanLoop() {
- if (!videoEl.videoWidth || !videoEl.videoHeight) {
- scanningFrameId = requestAnimationFrame(scanLoop);
- return;
- }
-
- const context = canvasEl.getContext("2d");
- if (!context) return;
-
- // Ensure canvas dimensions match the video
- canvasEl.width = videoEl.videoWidth;
- canvasEl.height = videoEl.videoHeight;
- context.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height);
-
- barcodeDetector
- .detect(canvasEl)
- .then((barcodes) => {
- if (barcodes.length > 0) {
- handleDetection(barcodes[0]);
- return; // Stop scanning on detection.
- }
- })
- .catch((err) => {
- console.error("Detection error:", err);
- });
-
- // Continue scanning if no detection has occurred.
- if (!detectionResult) {
- scanningFrameId = requestAnimationFrame(scanLoop);
- }
- }
-
- // Handle change of camera selection
- async function onCameraChange(event) {
- $camera = event.target.value;
- await startCamera($camera);
- videoEl.addEventListener("loadeddata", () => {
- canvasEl.width = videoEl.videoWidth;
- canvasEl.height = videoEl.videoHeight;
- scanLoop();
- });
- }
-
- // Provide a way to restart scanning (non-iOS)
- async function restart() {
- detectionResult = null;
- await startCamera($camera);
- videoEl.addEventListener("loadeddata", () => {
- canvasEl.width = videoEl.videoWidth;
- canvasEl.height = videoEl.videoHeight;
- scanLoop();
- });
- }
-
- // For iOS: Use a direct promise chain to call getUserMedia as part of the user gesture.
- function startScanning() {
- scanningStarted = true;
- stopCameraStream();
- detectionResult = null;
- const constraints = $camera
- ? { video: { deviceId: { exact: $camera } } }
- : { video: { facingMode: "environment" } };
- navigator.mediaDevices.getUserMedia(constraints)
- .then((stream) => {
- videoEl.srcObject = stream;
- return videoEl.play();
- })
- .then(() => {
- // When video is ready, start scanning.
- canvasEl.width = videoEl.videoWidth;
- canvasEl.height = videoEl.videoHeight;
- scanLoop();
- })
- .catch((err) => {
- console.error("Error accessing camera:", err);
- });
- }
-
let initialize = async () => {
- // Detect if we're on an iOS device
- isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
- barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] });
- await getCameraDevices();
-
- // For non-iOS devices, auto-start scanning.
- if (!isIOS) {
- await startCamera($camera);
- videoEl.addEventListener("loadeddata", () => {
- canvasEl.width = videoEl.videoWidth;
- canvasEl.height = videoEl.videoHeight;
- scanLoop();
- });
+ if (scanner) {
+ scanner.stop();
+ await new Promise((r) => setTimeout(r, 1000));
}
+
+ let options = {
+ highlightScanRegion: true,
+ highlightCodeOutline: true,
+ qrEngine: BarcodeDetector,
+ };
+ let cb = ({ data }) =>
+ scanner.stop() || goto(`/send/${encodeURIComponent(data)}`);
+ scanner = new QrScanner(vid, cb, options);
+ await tick();
+ scanner.start();
};
onMount(initialize);
-
- onDestroy(() => {
- stopCameraStream();
- });
+ onDestroy(() => scanner?.stop());
</script>
<svelte:window onresize={resize} />
+
<div class="flex w-full mb-4 p-4">
- <div class="mx-auto rounded-3xl space-y-5">
+ <div class="mx-auto rounded-3xl">
<video
- bind:this={videoEl}
+ bind:this={vid}
class="border-4 rounded-3xl border-black max-h-[calc(100vh*0.7)]"
></video>
- <canvas bind:this={canvasEl} style="display: none;"></canvas>
- <div class="flex justify-center">
- <button class="btn !w-auto" onclick={back}>Cancel</button>
- </div>
- <!-- Only display the camera selection if there are multiple cameras -->
- <div>
- <select
- id="cameraSelect"
- bind:value={$camera}
- onchange={onCameraChange}
- >
- {#each cameras as camera}
- <option value={camera.deviceId}>
- {camera.label || `Camera ${camera.deviceId}`}
- </option>
- {/each}
- </select>
- </div>
- <!-- For iOS users, show a "Start Scanning" button if scanning hasn't begun -->
- {#if isIOS && !scanningStarted}
- <div class="flex justify-center">
- <button class="btn !w-auto" onclick={startScanning}>
- Start Scanning
- </button>
- </div>
- {/if}
</div>
</div>
+<div class="flex justify-center">
+ <button class="btn !w-auto" onclick={back}>Cancel</button>
+</div>
+
<style>
video {
- width: 100%;
- max-width: 600px;
- border: 4px solid black;
- border-radius: 1rem;
- transform: scale(1);
transition: transform 0.3s ease-out;
+ transform: scale(1);
}
</style>
diff --git a/src/routes/(app)/send/[...text]/+page.svelte b/src/routes/(app)/send/[...text]/+page.svelte
index 183b4b51..52b92501 100644
--- a/src/routes/(app)/send/[...text]/+page.svelte
+++ b/src/routes/(app)/send/[...text]/+page.svelte
@@ -51,6 +51,20 @@
invalidate("app:contacts");
};
+ let trust = async (e, { id, trusted }) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (trusted) {
+ await post("/post/trust/delete", { id });
+ } else {
+ await post("/post/trust", { id });
+ }
+
+ if (all) all = await get("/contacts");
+ invalidate("app:contacts");
+ };
+
$effect(() => {
if (browser && pasted && text) el.click() && (pasted = false);
});
@@ -74,17 +88,6 @@
</div>
{/if}
- <textarea
- use:focus
- name="text"
- placeholder={$t("user.send.placeholder")}
- onkeypress={keypress}
- class="w-full p-4 border rounded-xl h-32 text-xl"
- bind:value={text}
- onpaste={() => (pasted = true)}
- autocapitalize="none"
- ></textarea>
-
<div class="flex gap-2">
<a href="/scan" class="contents">
<button type="button" class="btn !w-auto flex-grow">
@@ -101,19 +104,23 @@
</button>
</div>
+ <textarea
+ use:focus
+ name="text"
+ placeholder={$t("user.send.placeholder")}
+ onkeypress={keypress}
+ class="w-full p-4 border rounded-xl h-32 text-xl"
+ bind:value={text}
+ onpaste={() => (pasted = true)}
+ autocapitalize="none"
+ ></textarea>
+
<button bind:this={el} type="submit" class="btn btn-accent">
<iconify-icon noobserver icon="ph:paper-plane-right-bold" width="32"
></iconify-icon>
<div class="my-auto">{$t("user.send.next")}</div>
</button>
- <a href="/send/ecash" class="block">
- <button type="button" class="btn">
- <img src="/images/cash.png" class="w-8" />
- <div class="my-auto">{$t("payments.createEcash")}</div>
- </button>
- </a>
-
<a href="/send/fund" class="block">
<button type="button" class="btn">
<iconify-icon
@@ -139,12 +146,20 @@
<div class="my-auto text-left">
<p class="ml-1 text-lg break-words">{c.username}</p>
</div>
- <button class="ml-auto" onclick={(e) => pin(e, c)}>
- <iconify-icon
- icon={c.pinned ? "ph:push-pin-fill" : "ph:push-pin-bold"}
- width={32}
- ></iconify-icon>
- </button>
+ <div class="flex ml-auto gap-1">
+ <button onclick={(e) => pin(e, c)}>
+ <iconify-icon
+ icon={c.pinned ? "ph:push-pin-fill" : "ph:push-pin-bold"}
+ width={32}
+ ></iconify-icon>
+ </button>
+ <button class="ml-auto" onclick={(e) => trust(e, c)}>
+ <iconify-icon
+ icon={c.trusted ? "ph:star-fill" : "ph:star-bold"}
+ width={32}
+ ></iconify-icon>
+ </button>
+ </div>
</div>
</a>
{/each}
@@ -152,7 +167,7 @@
</div>
{/if}
- {#if !loaded}
+ {#if !loaded && contacts.length > 0}
<button onclick={loadMore} type="button" class="btn"
>{$t("user.loadMore")}</button
>
diff --git a/src/routes/(app)/send/bitcoin/[address]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/+page.svelte
index 060473ec..529e2691 100644
--- a/src/routes/(app)/send/bitcoin/[address]/+page.svelte
+++ b/src/routes/(app)/send/bitcoin/[address]/+page.svelte
@@ -1,25 +1,32 @@
<script>
+ import { tick } from "svelte";
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";
+ import { loc, fail, s } from "$lib/utils";
let { data } = $props();
let { balance, user } = data;
let { address } = $page.params;
let { currency, username } = user;
+ let locale = loc(user);
let amount = $state(0);
- let submit = $state(), fiat = $state();
+ let a = $state(0);
+ let submit = $state(),
+ fiat = $state();
$effect(() => ($rate = data.rate));
+ $effect(() => (amount = a));
- let setMax = (e) => {
+ let setMax = async (e) => {
e.preventDefault();
fiat = false;
amount = balance;
+ await tick();
+ submit.click();
};
</script>
@@ -28,7 +35,7 @@
<div class="text-xl text-secondary break-all">{address}</div>
- <Numpad bind:amount bind:fiat {currency} {submit} bind:rate={$rate} />
+ <Numpad bind:amount={a} bind:fiat {currency} {submit} bind:rate={$rate} {locale} />
<div class="flex justify-center gap-2">
<button
@@ -39,7 +46,12 @@
>
<form action={`/send/bitcoin/${address}/${amount}`} class="contents">
- <button use:focus bind:this={submit} type="submit" class="btn !w-auto grow btn-accent">
+ <button
+ use:focus
+ bind:this={submit}
+ type="submit"
+ class="btn !w-auto grow btn-accent"
+ >
{$t("payments.next")}
</button>
</form>
diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts
index 5f20d1bd..ddee9c1c 100644
--- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts
+++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts
@@ -12,7 +12,7 @@ export async function load({
const aid = cookies.get("aid") || user.id;
try {
- const { fee, fees, inputs, ourfee, hex } = await post(
+ const { fee, fees, inputs, ourfee, hex, subtract } = await post(
"/bitcoin/fee",
{ address, amount, feeRate, aid },
auth(cookies),
@@ -29,6 +29,7 @@ export async function load({
feeRate,
rate: rates[user.currency],
ourfee,
+ subtract,
hex,
inputs,
};
diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte
index 7d9b8f64..1551cc2c 100644
--- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte
+++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte
@@ -1,6 +1,4 @@
<script>
- import { run } from "svelte/legacy";
-
import { tick } from "svelte";
import { t } from "$lib/translations";
import { enhance } from "$app/forms";
@@ -15,7 +13,6 @@
import { applyAction } from "$app/forms";
import { hex as hexUtil } from "@scure/base";
import * as btc from "@scure/btc-signer";
- import { hash160 } from "@scure/btc-signer/utils";
import { decrypt } from "nostr-tools/nip49";
import { HDKey } from "@scure/bip32";
import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39";
@@ -71,8 +68,18 @@
let toggle = () => (submitting = !submitting);
- let { account, amount, address, message, fee, fees, ourfee, hex, inputs } =
- $derived(data);
+ let {
+ account,
+ amount,
+ address,
+ message,
+ fee,
+ fees,
+ subtract,
+ ourfee,
+ hex,
+ inputs,
+ } = $derived(data);
let { feeRate } = $state(data);
@@ -116,7 +123,11 @@
{:else}
<div class="text-xl text-secondary break-all">{address}</div>
- <Amount {amount} rate={$rate} {currency} />
+ <Amount
+ amount={subtract ? amount - fee - ourfee : amount}
+ rate={$rate}
+ {currency}
+ />
<div class="text-center">
<h2 class="text-secondary text-lg">{$t("payments.networkFee")}</h2>
@@ -137,14 +148,7 @@
<div class="text-center">
<h2 class="text-secondary text-lg">{$t("payments.platformFee")}</h2>
- <div class="flex flex-wrap gap-4 justify-center">
- <div class="my-auto">
- <h2 class="text-xl">
- {f(toFiat(ourfee, $rate), currency)}
- </h2>
- <h3 class="text-secondary">⚡️{s(ourfee)}</h3>
- </div>
- </div>
+ <Amount amount={ourfee} rate={$rate} {currency} />
</div>
{/if}
diff --git a/src/routes/(app)/send/ecash/+page.server.ts b/src/routes/(app)/send/ecash/+page.server.ts
deleted file mode 100644
index b290fb95..00000000
--- a/src/routes/(app)/send/ecash/+page.server.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import getRates from "$lib/rates";
-import { auth, fd, post } from "$lib/utils";
-import { error, redirect } from "@sveltejs/kit";
-
-export async function load({ parent }) {
- const rates = await getRates();
- const { user } = await parent();
- return { rate: rates[user.currency] };
-}
-
-export const actions = {
- default: async ({ cookies, request }) => {
- let id;
- const body = await fd(request);
-
- try {
- ({ id } = await post("/mint", body, auth(cookies)));
- } catch (e) {
- const { message } = e as Error;
- console.log("problem creating ecash", e);
- error(400, { message });
- }
-
- redirect(307, `/ecash/${id}`);
- },
-};
diff --git a/src/routes/(app)/send/ecash/+page.svelte b/src/routes/(app)/send/ecash/+page.svelte
deleted file mode 100644
index b9c93661..00000000
--- a/src/routes/(app)/send/ecash/+page.svelte
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
- import { enhance, applyAction } from "$app/forms";
- import { t } from "$lib/translations";
- import Icon from "$comp/Icon.svelte";
- import Numpad from "$comp/Numpad.svelte";
- import Spinner from "$comp/Spinner.svelte";
- import { page } from "$app/stores";
- import { rate } from "$lib/store";
- import { fail, s } from "$lib/utils";
- import handler from "$lib/handler";
-
- let { data } = $props();
-
- let { user } = $derived(data);
- let { balance, currency, username } = $derived(user);
-
- let amount = $state(0);
- let submit = $state(),
- fiat = $state();
-
- let setMax = (e) => {
- e.preventDefault();
- fiat = false;
- amount = balance;
- };
-
- let submitting = $state();
- let toggle = () => (submitting = !submitting);
- $effect(() => ({ rate: $rate } = data));
-</script>
-
-<div class="container px-4 max-w-xl mx-auto space-y-5 text-center">
- <form use:enhance={handler(toggle)} method="POST" class="space-y-5">
- <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold">
- {$t("payments.fundingAmount")}
- </h1>
- <Numpad bind:amount bind:fiat {currency} {submit} bind:rate={$rate} />
- <input type="hidden" name="amount" bind:value={amount} />
-
- <div class="flex gap-2">
- <button
- type="button"
- class="btn !w-auto grow"
- onclick={setMax}
- disabled={submitting}
- onkeydown={setMax}>Max ⚡️{s(balance)}</button
- >
-
- <button
- use:focus
- bind:this={submit}
- type="submit"
- class="btn btn-accent !w-auto grow"
- >
- {#if submitting}
- <Spinner />
- {:else}
- <div class="my-auto">{$t("payments.next")}</div>
- {/if}
- </button>
- </div>
- </form>
-</div>
diff --git a/src/routes/(app)/send/ecash/[request]/+page.server.ts b/src/routes/(app)/send/ecash/[request]/+page.server.ts
deleted file mode 100644
index 6cc593cc..00000000
--- a/src/routes/(app)/send/ecash/[request]/+page.server.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { fd, get } from "$lib/utils";
-import { decodePaymentRequest } from "@cashu/cashu-ts";
-import { redirect } from "@sveltejs/kit";
-
-export async function load({ cookies, params, url }) {
- if (!cookies.get("token"))
- redirect(307, `/login?redirect=${encodeURIComponent(url.pathname)}`);
-
- const req = { ...decodePaymentRequest(params.request) };
- const [{ target, type }] = req.transport;
-
- let recipient = { name: "", pubkey: null };
- if (type === "nostr") {
- recipient = await get(`/profile/${target}`);
- } else if (type === "post") {
- recipient.name = new URL(target).hostname;
- }
-
- return { req, recipient, type, target };
-}
-
-export const actions = {
- setAmount: async ({ request }) => {
- return await fd(request);
- },
-};
diff --git a/src/routes/(app)/send/ecash/[request]/+page.svelte b/src/routes/(app)/send/ecash/[request]/+page.svelte
deleted file mode 100644
index 42bc8e9d..00000000
--- a/src/routes/(app)/send/ecash/[request]/+page.svelte
+++ /dev/null
@@ -1,128 +0,0 @@
-<script lang="ts">
- import { goto } from "$app/navigation";
- import { focus, fail, post } from "$lib/utils";
- import { t } from "$lib/translations";
- import { enhance } from "$app/forms";
- import Amount from "$comp/Amount.svelte";
- import Numpad from "$comp/Numpad.svelte";
- import Spinner from "$comp/Spinner.svelte";
- import { rate, pin } from "$lib/store";
- import { wrapEvent } from "nostr-tools/nip17";
- import { getPrivateKey, sign, send } from "$lib/nostr";
- import { getDecodedToken } from "@cashu/cashu-ts";
- import type { PaymentRequestPayload } from "@cashu/cashu-ts";
- import { AbstractSimplePool } from "nostr-tools/abstract-pool";
-
- const alwaysTrue: any = (t: Event) => {
- t[Symbol("verified")] = true;
- return true;
- };
-
- export const pool = new AbstractSimplePool({ verifyEvent: alwaysTrue });
-
- const { data, form } = $props();
-
- let formElement = $state();
- let submitting = $state();
- let submit = async (e) => {
- e.preventDefault();
- submitting = true;
-
- try {
- let { pid, token } = await post("/ecash/mint", { amount });
- let { proofs } = getDecodedToken(token);
- let { id, mints, unit = "" } = req;
- let mint = mints[0];
-
- const payload = {
- id,
- mint,
- unit,
- proofs,
- };
-
- if (type === "nostr") {
- let event = wrapEvent(
- await getPrivateKey(user),
- { publicKey: recipient.pubkey },
- JSON.stringify(payload),
- );
-
- await Promise.any(pool.publish(recipient.relays, event));
- } else if (type === "post") {
- await post("/ecash/post", { target, payload });
- }
-
- goto(`/sent/${pid}`);
- } catch (e) {
- console.log(e);
- fail(e.message);
- }
-
- submitting = false;
- };
-
- let { recipient, req, user, type, target } = $derived({
- ...data,
- ...form,
- });
- let { currency } = $derived(user);
- let a = $state();
- let next = $state();
- let toggle = () => (show = !show);
- let amount = $derived(form?.amount || data.req.amount);
-
- $effect(() => ($rate ||= data.rate));
- $effect(() => form && (submitting = false));
-</script>
-
-<div class="container px-4 max-w-xl mx-auto text-center space-y-2">
- {#if amount}
- {#if form?.message}
- <div class="text-red-600 text-center">
- {$t(form.message)}
- </div>
- {/if}
-
- <div>
- <h1 class="text-lg text-secondary">
- {$t("payments.send")}
- </h1>
-
- <Amount {amount} rate={$rate} {currency} />
- </div>
-
- <div class="text-xl">
- <span class="text-secondary">{$t("payments.to")}</span>
- <span class="break-words font-semibold">{recipient.name}</span>
- </div>
-
- <form onsubmit={submit} class="space-y-2" bind:this={formElement}>
- <input name="amount" value={amount} type="hidden" />
- <input name="pin" value={$pin} type="hidden" />
-
- <button
- type="submit"
- class="btn btn-primary"
- disabled={submitting}
- use:focus
- >
- {#if submitting}
- <Spinner />
- {:else}
- {$t("payments.send")}
- {/if}
- </button>
- </form>
- {:else}
- <form method="POST" action="?/setAmount" class="space-y-2" use:enhance>
- <input type="hidden" value={a} name="amount" />
- <input name="rate" value={$rate} type="hidden" />
-
- <Numpad bind:amount={a} {currency} bind:rate={$rate} submit={next} />
- <button type="submit" class="btn" bind:this={next}
- >{$t("payments.next")}</button
- >
- </form>
- {/if}
-</div>
diff --git a/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte b/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte
index 314be3ef..d34ed073 100644
--- a/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte
+++ b/src/routes/(app)/send/fund/[...name]/[...amount]/+page.svelte
@@ -6,14 +6,16 @@
import { page } from "$app/stores";
import { pin } from "$lib/store";
import handler from "$lib/handler";
- import { s } from "$lib/utils";
+ import { loc, s } from "$lib/utils";
+ import { applyAction, deserialize } from "$app/forms";
let { data, form } = $props();
- let { amount } = $state(data);
- let { balance, currency } = data.user;
+ let { amount, user } = $state(data);
+ let { balance, currency } = $derived(user);
let { name, rate } = $derived(data);
let loading = $state();
let fiat = $state();
+ let locale = loc(user);
let submit = $state();
let submitting = $state();
@@ -22,11 +24,22 @@
if (form?.message?.includes("pin")) $pin = undefined;
loading = false;
});
+ let formElement = $state();
- let setMax = (e) => {
+ let setMax = async (e) => {
e.preventDefault();
- fiat = false;
- amount = balance;
+ let body = new FormData(formElement);
+ body.set("fiat", false);
+ body.set("amount", user.balance);
+
+ const response = await fetch(formElement.action, {
+ method: "POST",
+ body,
+ });
+
+ const result = deserialize(await response.text());
+ if (result.type === "success") await invalidateAll();
+ applyAction(result);
};
</script>
@@ -37,9 +50,14 @@
{/if}
<div class="container px-4 mt-20 max-w-xl mx-auto">
- <Numpad bind:amount {currency} {rate} {fiat} {submit} />
+ <Numpad bind:amount {currency} {rate} {fiat} {submit} {locale} />
- <form use:enhance={handler(toggle)} method="POST" class="space-y-5">
+ <form
+ use:enhance={handler(toggle)}
+ method="POST"
+ class="space-y-5"
+ bind:this={formElement}
+ >
<input name="fund" value={name} type="hidden" />
<input name="amount" value={amount} type="hidden" />
<input name="pin" value={$pin} type="hidden" />
diff --git a/src/routes/(app)/send/invoice/[id]/+page.server.ts b/src/routes/(app)/send/invoice/[id]/+page.server.ts
index 3ef933d4..70b007a8 100644
--- a/src/routes/(app)/send/invoice/[id]/+page.server.ts
+++ b/src/routes/(app)/send/invoice/[id]/+page.server.ts
@@ -1,12 +1,36 @@
import getRates from "$lib/rates";
import { auth, fd, get, post, types } from "$lib/utils";
-import { error, redirect } from "@sveltejs/kit";
+import { error, fail, redirect } from "@sveltejs/kit";
-export async function load({ cookies, params: { id }, parent }) {
- const { user } = await parent();
+export async function load({ cookies, depends, params: { id }, parent }) {
+ depends("app:trust");
+ const { subject, user } = await parent();
const aid = cookies.get("aid") || user.id;
+ const pin = cookies.get("pin");
- const invoice = await get(`/invoice/${id}`);
+ let invoice = await get(`/invoice/${id}`);
+
+ const trust = await get("/trust", auth(cookies));
+ const trusted = trust.includes(invoice.uid);
+ if (trusted && (pin || !user.haspin)) {
+ let p;
+ try {
+ if (!invoice.tip && invoice.user.prompt) {
+ if (user.tip > 0) {
+ invoice.tip = Math.round(invoice.amount * (user.tip / 100));
+ invoice = await post(`/invoice/${id}`, { invoice }, auth(cookies));
+ } else {
+ throw new Error("tip");
+ }
+ }
+ p = await post("/payments", { ...invoice, pin }, auth(cookies));
+ } catch (e) {
+ const { message } = e as Error;
+ if (message === "tip") redirect(307, `/invoice/${id}/tip`);
+ fail(400, { message });
+ }
+ if (p) redirect(307, `/sent/${p.id}`);
+ }
if (invoice.amount && invoice.prompt && invoice.tip === null)
redirect(307, `/invoice/${id}/tip`);
@@ -31,7 +55,7 @@ export async function load({ cookies, params: { id }, parent }) {
const invoiceRate = rates[invoice.currency];
const { balance } = await get(`/account/${aid}`, auth(cookies));
- return { balance, invoice, user, rate, invoiceRate };
+ return { balance, invoice, user, rate, invoiceRate, trusted };
}
export const actions = {
@@ -45,7 +69,7 @@ export const actions = {
} catch (e) {
const { message } = e as Error;
console.log("payment failed", id, e);
- error(500, message);
+ error(500, { message });
}
redirect(307, `/sent/${p.id}`);
diff --git a/src/routes/(app)/send/invoice/[id]/+page.svelte b/src/routes/(app)/send/invoice/[id]/+page.svelte
index 7d167f41..d5fd00a8 100644
--- a/src/routes/(app)/send/invoice/[id]/+page.svelte
+++ b/src/routes/(app)/send/invoice/[id]/+page.svelte
@@ -1,4 +1,5 @@
<script>
+ import { invalidate } from "$app/navigation";
import { untrack } from "svelte";
import handler from "$lib/handler";
import { onDestroy, onMount } from "svelte";
@@ -26,7 +27,7 @@
} from "$lib/utils";
let { data, form } = $props();
- let { balance, invoice, user } = $state(data);
+ let { trusted, balance, invoice, user } = $state(data);
let { address, hash, payreq, user: recipient, tip } = $state(invoice);
let { currency } = $derived(user);
let locale = $derived(loc(user));
@@ -64,6 +65,19 @@
});
};
+ let toggleTrust = async (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (trusted) {
+ await post("/post/trust/delete", { id: recipient.id });
+ } else {
+ await post("/post/trust", { id: recipient.id });
+ }
+
+ invalidate("app:trust");
+ };
+
$effect(() => {
if (form?.message?.includes("pin")) $pin = undefined;
submitting = false;
@@ -193,5 +207,12 @@
<button type="button" class="btn" onclick={external}
>{$t("payments.moreOptions")}</button
>
+
+ {#if !trusted}
+ <button type="button" class="btn" onclick={toggleTrust}>
+ <iconify-icon icon="ph:star-fill" width={32}></iconify-icon>
+ {$t("payments.trustUser")}</button
+ >
+ {/if}
</form>
</div>
diff --git a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts
index 0f7b4003..eb324775 100644
--- a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts
+++ b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts
@@ -39,8 +39,7 @@ export const actions = {
p = await post("/payments", body, auth(cookies));
} catch (e) {
- let { message } = e as Error;
- if (!message.includes("Insufficient")) message = "payments.failedToRoute";
+ const { message } = e as Error;
return fail(400, { message });
}
diff --git a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte
index e979e7c4..a5cc168c 100644
--- a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte
+++ b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte
@@ -13,7 +13,7 @@
let { alias, memo, payreq, ourfee, user } = $derived({ ...data, ...form });
let a = $state();
- let { currency } = $derived(user);
+ let { currency, locked } = $derived(user);
let locale = loc(user);
$effect(() => ($rate ||= data.rate));
@@ -36,7 +36,12 @@
<div class="container px-4 max-w-xl mx-auto text-center space-y-2">
{#if form?.message}
<div class="text-red-600 text-center">
- {$t(form.message)}
+ {#if form.message.includes($t("payments.insufficientFunds"))}
+ <div>{form.message}</div>
+ <div>{$t("payments.lockedBalance")}: {locked}</div>
+ {:else}
+ {$t("payments.failedToRoute")}
+ {/if}
</div>
{/if}
{#if amount}
diff --git a/src/routes/(app)/settings/+layout.svelte b/src/routes/(app)/settings/+layout.svelte
index 2fe95813..3c2ebd88 100644
--- a/src/routes/(app)/settings/+layout.svelte
+++ b/src/routes/(app)/settings/+layout.svelte
@@ -36,8 +36,9 @@
};
let tabs = [
- { name: "account", key: "ACCOUNT" },
- { name: "preferences", key: "POINT_OF_SALE" },
+ { name: "profile", key: "ACCOUNT" },
+ { name: "account", key: "POINT_OF_SALE" },
+ { name: "nostr", key: "NOSTR" },
{ name: "security", key: "SECURITY" },
];
@@ -208,16 +209,16 @@
{$t("user.settings.header")}
</h1>
- <div class="flex justify-around items-center border-b pb-3">
- {#each tabs.filter((t) => t.name !== "shopify") as { name, key }}
- <a href={`/settings/${name}`}>
- <button
- type="button"
- class="hover:opacity-80"
- class:font-bold={tab === name ||
- (name === "preferences" && ["nostr", "shopify"].includes(tab))}
- >{$t(`user.settings.${key}`)}</button
- >
+ <div
+ class="flex flex-wrap justify-around items-center border-b pb-3"
+ >
+ {#each tabs as { name, key }}
+ <a
+ href={`/settings/${name}`}
+ class="px-4 py-2 w-1/2 sm:w-1/4 flex-shrink-0 hover:opacity-80 transition-opacity duration-150 text-center"
+ class:font-bold={name === tab}
+ >
+ {$t(`user.settings.${key}`)}
</a>
{/each}
</div>
@@ -227,17 +228,18 @@
</div>
<div
- class="z-10 fixed bottom-0 bg-base-100 shadow border-t border-t-8 border-accent w-full flex justify-center items-center py-3"
+ class="z-10 fixed bottom-0 bg-base-100 shadow border-accent w-full flex justify-center items-center py-3"
>
<button
bind:this={$save}
type="submit"
- class="btn !w-auto"
+ class="btn btn-accent !w-auto"
class:bg-base-200={submitting}
>
{#if submitting}
<Spinner />
{:else}
+ <iconify-icon icon="ph:floppy-disk-bold" width={32}></iconify-icon>
<div class="my-auto">{$t("user.settings.saveSettings")}</div>
{/if}
</button>
diff --git a/src/routes/(app)/settings/account/+page.server.ts b/src/routes/(app)/settings/account/+page.server.ts
index 4145935e..ebb90f05 100644
--- a/src/routes/(app)/settings/account/+page.server.ts
+++ b/src/routes/(app)/settings/account/+page.server.ts
@@ -1,4 +1,12 @@
+import getRates from "$lib/rates";
import updateUser from "$lib/settings";
+import { auth, get } from "$lib/utils";
+
+export async function load({ cookies }) {
+ const rates = await getRates();
+ const connect = await get("/square/connect", auth(cookies));
+ return { connect, rates };
+}
export const actions = {
default: updateUser,
diff --git a/src/routes/(app)/settings/account/+page.svelte b/src/routes/(app)/settings/account/+page.svelte
index d4178fd1..8b38ffe8 100644
--- a/src/routes/(app)/settings/account/+page.svelte
+++ b/src/routes/(app)/settings/account/+page.svelte
@@ -1,78 +1,123 @@
<script>
- import { tick } from "svelte";
- import { avatar, banner as bannerStore } from "$lib/store";
- import { t } from "$lib/translations";
+ import { run } from "svelte/legacy";
+ import { theme } from "$lib/store";
+
+ import { onMount, tick } from "svelte";
+ import { browser } from "$app/environment";
+ import Numpad from "$comp/Numpad.svelte";
+ import LocaleSelector from "$comp/LocaleSelector.svelte";
+ import Toggle from "$comp/Toggle.svelte";
+ import { locale, t } from "$lib/translations";
+ import { post, success, fail } from "$lib/utils";
import { page } from "$app/stores";
+ import { PUBLIC_VAPID_PUBKEY } from "$env/static/public";
let { data } = $props();
- let { user } = $derived(data);
- let { id } = user;
- let { about, banner, picture, display, email, username, verified } =
- $state(user);
+ let { user } = $state(data);
+ let { connect, rates, subscriptions } = data;
+ let { currency, email, tip, verified } = $state(user);
+ let rate = rates[currency];
+
+ let fiats = Object.keys(rates).sort((a, b) => a.localeCompare(b));
+ let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click());
- let avatarFile,
- avatarInput = $state(),
- bannerFile,
- bannerInput = $state();
+ let editingReserve = $state(),
+ editingThreshold = $state(),
+ doneReserve = $state(),
+ doneThreshold;
+ let doneEditing = () => {
+ editingReserve = false;
+ editingThreshold = false;
+ };
- let selectAvatar = () => avatarInput.click();
- let selectBanner = () => bannerInput.click();
+ let editReserve = async () => {
+ editingReserve = true;
+ await tick();
+ reserveEl.focus();
+ };
- let percent;
- let progress = async (event) => {
- percent = Math.round((event.loaded / event.total) * 100);
+ let editThreshold = async () => {
+ editingThreshold = true;
+ await tick();
+ thresholdEl.focus();
};
- let tooLarge = $state({});
+ if (!user.threshold) user.threshold = 1000000;
+ if (!user.reserve) user.reserve = 100000;
+ let reserveEl = $state(),
+ thresholdEl = $state();
+
+ let push = $state(),
+ pm,
+ subscription;
+
+ onMount(async () => {
+ if (!browser) return;
- let handleFile = async ({ target }, type) => {
- tooLarge[type] = false;
- let file = target.files[0];
- if (!file) return;
+ pm =
+ navigator?.serviceWorker &&
+ (await navigator.serviceWorker.getRegistration()).pushManager;
- if (file.size > 10000000) return (tooLarge[type] = true);
+ permission = await pm.permissionState({
+ userVisibleOnly: true,
+ applicationServerKey: PUBLIC_VAPID_PUBKEY,
+ });
- if (type === "picture") {
- $avatar = { id, file, type, progress };
- } else if (type === "banner") {
- $bannerStore = { id, file, type, progress };
+ if (permission === "granted") {
+ subscription = await pm.getSubscription();
+ if (subscriptions.includes(JSON.stringify(subscription))) push = true;
}
+ });
- var reader = new FileReader();
- reader.onload = async (e) => {
- if (type === "picture") {
- $avatar.src = e.target.result;
- } else if (type === "banner") {
- $bannerStore.src = e.target.result;
- }
- };
+ let permission = $state();
+ let updateNotifications = async (push) => {
+ if (!browser || !pm) return (push = false);
- reader.readAsDataURL(file);
- };
+ if (subscription && !push) {
+ return post("/subscription/delete", { subscription });
+ }
- let url = $derived(`${$page.url.host}/${username}`);
- let full = $derived(`${$page.url.protocol}//${url}`);
- let addr = $derived(`${username}@${$page.url.host}`);
+ if (push && permission !== "denied") {
+ subscription = await pm.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: PUBLIC_VAPID_PUBKEY,
+ });
+ }
+
+ if (subscription) {
+ await post("/subscription", { subscription });
+ }
+ };
+ run(() => {
+ user.language = $locale;
+ });
+ run(() => {
+ updateNotifications(push);
+ });
$effect(() => {
if (email !== user.email) verified = false;
});
+
+ let revoke = () => {};
</script>
<div>
- <label for="username" class="font-bold mb-1 block"
- >{$t("user.settings.username")}</label
+ <label for="language" class="font-bold block mb-1"
+ >{$t("user.settings.locale")}</label
>
- <div class="flex mb-2">
- <input type="text" name="username" bind:value={username} />
- </div>
+ <LocaleSelector style="select-styles block py-3 w-full" />
</div>
<div>
- <label for="display" class="font-bold mb-1 block"
- >{$t("user.settings.displayName")}</label
+ <label for="currency" class="font-bold block mb-1"
+ >{$t("user.settings.localCurrency")}</label
>
- <input type="text" name="display" bind:value={display} />
+ <select name="currency" value={currency}>
+ {#each fiats as fiat}
+ <option value={fiat}>{fiat}</option>
+ {/each}
+ </select>
</div>
<div class="space-y-1 relative">
@@ -86,108 +131,210 @@
>
<input type="text" name="email" class="clean" bind:value={email} />
{#if verified}
- <iconify-icon noobserver icon="ph:check-bold" class="text-success ml-auto" width="32"
+ <iconify-icon
+ noobserver
+ icon="ph:check-bold"
+ class="text-success ml-auto"
+ width="32"
></iconify-icon>
{:else if email}
- <iconify-icon noobserver icon="ph:clock-bold" class="text-warning ml-auto" width="32"
+ <iconify-icon
+ noobserver
+ icon="ph:clock-bold"
+ class="text-warning ml-auto"
+ width="32"
></iconify-icon>
{/if}
</label>
</div>
-<div class="space-y-2">
- <span class="font-bold">{$t("user.settings.profileImage")}</span>
+<div>
+ <div class="flex justify-between items-center">
+ <span class="font-bold">{$t("user.settings.notifications")}</span>
+ <Toggle id="notify" bind:value={user.notify} />
+ </div>
+ <p class="text-secondary mt-1 w-9/12">
+ {$t("user.settings.notificationsDesc")}
+ </p>
+</div>
- <div class="flex">
- {#if $avatar || picture}
- <div
- class="relative rounded-full overflow-hidden text-center w-20 h-20 my-auto hover:opacity-80 cursor-pointer"
- onclick={selectAvatar}
- onkeydown={selectAvatar}
- >
- <img
- src={$avatar?.src || picture}
- class="absolute w-full h-full object-cover object-center visible overflow-hidden"
- alt={username}
- />
- </div>
+<div>
+ <div class="flex justify-between items-center">
+ <span class="font-bold">{$t("user.settings.pushNotifications")}</span>
+ {#if permission !== "denied"}
+ <Toggle id="push" bind:value={push} />
+ {/if}
+ </div>
+ <p class="text-secondary mt-1 w-9/12">
+ {#if permission === "denied"}
+ {$t("user.settings.pushNotificationsDisabled")}
{:else}
- <div
- class="rounded-full border-4 border-base-100 p-4 bg-base-200 w-24 h-24 my-auto hover:opacity-80 cursor-pointer"
- onclick={selectAvatar}
- onkeydown={selectAvatar}
- ></div>
+ {$t("user.settings.pushNotificationsDesc")}
{/if}
- <div class="ml-2 p-2">
- <!-- found missing translation -->
- <button
- type="button"
- class="btn"
- onclick={selectAvatar}
- onkeydown={selectAvatar}>{$t("user.settings.select")}</button
- >
- <input
- type="file"
- class="hidden"
- bind:this={avatarInput}
- onchange={(e) => handleFile(e, "picture")}
- />
- </div>
+ </p>
+</div>
+
+<div>
+ <div class="flex justify-between items-center">
+ <span class="font-bold">{$t("user.settings.tipPrompt")}</span>
+ <Toggle id="prompt" bind:value={user.prompt} />
</div>
+ <p class="text-secondary mt-1 w-9/12">
+ {$t("user.settings.tipPromptDescription")}
+ </p>
+</div>
+
+<div class="space-y-1 relative">
+ <label for="tip" class="font-bold block">{$t("user.settings.tip")}</label>
+ <p class="text-secondary">
+ {$t("user.settings.tipDesc")}
+ </p>
- {#if tooLarge["avatar"]}
- <div class="text-red-600">Max file size 10MB</div>
- {/if}
+ <label
+ class="input input-bordered border-primary input-lg rounded-2xl flex items-center gap-2"
+ >
+ <input type="text" name="tip" class="clean" bind:value={tip} />
+ </label>
</div>
-<div class="space-y-2">
+<div>
<div class="flex justify-between items-center">
- <span class="font-bold">{$t("user.settings.bannerImage")}</span>
+ <span class="font-bold">{$t("user.settings.autoWithdraw")}</span>
+ <Toggle id="autowithdraw" bind:value={user.autowithdraw} />
</div>
+ <p class="text-secondary mt-1 w-9/12">
+ {$t("user.settings.autoWithdrawDescription")}
+ </p>
+</div>
- {#if $bannerStore || banner}
- <img
- src={$bannerStore ? $bannerStore.src : banner}
- class="w-full object-cover object-center visible overflow-hidden h-48 mb-4 hover:opacity-80"
- onclick={selectBanner}
- onkeydown={selectBanner}
- alt="Banner"
- />
- {:else}
- <div
- class="bg-base-200 w-full h-48 mb-4 cursor-pointer hover:opacity-80"
- onclick={selectBanner}
- onkeydown={selectBanner}
- alt="Banner"
- ></div>
- {/if}
+<div class:hidden={!user.autowithdraw}>
+ <div class="mb-2">
+ <label for="display" class="font-bold mb-1 block"
+ >{$t("user.settings.destination")}</label
+ >
+ <textarea
+ name="destination"
+ placeholder={$t("user.settings.destinationPlaceholder")}
+ onkeypress={keypress}
+ class="w-full p-4 border rounded-xl h-48"
+ bind:value={user.destination}
+ ></textarea>
+ </div>
- <button
- type="button"
- class="btn !w-auto"
- onclick={selectBanner}
- onkeydown={selectBanner}>{$t("user.settings.select")}</button
- >
- <input
- type="file"
- class="hidden"
- bind:this={bannerInput}
- onchange={(e) => handleFile(e, "banner")}
- />
+ <div>
+ <label for="display" class="font-bold mb-1 block"
+ >{$t("user.settings.threshold")}</label
+ >
+ <button type="button" class="flex w-full" onclick={editThreshold}>
+ <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-base-200">
+ <iconify-icon
+ noobserver
+ icon="ph:lightning-fill"
+ class="text-yellow-300"
+ ></iconify-icon>
+ </div>
+ <div
+ class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left"
+ >
+ {user.threshold}
+ </div>
+ <input type="hidden" name="threshold" bind:value={user.threshold} />
+ </button>
+ <p class="text-secondary mt-1">
+ {$t("user.settings.thresholdDesc")}
+ </p>
+ </div>
- {#if tooLarge["banner"]}
- <div class="text-red-600">Max file size 10MB</div>
- {/if}
+ <div>
+ <label for="display" class="font-bold mb-1 block"
+ >{$t("user.settings.reserve")}</label
+ >
+ <button type="button" class="flex w-full" onclick={editReserve}>
+ <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-base-200">
+ <iconify-icon
+ noobserver
+ icon="ph:lightning-fill"
+ class="text-yellow-300"
+ ></iconify-icon>
+ </div>
+ <div
+ class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left"
+ >
+ {user.reserve}
+ </div>
+ <input type="hidden" name="reserve" bind:value={user.reserve} />
+ </button>
+ <p class="text-secondary mt-1">
+ {$t("user.settings.reserveDesc")}
+ </p>
+ </div>
</div>
-<div>
- <label for="about" class="font-bold mb-1 block"
- >{$t("user.settings.about")}</label
+{#if editingThreshold}
+ <div
+ class="fixed bg-base-100 bg-opacity-90 inset-0 overflow-y-auto h-full w-full z-50 max-w-lg mx-auto"
>
- <textarea
- type="text"
- name="about"
- bind:value={about}
- placeholder={$t("user.settings.aboutPlaceholder")}
- ></textarea>
-</div>
+ <div class="relative p-5 border shadow-lg rounded-md bg-base-100 space-y-5">
+ <h1 class="text-center text-2xl font-semibold">
+ {$t("user.settings.threshold")}
+ </h1>
+ <Numpad
+ bind:amount={user.threshold}
+ {currency}
+ bind:rate
+ bind:submit={doneReserve}
+ bind:element={thresholdEl}
+ />
+
+ <button
+ bind:this={doneReserve}
+ type="button"
+ onclick={doneEditing}
+ class="btn">Ok</button
+ >
+ </div>
+ </div>
+{/if}
+
+{#if editingReserve}
+ <div
+ class="fixed bg-base-100 bg-opacity-90 inset-0 overflow-y-auto h-full w-full z-50 mx-auto max-w-lg"
+ >
+ <div
+ class="relative mx-auto p-5 border shadow-lg rounded-md bg-base-100 space-y-5 text-center"
+ >
+ <h1 class="text-2xl font-semibold">
+ {$t("user.settings.reserve")}
+ </h1>
+ <Numpad
+ bind:amount={user.reserve}
+ bind:currency
+ bind:rate
+ bind:submit={doneReserve}
+ bind:element={reserveEl}
+ />
+ <button
+ bind:this={doneReserve}
+ type="button"
+ onclick={doneEditing}
+ class="btn">Ok</button
+ >
+ </div>
+ </div>
+{/if}
+
+{#if connect !== "connected"}
+ <a href={connect} class="btn flex">
+ <img src="/images/square.svg" class="w-12" />
+ <div>{$t("user.settings.connectSquare")}</div></a
+ >
+{:else}
+ <button type="button" onclick={revoke} class="btn">
+ <img
+ src="/images/square.svg"
+ class="w-12"
+ class:invert={$theme === "dark"}
+ />
+ <div>{$t("user.settings.revokeSquare")}</div>
+ </button>
+{/if}
diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte
index 421b590a..7168f4f7 100644
--- a/src/routes/(app)/settings/nostr/+page.svelte
+++ b/src/routes/(app)/settings/nostr/+page.svelte
@@ -163,25 +163,26 @@
<div class="space-y-2">
<div class="font-bold">{$t("user.nostrPubkey")}</div>
- <div class="flex gap-4">
- <textarea name="pubkey" bind:value={npub} rows={3}> </textarea>
- <div class="flex my-auto gap-1">
- <button
- type="button"
- class="my-auto btn btn-circle"
- onclick={() => copy(npub)}
- aria-label="Copy"
- ><iconify-icon noobserver icon="ph:copy-bold" width="32"
- ></iconify-icon></button
- >
- <a
- href={`/qr/${encodeURIComponent(npub)}`}
- class="my-auto btn btn-circle"
- aria-label="QR"
- >
- <iconify-icon noobserver icon="ph:qr-code-bold" width="32"
- ></iconify-icon>
- </a>
+ <textarea name="pubkey" bind:value={npub} rows={3}></textarea>
+ <div>
+ <div class="flex my-auto gap-1">
+ <button
+ type="button"
+ class="my-auto btn btn-circle !w-auto grow"
+ onclick={() => copy(npub)}
+ aria-label="Copy"
+ ><iconify-icon noobserver icon="ph:copy-bold" width="32"
+ ></iconify-icon> {$t("accounts.copy")}</button
+ >
+
+ <a
+ href={`/qr/${encodeURIComponent(npub)}`}
+ class="my-auto btn btn-circle !w-auto grow"
+ aria-label="QR"
+ >
+ <iconify-icon noobserver icon="ph:qr-code-bold" width="32"></iconify-icon>
+{$t("accounts.qr")}
+ </a>
</div>
</div>
diff --git a/src/routes/(app)/settings/preferences/+page.server.ts b/src/routes/(app)/settings/preferences/+page.server.ts
deleted file mode 100644
index f0cc700e..00000000
--- a/src/routes/(app)/settings/preferences/+page.server.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import getRates from "$lib/rates";
-import updateUser from "$lib/settings";
-
-export async function load() {
- const rates = await getRates();
- return { rates };
-}
-
-export const actions = {
- default: updateUser,
-};
diff --git a/src/routes/(app)/settings/preferences/+page.svelte b/src/routes/(app)/settings/preferences/+page.svelte
deleted file mode 100644
index 0f30c61b..00000000
--- a/src/routes/(app)/settings/preferences/+page.svelte
+++ /dev/null
@@ -1,291 +0,0 @@
-<script>
- import { run } from "svelte/legacy";
-
- import { onMount, tick } from "svelte";
- import { browser } from "$app/environment";
- import Numpad from "$comp/Numpad.svelte";
- import LocaleSelector from "$comp/LocaleSelector.svelte";
- import Toggle from "$comp/Toggle.svelte";
- import { locale, t } from "$lib/translations";
- import { post, success, fail } from "$lib/utils";
- import { page } from "$app/stores";
- import { PUBLIC_VAPID_PUBKEY } from "$env/static/public";
-
- let { data } = $props();
- let { user } = $state(data);
- let { rates, subscriptions } = data;
- let { currency } = user;
- let rate = rates[currency];
-
- let fiats = Object.keys(rates).sort((a, b) => a.localeCompare(b));
- let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click());
-
- let editingReserve = $state(),
- editingThreshold = $state(),
- doneReserve = $state(),
- doneThreshold;
- let doneEditing = () => {
- editingReserve = false;
- editingThreshold = false;
- };
-
- let editReserve = async () => {
- editingReserve = true;
- await tick();
- reserveEl.focus();
- };
-
- let editThreshold = async () => {
- editingThreshold = true;
- await tick();
- thresholdEl.focus();
- };
-
- if (!user.threshold) user.threshold = 1000000;
- if (!user.reserve) user.reserve = 100000;
- let reserveEl = $state(),
- thresholdEl = $state();
-
- let push = $state(),
- pm,
- subscription;
-
- onMount(async () => {
- if (!browser) return;
-
- pm =
- navigator?.serviceWorker &&
- (await navigator.serviceWorker.getRegistration()).pushManager;
-
- permission = await pm.permissionState({
- userVisibleOnly: true,
- applicationServerKey: PUBLIC_VAPID_PUBKEY,
- });
-
- if (permission === "granted") {
- subscription = await pm.getSubscription();
- if (subscriptions.includes(JSON.stringify(subscription))) push = true;
- }
- });
-
- let permission = $state();
- let updateNotifications = async (push) => {
- if (!browser || !pm) return (push = false);
-
- if (subscription && !push) {
- return post("/subscription/delete", { subscription });
- }
-
- if (push && permission !== "denied") {
- subscription = await pm.subscribe({
- userVisibleOnly: true,
- applicationServerKey: PUBLIC_VAPID_PUBKEY,
- });
- }
-
- if (subscription) {
- await post("/subscription", { subscription });
- }
- };
- run(() => {
- user.language = $locale;
- });
- run(() => {
- updateNotifications(push);
- });
-</script>
-
-<div>
- <label for="language" class="font-bold block mb-1"
- >{$t("user.settings.locale")}</label
- >
- <LocaleSelector style="select-styles block py-3 w-full" />
-</div>
-
-<div>
- <label for="currency" class="font-bold block mb-1"
- >{$t("user.settings.localCurrency")}</label
- >
- <select name="currency" value={currency}>
- {#each fiats as fiat}
- <option value={fiat}>{fiat}</option>
- {/each}
- </select>
-</div>
-
-<div>
- <label for="currency" class="font-bold block mb-1"
- >{$t("user.settings.integrations")}</label
- >
- <div class="flex flex-wrap gap-2 justify-center">
- <a href="/settings/nostr" class="block grow">
- <button type="button" class="btn">
- <img src="/images/nostr.png" class="w-8" />
- <div class="my-auto">{$t("user.settings.nostr")}</div>
- </button>
- </a>
-
- <a href="/settings/shopify" class="block grow">
- <button type="button" class="btn">
- <img src="/icons/shopify.svg" class="w-8" />
- <div class="my-auto">{$t("user.settings.shopify")}</div>
- </button>
- </a>
- </div>
-</div>
-
-<div>
- <div class="flex justify-between items-center">
- <span class="font-bold">{$t("user.settings.notifications")}</span>
- <Toggle id="notify" bind:value={user.notify} />
- </div>
- <p class="text-secondary mt-1 w-9/12">
- {$t("user.settings.notificationsDesc")}
- </p>
-</div>
-
-<div>
- <div class="flex justify-between items-center">
- <span class="font-bold">{$t("user.settings.pushNotifications")}</span>
- {#if permission !== "denied"}
- <Toggle id="push" bind:value={push} />
- {/if}
- </div>
- <p class="text-secondary mt-1 w-9/12">
- {#if permission === "denied"}
- {$t("user.settings.pushNotificationsDisabled")}
- {:else}
- {$t("user.settings.pushNotificationsDesc")}
- {/if}
- </p>
-</div>
-
-<div>
- <div class="flex justify-between items-center">
- <span class="font-bold">{$t("user.settings.tipPrompt")}</span>
- <Toggle id="prompt" bind:value={user.prompt} />
- </div>
- <p class="text-secondary mt-1 w-9/12">
- {$t("user.settings.tipPromptDescription")}
- </p>
-</div>
-
-<div>
- <div class="flex justify-between items-center">
- <span class="font-bold">{$t("user.settings.autoWithdraw")}</span>
- <Toggle id="autowithdraw" bind:value={user.autowithdraw} />
- </div>
- <p class="text-secondary mt-1 w-9/12">
- {$t("user.settings.autoWithdrawDescription")}
- </p>
-</div>
-
-{#if user.autowithdraw}
- <div class="mb-2">
- <label for="display" class="font-bold mb-1 block"
- >{$t("user.settings.destination")}</label
- >
- <textarea
- name="destination"
- placeholder={$t("user.settings.destinationPlaceholder")}
- onkeypress={keypress}
- class="w-full p-4 border rounded-xl h-48"
- bind:value={user.destination}
- ></textarea>
- </div>
-
- <div>
- <label for="display" class="font-bold mb-1 block"
- >{$t("user.settings.threshold")}</label
- >
- <button type="button" class="flex w-full" onclick={editThreshold}>
- <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-base-200">
- <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300"
- ></iconify-icon>
- </div>
- <div
- class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left"
- >
- {user.threshold}
- </div>
- <input type="hidden" name="threshold" bind:value={user.threshold} />
- </button>
- <p class="text-secondary mt-1">
- {$t("user.settings.thresholdDesc")}
- </p>
- </div>
-
- <div>
- <label for="display" class="font-bold mb-1 block"
- >{$t("user.settings.reserve")}</label
- >
- <button type="button" class="flex w-full" onclick={editReserve}>
- <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-base-200">
- <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300"
- ></iconify-icon>
- </div>
- <div
- class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left"
- >
- {user.reserve}
- </div>
- <input type="hidden" name="reserve" bind:value={user.reserve} />
- </button>
- <p class="text-secondary mt-1">
- {$t("user.settings.reserveDesc")}
- </p>
- </div>
-{/if}
-
-{#if editingThreshold}
- <div
- class="fixed bg-base-100 bg-opacity-90 inset-0 overflow-y-auto h-full w-full z-50 max-w-lg mx-auto"
- >
- <div class="relative p-5 border shadow-lg rounded-md bg-base-100 space-y-5">
- <h1 class="text-center text-2xl font-semibold">
- {$t("user.settings.threshold")}
- </h1>
- <Numpad
- bind:amount={user.threshold}
- {currency}
- bind:rate
- bind:submit={doneReserve}
- bind:element={thresholdEl}
- />
-
- <button
- bind:this={doneReserve}
- type="button"
- onclick={doneEditing}
- class="btn">Ok</button
- >
- </div>
- </div>
-{/if}
-
-{#if editingReserve}
- <div
- class="fixed bg-base-100 bg-opacity-90 inset-0 overflow-y-auto h-full w-full z-50 mx-auto max-w-lg"
- >
- <div
- class="relative mx-auto p-5 border shadow-lg rounded-md bg-base-100 space-y-5 text-center"
- >
- <h1 class="text-2xl font-semibold">
- {$t("user.settings.reserve")}
- </h1>
- <Numpad
- bind:amount={user.reserve}
- bind:currency
- bind:rate
- bind:submit={doneReserve}
- bind:element={reserveEl}
- />
- <button
- bind:this={doneReserve}
- type="button"
- onclick={doneEditing}
- class="btn">Ok</button
- >
- </div>
- </div>
-{/if}
diff --git a/src/routes/(app)/settings/profile/+page.server.ts b/src/routes/(app)/settings/profile/+page.server.ts
new file mode 100644
index 00000000..4145935e
--- /dev/null
+++ b/src/routes/(app)/settings/profile/+page.server.ts
@@ -0,0 +1,5 @@
+import updateUser from "$lib/settings";
+
+export const actions = {
+ default: updateUser,
+};
diff --git a/src/routes/(app)/settings/profile/+page.svelte b/src/routes/(app)/settings/profile/+page.svelte
new file mode 100644
index 00000000..3b7de9d3
--- /dev/null
+++ b/src/routes/(app)/settings/profile/+page.svelte
@@ -0,0 +1,181 @@
+<script>
+ import { tick } from "svelte";
+ import { avatar, banner as bannerStore } from "$lib/store";
+ import { t } from "$lib/translations";
+ import { page } from "$app/stores";
+ import PasswordInput from "$comp/PasswordInput.svelte";
+
+ let { data } = $props();
+ let { user } = $derived(data);
+ let { id } = user;
+ let { about, banner, picture, display, username } = $state(user);
+
+ let avatarFile,
+ avatarInput = $state(),
+ bannerFile,
+ password = $state(),
+ revealPassword = $state(),
+ bannerInput = $state();
+
+ let selectAvatar = () => avatarInput.click();
+ let selectBanner = () => bannerInput.click();
+
+ let percent;
+ let progress = async (event) => {
+ percent = Math.round((event.loaded / event.total) * 100);
+ };
+
+ let tooLarge = $state({});
+
+ let handleFile = async ({ target }, type) => {
+ tooLarge[type] = false;
+ let file = target.files[0];
+ if (!file) return;
+
+ if (file.size > 10000000) return (tooLarge[type] = true);
+
+ if (type === "picture") {
+ $avatar = { id, file, type, progress };
+ } else if (type === "banner") {
+ $bannerStore = { id, file, type, progress };
+ }
+
+ var reader = new FileReader();
+ reader.onload = async (e) => {
+ if (type === "picture") {
+ $avatar.src = e.target.result;
+ } else if (type === "banner") {
+ $bannerStore.src = e.target.result;
+ }
+ };
+
+ reader.readAsDataURL(file);
+ };
+
+ let url = $derived(`${$page.url.host}/${username}`);
+ let full = $derived(`${$page.url.protocol}//${url}`);
+ let addr = $derived(`${username}@${$page.url.host}`);
+</script>
+
+<div>
+ <label for="username" class="font-bold mb-1 block"
+ >{$t("user.settings.username")}</label
+ >
+ <div class="flex mb-2">
+ <input type="text" name="username" bind:value={username} />
+ </div>
+</div>
+
+<input type="hidden" name="confirm" bind:value={password} />
+
+<div>
+ <label for="password" class="block font-bold block mb-1"
+ >{$t("user.settings.newPassword")}</label
+ >
+
+ <PasswordInput bind:value={password} placeholder="(Leave blank to keep unchanged)" />
+</div>
+
+<div>
+ <label for="display" class="font-bold mb-1 block"
+ >{$t("user.settings.displayName")}</label
+ >
+ <input type="text" name="display" bind:value={display} />
+</div>
+
+<div class="space-y-2">
+ <span class="font-bold">{$t("user.settings.profileImage")}</span>
+
+ <div class="flex">
+ {#if $avatar || picture}
+ <div
+ class="relative rounded-full overflow-hidden text-center w-20 h-20 my-auto hover:opacity-80 cursor-pointer"
+ onclick={selectAvatar}
+ onkeydown={selectAvatar}
+ >
+ <img
+ src={$avatar?.src || picture}
+ class="absolute w-full h-full object-cover object-center visible overflow-hidden"
+ alt={username}
+ />
+ </div>
+ {:else}
+ <div
+ class="rounded-full border-4 border-base-100 p-4 bg-base-200 w-24 h-24 my-auto hover:opacity-80 cursor-pointer"
+ onclick={selectAvatar}
+ onkeydown={selectAvatar}
+ ></div>
+ {/if}
+ <div class="ml-2 p-2">
+ <!-- found missing translation -->
+ <button
+ type="button"
+ class="btn"
+ onclick={selectAvatar}
+ onkeydown={selectAvatar}>{$t("user.settings.select")}</button
+ >
+ <input
+ type="file"
+ class="hidden!"
+ bind:this={avatarInput}
+ onchange={(e) => handleFile(e, "picture")}
+ />
+ </div>
+ </div>
+
+ {#if tooLarge["avatar"]}
+ <div class="text-red-600">Max file size 10MB</div>
+ {/if}
+</div>
+
+<div class="space-y-2">
+ <div class="flex justify-between items-center">
+ <span class="font-bold">{$t("user.settings.bannerImage")}</span>
+ </div>
+
+ {#if $bannerStore || banner}
+ <img
+ src={$bannerStore ? $bannerStore.src : banner}
+ class="w-full object-cover object-center visible overflow-hidden h-48 mb-4 hover:opacity-80"
+ onclick={selectBanner}
+ onkeydown={selectBanner}
+ alt="Banner"
+ />
+ {:else}
+ <div
+ class="bg-base-200 w-full h-48 mb-4 cursor-pointer hover:opacity-80"
+ onclick={selectBanner}
+ onkeydown={selectBanner}
+ alt="Banner"
+ ></div>
+ {/if}
+
+ <button
+ type="button"
+ class="btn !w-auto"
+ onclick={selectBanner}
+ onkeydown={selectBanner}>{$t("user.settings.select")}</button
+ >
+ <input
+ type="file"
+ class="hidden!"
+ bind:this={bannerInput}
+ onchange={(e) => handleFile(e, "banner")}
+ />
+
+ {#if tooLarge["banner"]}
+ <div class="text-red-600">Max file size 10MB</div>
+ {/if}
+</div>
+
+<div>
+ <label for="about" class="font-bold mb-1 block"
+ >{$t("user.settings.about")}</label
+ >
+ <textarea
+ type="text"
+ name="about"
+ bind:value={about}
+ placeholder={$t("user.settings.aboutPlaceholder")}
+ ></textarea>
+</div>
diff --git a/src/routes/(app)/settings/security/+page.svelte b/src/routes/(app)/settings/security/+page.svelte
index 5d4b492f..8c38bca1 100644
--- a/src/routes/(app)/settings/security/+page.svelte
+++ b/src/routes/(app)/settings/security/+page.svelte
@@ -4,7 +4,6 @@
import { t } from "$lib/translations";
import Pin from "$comp/Pin.svelte";
import Qr from "$comp/Qr.svelte";
- import PasswordInput from "$comp/PasswordInput.svelte";
import { copy, post, success, fail } from "$lib/utils";
import { save, pin as current } from "$lib/store";
import { invalidate } from "$app/navigation";
@@ -21,9 +20,7 @@
newNsec = $state(),
nsec = $state(),
otp = $state(),
- password = $state(),
pin = $state(""),
- revealPassword = $state(),
revealSeed,
revealNsec = $state(),
token = $state(""),
@@ -157,15 +154,6 @@
</script>
<input type="hidden" name="newpin" value={disablingPin ? "delete" : pin} />
-<input type="hidden" name="confirm" bind:value={password} />
-
-<div>
- <label for="password" class="block font-bold block mb-1"
- >{$t("user.settings.newPassword")}</label
- >
-
- <PasswordInput bind:value={password} />
-</div>
<div>
<span class="font-bold mb-1"
@@ -187,7 +175,8 @@
/>
{:else}
<button type="button" class="btn" onclick={togglePin}>
- <iconify-icon noobserver
+ <iconify-icon
+ noobserver
icon={haspin ? "ph:lock-key-open-bold" : "ph:lock-key-bold"}
width="32"
></iconify-icon>
@@ -220,12 +209,14 @@
</button>
{:else if user.twofa}
<button type="button" class="btn" onclick={startDisabling2fa}>
- <iconify-icon noobserver icon="ph:device-mobile-bold" width="32"></iconify-icon>
+ <iconify-icon noobserver icon="ph:device-mobile-bold" width="32"
+ ></iconify-icon>
<div class="my-auto">{$t("user.settings.twofaDisable")}</div>
</button>
{:else}
<button type="button" class="btn" onclick={startEnabling2fa}>
- <iconify-icon noobserver icon="ph:device-mobile-bold" width="32"></iconify-icon>
+ <iconify-icon noobserver icon="ph:device-mobile-bold" width="32"
+ ></iconify-icon>
<div class="my-auto">{$t("user.settings.twofaSetup")}</div>
</button>
{/if}
@@ -240,3 +231,13 @@
/>
{/if}
</div>
+
+<div>
+ <a href={`/account/savings`} class="contents">
+ <button class="btn btn-lg w-full rounded-2xl whitespace-nowrap">
+ <iconify-icon noobserver icon="ph:plus-circle-bold" width="32"
+ ></iconify-icon>
+ {$t("accounts.addAccount")}
+ </button>
+ </a>
+</div>
diff --git a/src/routes/(app)/settings/square/+page.server.ts b/src/routes/(app)/settings/square/+page.server.ts
new file mode 100644
index 00000000..e00bdf1b
--- /dev/null
+++ b/src/routes/(app)/settings/square/+page.server.ts
@@ -0,0 +1,18 @@
+import { auth, get } from "$lib/utils";
+import { fail, redirect } from "@sveltejs/kit";
+
+export async function load({ cookies, url }) {
+ let connected;
+
+ const query = url.searchParams.toString();
+
+ try {
+ await get(`/square/auth?${query}`, auth(cookies));
+ connected = true;
+ } catch (e) {
+ const { message } = e as Error;
+ fail(400, { message });
+ }
+
+ if (connected) redirect(307, "/settings/account");
+}
diff --git a/src/routes/(app)/settings/square/+page.svelte b/src/routes/(app)/settings/square/+page.svelte
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/routes/(app)/settings/square/+page.svelte
diff --git a/src/routes/(static)/bounties/+page.server.ts b/src/routes/(static)/bounties/+page.server.ts
new file mode 100644
index 00000000..c9d872b0
--- /dev/null
+++ b/src/routes/(static)/bounties/+page.server.ts
@@ -0,0 +1,18 @@
+import { get } from "$lib/utils";
+export const load = async () => {
+ const users = [
+ { username: "fustler", merchant: "rrdrive", type: "Bakery" },
+ { username: "laluna", merchant: "butchersguild", type: "Butcher Shop" },
+ { username: "ica547", merchant: "vanlove", type: "Sushi Restaurant" },
+ { username: "reverendhodl", merchant: "laughingbean", type: "Cafe" },
+ ];
+
+ for (const user of users) {
+ const { username, merchant } = user;
+ console.log(username, merchant);
+ user.user = await get(`/users/${username}`);
+ user.merchant = await get(`/users/${merchant}`);
+ }
+
+ return { users };
+};
diff --git a/src/routes/(static)/bounties/+page.svelte b/src/routes/(static)/bounties/+page.svelte
index b53dc85d..db7a012b 100644
--- a/src/routes/(static)/bounties/+page.svelte
+++ b/src/routes/(static)/bounties/+page.svelte
@@ -1,96 +1,174 @@
-<div class="space-y-5">
- <h1 class="text-3xl mb-6 pt-28 md:pt-12">Bounties</h1>
+<script>
+ import Avatar from "$comp/Avatar.svelte";
+ import { s } from "$lib/utils";
+ import Amount from "$comp/Amount.svelte";
- <p>
- We're offering ⚡️1,000,000 to the first 10 people that help us onboard
- these businesses in Vancouver to take bitcoin payments.
- </p>
+ let { data } = $props();
+ let { users } = data;
- <ul class="text-center text-lg space-y-3">
- <li>Grocery / Produce Store</li>
- <li class="leading-3">
- <span class="line-through">Cafe</span><br />
- <span class="text-xs"
- ><a href="/reverendhodl">ReverendHodl</a> for
- <a href="/laughingbean">Laughing Bean Coffee Co.</a></span
- >
- </li>
- <li>Bakery</li>
- <li class="leading-3">
- <span class="line-through">Butcher Shop</span><br />
- <span class="text-xs"
- ><a href="/danielle">danielle</a> for
- <a href="/butchersguild">All Natural &amp; Exotic Meats</a></span
- >
- </li>
- <li>Laundromat / Dry Cleaner</li>
- <li>Dollar Store / Book Store / Gift Shop</li>
- <li class="leading-3">
- <span class="line-through">Sushi Restaurant</span><br />
- <span class="text-xs"
- ><a href="/ica547">ica547</a> for
- <a href="/metallmash">VanLove Sushi</a></span
+ let bounties = [
+ "Clothing Store",
+ "Hardware Store",
+ "Gas Station",
+ "Shoe Repair",
+ "Dollar Store",
+ "Book Store",
+ "Gift Shop",
+ "Game or Toy Store",
+ "Pet Store",
+ "Pharmacy",
+ ];
+
+ let small = [
+ {
+ type: "Restaurant",
+ amount: 50000,
+ },
+ { type: "Cafe", amount: 30000 },
+ { type: "Tradesperson or Professional", amount: 25000 },
+ { type: "Food Truck", amount: 20000 },
+ { type: "Farmer's Market Vendor", amount: 20000 },
+ ];
+</script>
+
+<div class="space-y-5 pb-20">
+ <h1 class="text-3xl mb-6 md:pt-12 font-bold">Coinos Bounties</h1>
+
+ <p>Earn sats by referring us to local businesses in Vancouver!</p>
+
+ <h2 class="text-2xl font-bold">Currently Available</h2>
+
+ <div class="space-y-0">
+ {#each bounties as b}
+ <div class="flex max-w-xs sm:max-w-xl">
+ <div>First {b}</div>
+
+ <iconify-icon
+ noobserver
+ icon="ph:lightning-fill"
+ class="text-yellow-300 ml-auto"
+ ></iconify-icon>{s(100000, "en-CA")}
+ </div>
+ {/each}
+ </div>
+
+ <hr />
+
+ <div class="space-y-0">
+ {#each small as b}
+ <div class="flex max-w-xs sm:max-w-xl">
+ <div>{b.type}</div>
+
+ <iconify-icon
+ noobserver
+ icon="ph:lightning-fill"
+ class="text-yellow-300 ml-auto"
+ ></iconify-icon>{s(b.amount, "en-CA")}
+ </div>
+ {/each}
+ </div>
+
+ <h2 class="text-2xl font-bold">Previously Awarded</h2>
+
+ <div class="flex flex-col gap-4">
+ {#each users as { user, merchant, type }}
+ <article
+ class="rounded-2xl border border-neutral-200 bg-white/70 p-4 shadow-sm transition hover:shadow-md dark:border-neutral-800 dark:bg-neutral-900/60 sm:max-w-xl"
>
- </li>
- <li>Pizza by the Slice</li>
- <li>Liquor Store / Gas Station</li>
- <li>Bubble Tea / Ice Cream Parlour</li>
- </ul>
+ <div class="flex items-start gap-3 min-w-0 flex-wrap sm:flex-nowrap">
+ <div>
+ <div class="flex">
+ <div class="shrink-0">
+ <Avatar user={merchant} size={12} />
+ </div>
- <p>Eligible businesses:</p>
+ <div class="flex items-center gap-2 min-w-0">
+ <h3
+ class="whitespace-wrap text-base font-semibold max-w-[150px] truncate sm:max-w-full"
+ title={merchant.display || merchant.username}
+ >
+ <a href={`/${merchant.username}`}
+ >{merchant.display || merchant.username}</a
+ >
+ </h3>
- <ul class="text-sm ml-4 space-y-2">
- <li>
- Have a physical location in Vancouver, North Vancouver, West Vancouver,
- Burnaby, New Westminster, or Richmond
- </li>
- <li>Take walk-in customers</li>
- <li>Have regular hours and a website with contact info</li>
- <li>Have a 3 star or better rating on Google</li>
- <li>
- Accept bitcoin during their posted hours without needing to make any
- special arrangements
- </li>
- <li>Use Coinos</li>
- </ul>
+ <span
+ class="shrink-0 rounded-full border border-neutral-200 px-2 py-0.5 text-xs font-medium text-neutral-600 dark:border-neutral-700 dark:text-neutral-300"
+ >
+ {type}
+ </span>
+ </div>
+ </div>
+
+ <div
+ class="mt-2 flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-300 min-w-0"
+ >
+ <Avatar {user} size={8} class="shrink-0" />
+ <span class="truncate max-w-full"
+ >Referred by @{user.username}</span
+ >
+ </div>
+ </div>
+
+ <div class="ml-auto flex shrink-0 items-center gap-1 whitespace">
+ <iconify-icon
+ noobserver
+ icon="ph:lightning-fill"
+ class="text-yellow-300"
+ />
+ <span class="text-sm font-semibold tabular-nums">
+ {s(1000000, "en-CA")}
+ </span>
+ </div>
+ </div>
+ </article>
+ {/each}
+ </div>
+
+ <h2 class="text-2xl font-bold">Terms and Conditions</h2>
- <p>Each business category can be claimed only once</p>
<p>One bounty per person</p>
- <p>
- The businesses can expect free ongoing support and promotion from us and our
- community.
- </p>
+ <div>Eligible businesses should:</div>
+
+ <ul class="ml-4 space-y-0 list-disc">
+ <li>Have a physical location in Greater Vancouver</li>
+ <li>Take walk-ins without appointments</li>
+ <li>Have a website with contact info</li>
+ <li>Have at least a 3-star rating on Google</li>
+ </ul>
- <p>The prize money will be offered to the person who makes the referral.</p>
+ <div>They will be expected to:</div>
+
+ <ul class="ml-4 space-y-0 list-disc">
+ <li>Display a window sticker and QR</li>
+ <li>Train staff on Bitcoin procedures</li>
+ <li>Accept Bitcoin during posted business hours</li>
+ </ul>
<p>
- <b>⚡️250,000</b> will be awarded once the business has met with us and is ready
- to take payments
+ 25% of the bounty will be awarded once the business is ready to take
+ payments and has been announced
</p>
+
<p>
- <b>⚡️750,000</b> will be awarded once the merchant has accepted bitcoin for
- 6 months. We'll be sending
- <a
- href="https://btcmap.org/leaderboard"
- class="text-black hover:underline"
- target="_blank"
- rel="noreferrer">shadowy supertaggers</a
- > to check up on them periodically.
+ The rest will be awarded once they've been accepting Bitcoin for 6 months
</p>
+ <p>Bounties will be awarded at our discretion</p>
+
<p>
- Bounties will be awarded solely at our discretion and we reserve the right
- to refuse any applicants without cause
+ We reserve the right to refuse applicants and change these terms without
+ cause or notice
</p>
- <h2 class="text-xl">Ready to begin!?</h2>
+ <h2 class="text-2xl font-bold">Get Started!</h2>
<p>
- To make a referral, reach out on one of our socials or email us at <a
+ Reach out to <a
href="mailto:[email protected]"
- class="text-black hover:underline">[email protected]</a
- > and we'll set up an appointment with you and the business owner to get started.
+ class="text-blue-600 hover:underline">[email protected]</a
+ > and let us know who we should contact
</p>
<p>
@@ -98,11 +176,10 @@
which ones are still available.
</p>
+ <p>
+ Have a lead or idea for something that's not on the list? Reach out! We
+ offer sats for all sorts of referrals!
+ </p>
+
<p>Thanks for participating and helping to orange pill Vancouver!</p>
</div>
-
-<style>
- a {
- @apply text-blue-400;
- }
-</style>
diff --git a/src/routes/(static)/docs/+page.svelte b/src/routes/(static)/docs/+page.svelte
index 7ac6e013..f53787e5 100644
--- a/src/routes/(static)/docs/+page.svelte
+++ b/src/routes/(static)/docs/+page.svelte
@@ -105,16 +105,18 @@
<div class="text-secondary">
<div>Request params</div>
<div class="grid grid-cols-3 ml-2">
- <div class="font-bold">type</div>
+ <div class="font-bold">type (string, default: lightning)</div>
<div class="col-span-2">bitcoin or lightning</div>
- <div class="font-bold">amount</div>
- <div class="col-span-2">amount in satoshis</div>
- <div class="font-bold">webhook</div>
+ <div class="font-bold">fiat (boolean, default: false)</div>
+ <div class="col-span-2">whether the amount is denominated in satoshis or local currency</div>
+ <div class="font-bold">amount (numeric)</div>
+ <div class="col-span-2">amount to be paid</div>
+ <div class="font-bold">webhook (string, optional)</div>
<div class="col-span-2">
- (optional) endpoint to hit when the invoice is paid
+ remote endpoint to call when the invoice is paid
</div>
- <div class="font-bold">secret</div>
- <div class="col-span-2">a secret for the webhook to check</div>
+ <div class="font-bold">secret (string, optional)</div>
+ <div class="col-span-2">a secret value passed to the webhook to authenticate the request</div>
</div>
</div>
diff --git a/src/routes/(static)/login/+page.server.ts b/src/routes/(static)/login/+page.server.ts
index a24abd07..ffe6335d 100644
--- a/src/routes/(static)/login/+page.server.ts
+++ b/src/routes/(static)/login/+page.server.ts
@@ -11,6 +11,7 @@ export const load = async ({ parent }) => {
export const actions = {
login: async ({ cookies, request }) => {
+ console.log(request.headers.get('referer'));
const form = await fd(request);
let { username, password, token, loginRedirect } = form;
const user = { username, password, token };
diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte
index 1a1f745c..825f6a88 100644
--- a/src/routes/(static)/login/+page.svelte
+++ b/src/routes/(static)/login/+page.svelte
@@ -124,7 +124,7 @@
</script>
<div
- class="mx-auto md:shadow-xl rounded-3xl max-w-xl w-full md:w-[480px] md:p-8 mb-20"
+ class="mx-auto md:shadow-xl rounded-3xl max-w-xl w-full md:w-[480px] md:p-8 mb-20 space-y-5"
>
<h1 class="text-2xl font-bold text-center">{$t("login.signIn")}</h1>
{#if form?.error && !form?.message.includes("2fa")}
diff --git a/src/routes/(static)/login/[token]/+page.server.ts b/src/routes/(static)/login/[token]/+page.server.ts
new file mode 100644
index 00000000..8fe49578
--- /dev/null
+++ b/src/routes/(static)/login/[token]/+page.server.ts
@@ -0,0 +1,13 @@
+import { redirect } from "@sveltejs/kit";
+
+export const load = async ({ cookies, params }) => {
+ const { token } = params;
+ const maxAge = 380 * 24 * 60 * 60;
+
+ const expires = new Date();
+ expires.setSeconds(expires.getSeconds() + maxAge);
+
+ const opts = { path: "/", expires };
+ cookies.set("token", token, opts);
+ redirect(307, "/login");
+};
diff --git a/src/routes/(static)/register/+page.server.ts b/src/routes/(static)/register/+page.server.ts
index 2c2807bd..cddd97ea 100644
--- a/src/routes/(static)/register/+page.server.ts
+++ b/src/routes/(static)/register/+page.server.ts
@@ -1,5 +1,5 @@
-import { fd, get, login, post } from "$lib/utils";
-import { fail, redirect } from "@sveltejs/kit";
+import { fd, get, register } from "$lib/utils";
+import { redirect } from "@sveltejs/kit";
export const load = async ({ parent }) => {
const { user } = await parent();
@@ -14,39 +14,12 @@ export const load = async ({ parent }) => {
export const actions = {
default: async ({ cookies, request }) => {
const ip = request.headers.get("cf-connecting-ip");
-
const form = await fd(request);
- let { picture, username, password, pubkey, loginRedirect } = form;
- const user = { picture, username, password, pubkey };
- let error;
-
+ const { picture, username, password } = form;
+ let { loginRedirect } = form;
if (loginRedirect === "undefined") loginRedirect = undefined;
- let sk;
- try {
- ({ sk } = await post("/register", { user }, { "cf-connecting-ip": ip }));
- } catch (e) {
- ({ message: error } = e as Error);
- }
-
- try {
- await login(user, cookies, ip);
- error = null;
- } catch (e) {
- const { message } = e as Error;
- error ||= message;
- }
-
- const expires = new Date();
- expires.setSeconds(expires.getSeconds() + 21000000);
- cookies.set("sk", sk, {
- path: "/",
- expires,
- httpOnly: false,
- sameSite: "lax",
- });
-
- if (error) return fail(400, { error });
- redirect(303, loginRedirect || `/${user.username}`);
+ const user = { picture, username, password };
+ return register(user, ip, cookies, loginRedirect);
},
};
diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte
index 43cafe6e..b0368d47 100644
--- a/src/routes/(static)/register/+page.svelte
+++ b/src/routes/(static)/register/+page.svelte
@@ -222,7 +222,7 @@
<h1 class="text-2xl font-bold text-center">{$t("login.createAccount")}</h1>
<input
type="file"
- class="hidden"
+ class="hidden!"
bind:this={avatarInput}
onchange={(e) => handleFile(e, "picture")}
/>
@@ -263,9 +263,10 @@
</button>
</div>
- {#if form?.error}
+ {#if form?.error || form?.message}
<div class="text-red-600 text-center" in:fly>
- {form.error}
+ {form?.message}
+ {form?.error}
</div>
{/if}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index d25f570a..7653e05a 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,6 +1,5 @@
<script>
import { run } from "svelte/legacy";
-
import { browser } from "$app/environment";
import { PUBLIC_DOMAIN } from "$env/static/public";
import "../app.css";
@@ -8,21 +7,72 @@
import { onMount } from "svelte";
import { installPrompt, theme as themeStore } from "$lib/store";
+
+const map = { light: 'lofi', dark: 'black', system: 'system' };
+
let { data, children } = $props();
let { pathname, theme } = $state(data);
$effect(() => ($themeStore = data.theme));
$effect(() => (theme = $themeStore));
+$effect(() => {
+ const v = map[theme] ?? theme;
+ if (!v || v === 'system') {
+ document.documentElement.removeAttribute('data-theme');
+ } else {
+ document.documentElement.setAttribute('data-theme', v);
+ }
+});
let host = PUBLIC_DOMAIN.includes("localhost")
? `http://${PUBLIC_DOMAIN}`
: `https://${PUBLIC_DOMAIN}`;
+ function clearBadge() {
+ if ("clearAppBadge" in navigator) {
+ navigator.clearAppBadge().catch(() => {});
+ }
+ }
+
onMount(() => {
if (!browser) return;
+
+ // Handle install prompt
window.addEventListener("beforeinstallprompt", (event) => {
event.preventDefault();
$installPrompt = event;
});
+
+ clearBadge(); // first load
+ document.addEventListener("visibilitychange", () => {
+ if (document.visibilityState === "visible") clearBadge();
+ });
+
+ let hasNotch = typeof window.AndroidNotch !== "undefined";
+ if (hasNotch) {
+ window.AndroidNotch.getInsetTop(
+ (px) => {
+ document.documentElement.style.setProperty(
+ "--safe-area-inset-top",
+ Math.round(px) + "px",
+ );
+ },
+ (err) => {
+ console.error("Failed to get notch inset:", err);
+ },
+ );
+
+ window.AndroidNotch.getInsetBottom(
+ (px) => {
+ document.documentElement.style.setProperty(
+ "--safe-area-inset-bottom",
+ 24 + "px",
+ );
+ },
+ (err) => {
+ console.error("Failed to get bottom inset:", err);
+ },
+ );
+ }
});
</script>
@@ -32,23 +82,20 @@
<meta name="twitter:title" content="coinos" />
<meta property="og:image" content={`${host}/images/logo.webp`} />
-
<meta property="og:type" content="website" />
<meta property="og:description" content="An easy to use bitcoin web wallet" />
<meta name="description" content="An easy to use bitcoin web wallet" />
<meta name="keywords" content="coinos easy bitcoin web wallet" />
-
<meta name="twitter:image" content={`${host}/images/logo.webp`} />
<meta name="twitter:card" content="summary_large_image" />
-
<meta property="og:url" content={host + pathname} />
<meta name="twitter:site" content="@coinoswallet" />
<meta name="twitter:creator" content="@coinoswallet" />
</svelte:head>
{#if !$loading}
- <main data-theme={theme}>
+ <main>
{@render children?.()}
</main>
{/if}
@@ -60,5 +107,13 @@
--toastContainerBottom: 8rem;
--toastContainerLeft: calc(50vw - 8rem);
--toastBackground: #292929;
+
+ --safe-area-inset-top: 0px;
+ --safe-area-inset-bottom: 0px;
+ }
+
+ main {
+ padding-top: var(--safe-area-inset-top);
+ padding-bottom: var(--safe-area-inset-bottom);
}
</style>
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts
index 5679ea61..97184d3d 100644
--- a/src/routes/+page.server.ts
+++ b/src/routes/+page.server.ts
@@ -1,3 +1,3 @@
export const load = async () => {
- return { faqs: ["cost", "compatibility", "safety", "pos", "bitcoin"] };
+ return { faqs: ["cost", "compatibility", "pos", "bitcoin"] };
};
diff --git a/src/routes/phtest/+page.svelte b/src/routes/phtest/+page.svelte
new file mode 100644
index 00000000..625f0465
--- /dev/null
+++ b/src/routes/phtest/+page.svelte
@@ -0,0 +1 @@
+<a href="bitcoin:lnurl1dp68gurn8ghj7cm0d9hx7uewd9hj7up0v9kxjcm9lgkapg">Bitcoin link</a>