aboutsummaryrefslogtreecommitdiffstats
path: root/src
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
parent2abcf6e435fca5d86960f3ba4228002bfb0a19e6 (diff)
parent9277b2921e09ee9dd5f39cf4af30a2330f4d95d1 (diff)
downloadcoinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.tar.gz
coinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.zip
Merge branch 'coinos:master' into master
Diffstat (limited to 'src')
-rw-r--r--src/app.css80
-rw-r--r--src/components/Account.svelte47
-rw-r--r--src/components/Amount.svelte16
-rw-r--r--src/components/AppHeader.svelte2
-rw-r--r--src/components/Balance.svelte29
-rw-r--r--src/components/Footer.svelte2
-rw-r--r--src/components/InvoiceActions.svelte113
-rw-r--r--src/components/Menu.svelte14
-rw-r--r--src/components/Nostr.svelte9
-rw-r--r--src/components/Numpad.svelte414
-rw-r--r--src/components/Payments.svelte11
-rw-r--r--src/components/Pin.svelte8
-rw-r--r--src/components/Qr.svelte14
-rw-r--r--src/components/SetAmount.svelte10
-rw-r--r--src/lib/invoice.ts2
-rw-r--r--src/lib/locales.ts62
-rw-r--r--src/lib/parse.ts25
-rw-r--r--src/lib/random.ts20
-rw-r--r--src/lib/settings.ts2
-rw-r--r--src/lib/socket.ts3
-rw-r--r--src/lib/utils.ts32
-rw-r--r--src/locales/ar.json37
-rw-r--r--src/locales/bn.json37
-rw-r--r--src/locales/de.json37
-rw-r--r--src/locales/el.json37
-rw-r--r--src/locales/en.json66
-rw-r--r--src/locales/es.json37
-rw-r--r--src/locales/fa.json37
-rw-r--r--src/locales/fr.json41
-rw-r--r--src/locales/hi.json37
-rw-r--r--src/locales/it.json37
-rw-r--r--src/locales/ja.json37
-rw-r--r--src/locales/ko.json37
-rw-r--r--src/locales/nl.json37
-rw-r--r--src/locales/pl.json37
-rw-r--r--src/locales/pt.json37
-rw-r--r--src/locales/ru.json37
-rw-r--r--src/locales/th.json37
-rw-r--r--src/locales/tr.json37
-rw-r--r--src/locales/zh.json37
-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
110 files changed, 2985 insertions, 1763 deletions
diff --git a/src/app.css b/src/app.css
index f01c9346..901893b9 100644
--- a/src/app.css
+++ b/src/app.css
@@ -1,48 +1,67 @@
+/* 1) Load Tailwind first */
+@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
-@font-face {
- font-family: "PublicSans";
- src: url("/fonts/Public_Sans/static/PublicSans-Regular.ttf")
- format("truetype");
+/* 2) Register DaisyUI themes */
+@plugin "daisyui" {
+ themes: lofi --default, black --prefersdark;
}
-html {
- font-family: PublicSans, sans-serif;
+@plugin "daisyui/theme" {
+ name: "lofi";
+ default: true;
+ --gradient: #C3CFE2;
+ --color-error: #b91c1c;
}
-::selection {
- color: black;
- background: #eaf2ff;
+@plugin "daisyui/theme" {
+ name: "black";
+ prefersdark: true;
+ --gradient: #C3CFE2;
+ --color-secondary: #CCC;
+ --color-error: #f87171;
}
-:fullscreen {
- background-color: white;
-}
+/* 3) Tell Tailwind which layers we’ll @apply from */
+@reference utilities;
+@reference components;
-input:not([type="range"]) {
- @apply input
+/* 4) Now your custom CSS that uses @apply */
+.btn-menu {
+ @apply flex justify-center items-center bg-base-100 p-2 rounded-full
+ w-12 h-12 sm:w-16 sm:h-16 drop-shadow-xl;
}
-.input {
- @apply input-bordered input-lg input-primary;
+@font-face {
+ font-family: "PublicSans";
+ src: url("/fonts/Public_Sans/static/PublicSans-Regular.ttf") format("truetype");
}
-textarea {
- @apply textarea textarea-bordered textarea-lg textarea-primary;
-}
+html { font-family: PublicSans, sans-serif; }
-select {
- @apply select select-bordered select-lg select-primary;
-}
+::selection { color: black; background: #eaf2ff; }
+:fullscreen { background-color: white; }
-input:not([type="range"]), textarea, select, .input {
- @apply w-full rounded-2xl;
+/* Note: @apply-ing DaisyUI component classes requires components reference */
+.input { @apply input-lg input-primary h-16 px-4; }
+
+/* If you want to *turn* native inputs into DaisyUI inputs via @apply: */
+input[type="text"],
+input[type="email"],
+input[type="password"],
+input[type="number"],
+input[type="search"],
+input[type="url"],
+input[type="tel"] {
+ @apply input input-lg input-primary h-16 px-4;
}
+textarea { @apply textarea textarea-lg textarea-primary py-4; }
+select { @apply select select-lg select-primary h-16 px-4; }
-textarea:focus, textarea:focus-within, .input:focus, .input:focus-within {
- @apply outline-offset-0;
+input:not([type="range"]), textarea, select, .input {
+ @apply rounded-2xl w-full;
}
input.clean, textarea.clean {
@@ -54,6 +73,11 @@ input.clean, textarea.clean {
box-shadow: none !important;
}
-.btn {
- @apply rounded-2xl btn-lg btn-block outline-none
+button { cursor: pointer !important; }
+.btn { @apply rounded-2xl btn-lg btn-block outline-none h-16 cursor-pointer; }
+
+@layer components {
+ :where(input, textarea, select, .input, .textarea, .select) {
+ outline-offset: 0 !important;
+ }
}
diff --git a/src/components/Account.svelte b/src/components/Account.svelte
index 4edd659c..744821a3 100644
--- a/src/components/Account.svelte
+++ b/src/components/Account.svelte
@@ -23,36 +23,23 @@
aria-label="Payments"
>
<div class="flex">
- <Balance {balance} {user} {rate} />
- <a
- href={`/payments`}
- class="contents"
- onclick={setAccount}
- aria-label="Payments"
- >
- <button class="ml-auto flex gap-1 mb-auto pb-4 pl-4">
- <iconify-icon
- noobserver
- icon="ph:clock-bold"
- width="32"
- aria-label="Payments"
- ></iconify-icon>
- </button>
- </a>
- <a
- href={id === user.id ? "/settings/nostr" : `/account/${id}`}
- class="contents"
- aria-label="Settings"
- >
- <button class="flex gap-1 mb-auto pb-4 pl-4">
- <iconify-icon
- noobserver
- icon="ph:gear-bold"
- width="32"
- aria-label="Settings"
- ></iconify-icon>
- </button>
- </a>
+ <Balance {balance} {user} {rate} {id} />
+ {#if id !== user.id}
+ <a
+ href={id === user.id ? "/settings/nostr" : `/account/${id}`}
+ class="contents"
+ aria-label="Settings"
+ >
+ <button class="flex gap-1 mb-auto pb-4 pl-4 ml-auto">
+ <iconify-icon
+ noobserver
+ icon="ph:gear-bold"
+ width="32"
+ aria-label="Settings"
+ ></iconify-icon>
+ </button>
+ </a>
+ {/if}
</div>
<div class="flex justify-center w-full text-xl gap-2">
diff --git a/src/components/Amount.svelte b/src/components/Amount.svelte
index f13949c6..2df2f702 100644
--- a/src/components/Amount.svelte
+++ b/src/components/Amount.svelte
@@ -1,7 +1,17 @@
<script>
import { copy, s, f, toFiat } from "$lib/utils";
import { fiat } from "$lib/store";
- const { align, amount, locale, tip, rate, currency } = $props();
+ const {
+ align,
+ amount,
+ locale,
+ tip,
+ rate,
+ currency,
+ locked = false,
+ } = $props();
+
+ const icon = $derived(locked ? "ph:lock-fill" : "ph:lightning-fill");
</script>
{#if typeof amount !== "undefined"}
@@ -11,14 +21,14 @@
class:justify-center={align !== "left"}
>
<div class="flex items-center" onclick={() => copy(amount)}>
- <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300"
+ <iconify-icon noobserver {icon} class="text-yellow-300"
></iconify-icon>{s(amount, locale)}
</div>
{#if tip}
<div class="flex items-center text-lg ml-2">
<div>+</div>
- <iconify-icon noobserver icon="ph:lightning-fill" class="text-yellow-300"
+ <iconify-icon noobserver {icon} class="text-yellow-300"
></iconify-icon>
<div>{s(tip, locale)}</div>
</div>
diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte
index 7ed4563f..326f488b 100644
--- a/src/components/AppHeader.svelte
+++ b/src/components/AppHeader.svelte
@@ -93,7 +93,7 @@
<style>
.btn-menu {
- @apply flex justify-center items-center bg-base-100 p-2 rounded-full w-12 h-12 sm:w-16 sm:h-16 drop-shadow-xl;
+ @apply flex justify-center items-center bg-base-100 p-2 rounded-full w-12 h-12 drop-shadow-xl;
}
header {
diff --git a/src/components/Balance.svelte b/src/components/Balance.svelte
index da26f6de..dfa5f616 100644
--- a/src/components/Balance.svelte
+++ b/src/components/Balance.svelte
@@ -6,8 +6,8 @@
import { t } from "$lib/translations";
let show = $state();
- let { user, rate, balance } = $props();
- let { currency } = $derived(user);
+ let { id, user, rate, balance } = $props();
+ let { currency, locked } = $derived(user);
let locale = loc(user);
let toggleShow = (e) => {
e.preventDefault();
@@ -23,11 +23,32 @@
<div>
{#if user.haspin && !$pin}
<button onclick={toggleShow} class="flex gap-2 text-xl">
- <iconify-icon noobserver icon="ph:eye-slash-bold" width="32"></iconify-icon>
+ <iconify-icon noobserver icon="ph:eye-slash-bold" width="32"
+ ></iconify-icon>
<div class="my-auto">{$t("user.showBalance")}</div>
</button>
{:else if !isNaN(rate)}
- <Amount amount={balance} {rate} {currency} {locale} align="left" />
+ <div class="flex gap-4">
+ <Amount
+ amount={Math.max(balance - (id === user.id ? (locked || 0) : 0), 0)}
+ {rate}
+ {currency}
+ {locale}
+ align="left"
+ />
+ {#if locked && id === user.id}
+ <div class="text-red-600">
+ <Amount
+ amount={locked}
+ {rate}
+ {currency}
+ {locale}
+ align="left"
+ locked={true}
+ />
+ </div>
+ {/if}
+ </div>
{:else}
<div class="text-gray-200">&mdash;</div>
{/if}
diff --git a/src/components/Footer.svelte b/src/components/Footer.svelte
index 0cab1d13..8d2275ca 100644
--- a/src/components/Footer.svelte
+++ b/src/components/Footer.svelte
@@ -5,7 +5,7 @@
const followLinks = [
{
- url: "nostr:nprofile1qqst4qyeqenw7zm0fwjsty68h6cnys5jre2xd8ngqpjv5a2j26s78fspz4mhxue69uhhyetvv9ujucm0d9hx7uewd9hsgwx239",
+ url: "nostr:nprofile1qy28wue69uhnzv3h9cczuvpwxyargwpk8yhszxrhwvaz7tmhda6zucnjd9nksarzdak8gtnwv46z7qpqw27mc4aa6m0ufe3xs5z3m6qyr52gc0rglept7vqlwx4xeaf72tasv5xyqv",
titleID: "nostr",
},
{ url: "https://t.me/coinoswallet", titleID: "telegram" },
diff --git a/src/components/InvoiceActions.svelte b/src/components/InvoiceActions.svelte
index b11a66c3..e2a31ab8 100644
--- a/src/components/InvoiceActions.svelte
+++ b/src/components/InvoiceActions.svelte
@@ -45,62 +45,6 @@
<div class="my-auto text-lg">Liquid</div>
</button>
- <div class="flex gap-2">
- <button
- class="btn flex-nowrap !w-auto grow"
- class:bg-base-300={type === types.lightning}
- class:text-secondary={type === types.lightning}
- onclick={() => {
- $amountPrompt = false;
- newAmount = undefined;
- setType(types.lightning);
- }}
- >
- <div
- class="bg-black rounded-full w-8 h-8 items-center justify-center flex"
- >
- <div class="m-auto">
- <iconify-icon
- noobserver
- icon="ph:lightning-fill"
- class="text-yellow-300 text-2xl"
- ></iconify-icon>
- </div>
- </div>
- <div class="my-auto text-lg">Lightning</div>
- </button>
-
- <button
- class="btn flex-nowrap !w-auto grow"
- class:bg-base-300={type === types.bitcoin}
- class:text-secondary={type === types.bitcoin}
- onclick={() => setType(types.bitcoin)}
- >
- <img src="/images/bitcoin.svg" class="w-8" alt="Bitcoin" />
- <div class="my-auto text-lg">Bitcoin</div>
- </button>
- </div>
-
- <div class="flex flex-wrap gap-2 justify-around text-secondary w-full">
- <button type="button" class="btn !w-auto grow" onclick={() => copy(txt)}>
- <iconify-icon noobserver icon="ph:copy-bold" width="32"></iconify-icon>
- <div class="my-auto">
- {t("payments.copy")}
- </div>
- </button>
-
- <button class="btn grow !w-auto" onclick={() => goto("/receive")}>
- <iconify-icon noobserver icon="ph:clipboard-text-bold" width="32"
- ></iconify-icon>
- <div class="my-auto text-lg">{t("user.send.paste")}</div>
- </button>
-
- <button class="btn grow !w-auto" onclick={() => goto("/scan")}>
- <iconify-icon noobserver icon="ph:camera-bold" width="32"></iconify-icon>
- <div class="my-auto text-lg">{t("user.send.scan")}</div>
- </button>
- </div>
-
<div class="flex flex-wrap gap-2 justify-around text-secondary w-full">
{#if user?.id === invoice?.user?.id}
<button class="btn grow !w-auto" onclick={toggleAmount}>
@@ -127,6 +71,26 @@
</button>
</div>
+ <div class="flex flex-wrap gap-2 justify-around text-secondary w-full">
+ <button type="button" class="btn !w-auto grow" onclick={() => copy(txt)}>
+ <iconify-icon noobserver icon="ph:copy-bold" width="32"></iconify-icon>
+ <div class="my-auto">
+ {t("payments.copy")}
+ </div>
+ </button>
+
+ <button class="btn grow !w-auto" onclick={() => goto("/receive")}>
+ <iconify-icon noobserver icon="ph:clipboard-text-bold" width="32"
+ ></iconify-icon>
+ <div class="my-auto text-lg">{t("user.send.paste")}</div>
+ </button>
+
+ <button class="btn grow !w-auto" onclick={() => goto("/scan")}>
+ <iconify-icon noobserver icon="ph:camera-bold" width="32"></iconify-icon>
+ <div class="my-auto text-lg">{t("user.send.scan")}</div>
+ </button>
+ </div>
+
{#if txt.length > 120}
<div class="w-full flex justify-center gap-2 flex-wrap">
<button class="btn" onclick={() => (showQr = !showQr)}>
@@ -144,6 +108,43 @@
</div>
{/if}
+ <div class="flex gap-2">
+ <button
+ class="btn flex-nowrap !w-auto grow"
+ class:bg-base-300={type === types.lightning}
+ class:text-secondary={type === types.lightning}
+ onclick={() => {
+ $amountPrompt = false;
+ newAmount = undefined;
+ setType(types.lightning);
+ }}
+ >
+ <div
+ class="bg-black rounded-full w-8 h-8 items-center justify-center flex"
+ >
+ <div class="m-auto">
+ <iconify-icon
+ noobserver
+ icon="ph:lightning-fill"
+ class="text-yellow-300 text-2xl"
+ ></iconify-icon>
+ </div>
+ </div>
+ <div class="my-auto text-lg">Lightning</div>
+ </button>
+
+ <button
+ class="btn flex-nowrap !w-auto grow"
+ class:bg-base-300={type === types.bitcoin}
+ class:text-secondary={type === types.bitcoin}
+ onclick={() => setType(types.bitcoin)}
+ >
+ <img src="/images/bitcoin.svg" class="w-8" alt="Bitcoin" />
+ <div class="my-auto text-lg">Bitcoin</div>
+ </button>
+ </div>
+
+
<button type="button" class="btn" onclick={toggleType}>
<img src="/images/liquid.svg" class="w-8" alt="Liquid" />
<img src="/images/cash.png" class="w-8 my-auto" />
diff --git a/src/components/Menu.svelte b/src/components/Menu.svelte
index 7408687c..e433fefb 100644
--- a/src/components/Menu.svelte
+++ b/src/components/Menu.svelte
@@ -1,7 +1,7 @@
<script>
import { theme } from "$lib/store";
import DarkToggle from "$comp/DarkToggle.svelte";
- import OutClick from "svelte-outclick";
+ import { OutClick } from "svelte-outclick";
let { t, user, w, opacity } = $props();
let dark = () => ($theme = $theme === "dark" ? "light" : "dark");
@@ -11,11 +11,11 @@
{ key: "nav.settings", icon: "ph:gear-bold", href: `/settings` },
{ key: "nav.support", icon: "ph:lifebuoy-bold", href: `/support` },
{ key: "nav.map", icon: "ph:map-trifold-bold", href: `/map` },
- {
- key: "nav.merch",
- icon: "ph:storefront-bold",
- href: `https://coinosmerch.com`,
- },
+ // {
+ // key: "nav.merch",
+ // icon: "ph:storefront-bold",
+ // href: `https://coinosmerch.com`,
+ // },
{ key: "nav.dark", icon: "ph:moon-stars-bold", href: `/dark` },
{ key: "nav.signOut", icon: "ph:sign-out-bold", href: `/logout` },
];
@@ -25,7 +25,7 @@
</script>
<div>
- <OutClick on:outclick={hideMenu}>
+ <OutClick onOutClick={hideMenu}>
<button class="btn-menu {opacity('/support')}" onclick={toggleMenu}
><iconify-icon noobserver icon="ph:list-bold" width={w > 640 ? 32 : 24}
></iconify-icon>
diff --git a/src/components/Nostr.svelte b/src/components/Nostr.svelte
index ad70cd31..a616194b 100644
--- a/src/components/Nostr.svelte
+++ b/src/components/Nostr.svelte
@@ -13,7 +13,6 @@
getPublicKey,
} from "nostr-tools";
import { getConversationKey, decrypt } from "nostr-tools/nip44";
- import { AmberClipboardSigner } from "applesauce-signer";
import { copy, focus, fail, post } from "$lib/utils";
import { eventToSign, signer } from "$lib/store";
@@ -105,14 +104,6 @@
$signer = { method: "extension", ready: true };
};
- let signerGetPubkey = async () => {
- const pubkey = await new AmberClipboardSigner().getPublicKey();
- $signer = {
- method: "signer",
- params: { pubkey },
- };
- };
-
let signerSign = async () => {
document.addEventListener("focus", signerSigned, true);
};
diff --git a/src/components/Numpad.svelte b/src/components/Numpad.svelte
index d80f0400..c321e0bc 100644
--- a/src/components/Numpad.svelte
+++ b/src/components/Numpad.svelte
@@ -1,23 +1,30 @@
<script>
import { onMount } from "svelte";
- import { fiat } from "$lib/store";
import Left from "$comp/Left.svelte";
- import Icon from "$comp/Icon.svelte";
import { f, s, focus, warning, sat, sats } from "$lib/utils";
import { t } from "$lib/translations";
+ import { fiat as fiatStore } from "$lib/store";
- export let amount = 0,
+ // ---- Props (Svelte 5 runes) ----
+ let {
+ amount = $bindable(),
currency = "USD",
+ fiat = $bindable(),
element,
- rate,
+ rate = $bindable(),
locale,
- submit = undefined;
+ submit = undefined,
+ amountFiat = 0,
+ } = $props();
- export let amountFiat = 0;
+ // ---- Local state ----
+ let fiatDigits = $state("0"); // cents buffer in fiat mode
+ let html = $state("0");
- let pasted = async () => {
- html = (await navigator.clipboard.readText()).replace(/,/g, "");
- };
+ // currency info as reactive state
+ let decimalChar = $state(".");
+ let symbol = $state("");
+ let position = $state("before");
function getCurrencyInfo(locale = "en-US", currency) {
const formatter = new Intl.NumberFormat(locale, {
@@ -27,168 +34,232 @@
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
-
const parts = formatter.formatToParts(123456.78);
- const { value: decimal } = parts.find((part) => part.type === "decimal");
- const currencyPart = parts.find((part) => part.type === "currency");
- const symbol = currencyPart ? currencyPart.value : "";
+ const { value: decimal } = parts.find((p) => p.type === "decimal");
+ const currencyPart = parts.find((p) => p.type === "currency");
+ const sym = currencyPart ? currencyPart.value : "";
+ const firstNumberIndex = parts.findIndex((p) => p.type === "integer");
+ const currencyIndex = parts.findIndex((p) => p.type === "currency");
+ const pos = currencyIndex < firstNumberIndex ? "before" : "after";
+ return { symbol: sym, position: pos, decimal };
+ }
- const firstNumberIndex = parts.findIndex((part) => part.type === "integer");
- const currencyIndex = parts.findIndex((part) => part.type === "currency");
- const position = currencyIndex < firstNumberIndex ? "before" : "after";
+ // reflect external prop/locale changes
+ $effect(() => {
+ const info = getCurrencyInfo(locale, currency);
+ decimalChar = info.decimal;
+ symbol = info.symbol;
+ position = info.position;
+ });
- return { symbol, position, decimal };
+ function formatFiatFromDigits(digits, dec = ".") {
+ let d = digits.replace(/\D+/g, "");
+ if (!d) d = "0";
+ if (d.length > 15) d = d.slice(0, 15);
+ if (d.length === 1) d = "0" + d;
+ if (d.length === 2) d = "0" + d;
+ const whole = d.slice(0, -2).replace(/^0+(?=\d)/, "") || "0";
+ const cents = d.slice(-2);
+ return `${whole}${dec}${cents}`;
}
- function strip(input) {
- const regex = new RegExp(`[^0-9\.]`, "g");
- return input.replace(decimal, ".").replace(regex, "");
+ function normalizeSats(str) {
+ const digits = (str ?? "").replace(/[^\d]/g, "");
+ const noLead = digits.replace(/^0+(?=\d)/, "");
+ return noLead || "0";
}
- let { decimal, symbol, position } = getCurrencyInfo(locale, currency);
-
- let arrow = "<";
-
- $: update(amount, amountFiat);
- let update = (a, f) => {
- if ($fiat) {
- amount = f ? Math.round(strip(f) / (rate / sats)) : 0;
+ const updateByMode = () => {
+ if (fiat) {
+ const numericFiat =
+ parseFloat((fiatDigits || "0").replace(/\D/g, "")) / 100;
+ amountFiat = isFinite(numericFiat) ? numericFiat.toFixed(2) : "0.00";
+ amount = numericFiat ? Math.round((numericFiat * sats) / rate) : 0;
} else {
- amountFiat = a ? ((a * rate) / sats).toFixed(2) : 0;
+ // integer-only sats, already normalized
+ const a = parseInt((html || "0").replace(/[^\d]/g, "")) || 0;
+ amount = a || 0;
+ const fval = amount ? (amount * rate) / sats : 0;
+ amountFiat = fval ? fval.toFixed(2) : "0.00";
}
};
- let loading = false;
-
- const numPad = [
- "1",
- "2",
- "3",
- "4",
- "5",
- "6",
- "7",
- "8",
- "9",
- decimal,
- "0",
- "<",
- ];
-
- const handleInput = (value) => {
- if (value === "<") html = html.substr(0, html.length - 1);
- else html += value;
+ const pasted = async () => {
+ const txt = (await navigator.clipboard.readText()) ?? "";
+ if (fiat) {
+ const m = txt.replace(/[^\d.,]/g, "");
+ const normalized = m.replace(new RegExp(`[${decimalChar}]`, "g"), ".");
+ const val = parseFloat(normalized);
+ const cents = isFinite(val) ? Math.round(val * 100) : 0;
+ fiatDigits = Math.max(0, cents).toString();
+ html = formatFiatFromDigits(fiatDigits, decimalChar);
+ } else {
+ const clean = normalizeSats(txt);
+ html = clean;
+ element && (element.innerHTML = html);
+ }
input(null, true);
};
- $: html = $fiat ? amountFiat : (amount || 0).toString();
-
- let prev = "";
-
- let input = (e, numpad = false) => {
- let d = decimal.replace(".", "\\.");
-
- if (!$fiat && html.includes(decimal)) {
- html = html.replace(new RegExp(d), "");
- }
-
- if (prev === "0" && html !== `0${decimal}`) {
- prev = "";
- html = html.replace("0", "");
- }
-
- let sel = getSelection();
- let i = sel.focusOffset;
-
- if (html[0] === "0") i = html.length;
-
- if (html.split(decimal)[0][0] === "0" && html.length > 1) {
- html = html.replace("0", "");
- i++;
- }
+ let prevHtml = $state("");
- if (html[0] === decimal) {
- html = `0${html}`;
- i = html.length;
- }
+ // (kept for completeness; no longer used in sats path)
+ function trimTrailingZeros(str) {
+ if (!str.includes(".")) return str;
+ let out = str.replace(/(\.\d*?[1-9])0+$/, "$1");
+ out = out.replace(/\.0*$/, "");
+ return out;
+ }
- if (html.includes(decimal) && html.split(decimal)[1].length > 2) {
- html = html.slice(0, html.indexOf(decimal) + 3);
- i = html.length;
+ const input = (e, fromPad = false) => {
+ if (fiat) {
+ if (!fromPad) {
+ const raw = (element?.textContent ?? "").replace(/[^\d]/g, "");
+ fiatDigits = raw || "0";
+ }
+ const cents = parseInt(fiatDigits || "0") || 0;
+ const numericFiat = cents / 100;
+ const satsEstimate = Math.round((numericFiat * sats) / rate);
+ if (satsEstimate > sats && html.length > prevHtml.length) {
+ fiatDigits = fiatDigits.slice(0, -1) || "0";
+ warning($t("user.receive.lessThan1BTCWarning"));
+ }
+ html = formatFiatFromDigits(fiatDigits, decimalChar);
+ } else {
+ // integer-only sats, normalize leading zeros
+ let clean = normalizeSats(element?.textContent ?? "");
+ const maybe = parseInt(clean) || 0;
+ if (maybe > sats && clean.length > prevHtml.length) {
+ clean = normalizeSats(clean.slice(0, -1));
+ warning($t("user.receive.lessThan1BTCWarning"));
+ }
+ html = clean;
+ element && (element.innerHTML = html);
}
- let clean = html
- .substr(0, 15)
- .replace(new RegExp(`[^0-9${d}]+`, "g"), "")
- .replace(new RegExp(d), "X")
- .replace(new RegExp(d, "g"), "")
- .replace("X", decimal);
+ updateByMode();
- if (clean !== html) {
- html = clean;
- i = html.length;
- }
- if (amount > sats && html.length > prev.length) {
- html = html.substr(0, html.length - 1);
- i = html.length;
- warning($t("user.receive.lessThan1BTCWarning"));
+ if (!fromPad) {
+ queueMicrotask(() => {
+ const node = element?.childNodes?.[0];
+ if (!node) return;
+ const range = document.createRange();
+ const sel = getSelection();
+ const L = node.length ?? node.textContent?.length ?? 0;
+ range.setStart(node, L);
+ range.setEnd(node, L);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ });
}
+ prevHtml = html.toString();
+ };
- if ($fiat) {
- amountFiat = html;
+ const handleInput = (value) => {
+ if (fiat) {
+ if (value === arrow) {
+ fiatDigits = fiatDigits.length > 1 ? fiatDigits.slice(0, -1) : "0";
+ } else if (value === "00") {
+ fiatDigits = (fiatDigits === "0" ? "" : fiatDigits) + "00";
+ if (!fiatDigits) fiatDigits = "0";
+ } else {
+ fiatDigits = (fiatDigits === "0" ? "" : fiatDigits) + value;
+ }
+ html = formatFiatFromDigits(fiatDigits, decimalChar);
} else {
- amount = parseInt(html);
- }
-
- if (!amount) amount = null;
-
- if (!numpad) {
- setTimeout(() => {
- let node = element.childNodes[0];
- let range = document.createRange();
-
- if (node) {
- if (i > node.length) i = node.length;
- range.setStart(node, i);
- range.setEnd(node, i);
-
- let sel = getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
- node.selectionStart = node.selectionEnd;
- }
-
- prev = html.toString();
- }, 0);
+ if (value === arrow) {
+ html = html.length > 1 ? html.slice(0, -1) : "0";
+ } else if (value === "00") {
+ // If current is "0", keep it "0"; otherwise append two zeros
+ html = html === "0" ? "0" : html + "00";
+ } else {
+ // If current is "0", replace with the digit; else append
+ html = html === "0" ? value : html + value;
+ }
+ // normalize away any accidental leading zeros
+ html = normalizeSats(html);
+ element && (element.innerHTML = html);
}
+ input(null, true);
};
- let select = (e) => {
- let range = document.createRange();
+ const select = (e) => {
+ const range = document.createRange();
range.selectNodeContents(e.target);
- let sel = getSelection();
+ const sel = getSelection();
sel.removeAllRanges();
sel.addRange(range);
};
- let blur = () => {
- if (!html) html = "0";
+ const blur = () => {
+ if (!html) html = fiat ? formatFiatFromDigits("0", decimalChar) : "0";
};
- let keydown = (e) =>
- e.key === "Enter" && (e.preventDefault() || submit.click());
+ const keydown = (e) =>
+ e.key === "Enter" && (e.preventDefault() || submit?.click?.());
- let swap = () => {
- if ($fiat) {
- amount = parseInt((amountFiat / (rate / sats)).toFixed(0));
+ // CHANGED: make swap set html for the new mode immediately and keep DOM in sync
+ const swap = () => {
+ if (fiat) {
+ // going -> sats
+ fiat = false;
+ html = normalizeSats((amount || 0).toString());
+ element && (element.innerHTML = html);
} else {
- amountFiat =
- (amount * (rate / sats)).toFixed(2) > 0.0
- ? (amount * (rate / sats)).toFixed(2)
- : 0;
+ // going -> fiat
+ const cents = Math.round((parseFloat(amountFiat || "0") || 0) * 100);
+ fiatDigits = Math.max(0, cents).toString();
+ fiat = true;
+ html = formatFiatFromDigits(fiatDigits, decimalChar);
+ element && (element.innerHTML = html);
}
- $fiat = !$fiat;
+ updateByMode();
+ prevHtml = html.toString();
+
+ // Move caret to end after swap
+ queueMicrotask(() => {
+ const node = element?.childNodes?.[0];
+ if (!node) return;
+ const range = document.createRange();
+ const sel = getSelection();
+ const L = node.length ?? node.textContent?.length ?? 0;
+ range.setStart(node, L);
+ range.setEnd(node, L);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ });
+
+ $fiatStore = fiat;
};
+
+ const arrow = "<";
+ const numPad = [
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "00",
+ "0",
+ arrow,
+ ];
+
+ onMount(() => {
+ fiat = $fiatStore;
+ if (fiat) {
+ const cents = Math.round((parseFloat(amountFiat || "0") || 0) * 100);
+ fiatDigits = Math.max(0, cents).toString();
+ html = formatFiatFromDigits(fiatDigits, decimalChar); // keep html in sync on mount
+ } else {
+ html = normalizeSats((amount || 0).toString());
+ }
+ element && (element.innerHTML = html);
+ updateByMode();
+ });
</script>
<div class="flex justify-center items-center">
@@ -197,11 +268,9 @@
<div
class="text-5xl md:text-6xl font-semibold tracking-widest flex justify-center"
>
- <div class="my-auto" class:text-5xl={!$fiat}>
- {#if $fiat}
- {#if position === "before"}
- {symbol}
- {/if}
+ <div class="my-auto" class:text-5xl={!fiat}>
+ {#if fiat}
+ {#if position === "before"}{symbol}{/if}
{:else}
<iconify-icon
noobserver
@@ -210,27 +279,47 @@
></iconify-icon>
{/if}
</div>
+
+ <!-- contenteditable input -->
<div
use:focus
+ role="textbox"
+ tabindex="0"
+ aria-label="Amount input"
+ aria-multiline="false"
contenteditable
bind:innerHTML={html}
- on:paste={pasted}
- on:focus={select}
- on:blur={blur}
- on:input={input}
- on:keydown={keydown}
+ inputmode={fiat ? "decimal" : "numeric"}
+ enterkeyhint="done"
+ autocapitalize="off"
+ autocorrect="off"
+ spellcheck="false"
+ onpaste={pasted}
+ onfocus={(e) => {
+ select(e);
+ }}
+ onblur={blur}
+ oninput={input}
+ onkeydown={keydown}
class="outline-none my-auto"
bind:this={element}
- />
- {#if $fiat && position === "after"}
- {symbol}
- {/if}
+ ></div>
+
+ {#if fiat && position === "after"}{symbol}{/if}
</div>
- <div
- class="flex items-center justify-center text-2xl cursor-pointer"
- on:click|preventDefault|stopPropagation={swap}
+
+ <!-- swap button -->
+ <button
+ type="button"
+ class="flex items-center justify-center text-2xl cursor-pointer mx-auto select-none"
+ aria-label="Swap currency display"
+ onclick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ swap();
+ }}
>
- {#if $fiat}
+ {#if fiat}
<iconify-icon
noobserver
icon="ph:lightning-fill"
@@ -240,7 +329,7 @@
{:else}
{f(amountFiat, currency, locale)}
{/if}
- </div>
+ </button>
</div>
<div class="grid grid-cols-3 gap-2 w-full mx-auto">
@@ -249,7 +338,12 @@
<button
type="button"
class="btn"
- on:click|preventDefault|stopPropagation={() => handleInput(value)}
+ aria-label="Backspace"
+ onclick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleInput(value);
+ }}
>
<Left />
</button>
@@ -257,9 +351,15 @@
<button
type="button"
class="btn"
- on:click|preventDefault|stopPropagation={() => handleInput(value)}
- >{value}</button
+ aria-label={`Key ${value}`}
+ onclick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleInput(value);
+ }}
>
+ {value}
+ </button>
{/if}
{/each}
</div>
diff --git a/src/components/Payments.svelte b/src/components/Payments.svelte
index 814a5b4e..36633089 100644
--- a/src/components/Payments.svelte
+++ b/src/components/Payments.svelte
@@ -6,6 +6,7 @@
import { t } from "$lib/translations";
import locales from "$lib/locales";
const { fund, locale, user, payments } = $props();
+ const language = $derived(user?.language || "en");
</script>
<div class="text-base">
@@ -106,7 +107,11 @@
{#if p.type === types.ecash}
{amount > 0 ? $t("payments.redeemed") : $t("payments.minted")}
{:else}
- {amount > 0 ? (p.confirmed ? $t("payments.received") : $t("payments.pending")) : $t("payments.sent")}
+ {amount > 0
+ ? p.confirmed
+ ? $t("payments.received")
+ : $t("payments.pending")
+ : $t("payments.sent")}
{/if}
</div>
</div>
@@ -116,12 +121,12 @@
<div class="text-secondary text-right text-sm my-auto col-span-3">
<div>
{format(new Date(p.created), "h:mm aaa", {
- locale: locales[user.language],
+ locale: locales[language],
})}
</div>
<div>
{format(new Date(p.created), "MMM d, yy", {
- locale: locales[user.language],
+ locale: locales[language],
})}
</div>
</div>
diff --git a/src/components/Pin.svelte b/src/components/Pin.svelte
index a429e976..980e17fa 100644
--- a/src/components/Pin.svelte
+++ b/src/components/Pin.svelte
@@ -20,7 +20,7 @@
let loaded;
onMount(() => setTimeout(() => (loaded = true), 500));
- let locktime = 30 * 60;
+ let locktime = 5 * 60;
$: loaded && checkPin(p);
let checkPin = async (p) => {
@@ -34,7 +34,10 @@
if (result) {
if (notify) success("Pin confirmed");
- if (locktime) setCookie("pin", p, locktime);
+ if (locktime) {
+ setCookie("pin", p, locktime);
+ setTimeout(() => ($pin = undefined), locktime * 1000);
+ }
$pin = p;
} else {
fail("Invalid pin");
@@ -63,7 +66,6 @@
<select name="locktime" bind:value={locktime}>
<option value={30}>30 {$t("user.settings.seconds")}</option>
<option value={5 * 60}>5 {$t("user.settings.minutes")}</option>
- <option value={10 * 60}>10 {$t("user.settings.minutes")}</option>
<option value={30 * 60}>30 {$t("user.settings.minutes")}</option>
<option value={60 * 60}>1 {$t("user.settings.hour")}</option>
<option value={8 * 60 * 60}>8 {$t("user.settings.hours")}</option>
diff --git a/src/components/Qr.svelte b/src/components/Qr.svelte
index 72655ebc..da4ceb00 100644
--- a/src/components/Qr.svelte
+++ b/src/components/Qr.svelte
@@ -1,18 +1,18 @@
<script>
import screenfull from "screenfull";
- let { text } = $props();
+ let { icon, text } = $props();
let qr = $state();
+
+ let src = `/qr/${encodeURIComponent(text)}`;
+ if (icon) src += `/${icon}`;
+ else src += "/raw";
</script>
<div
- class="w-80 mx-auto cursor-pointer"
+ class="w-72 sm:w-80 mx-auto cursor-pointer"
bind:this={qr}
onclick={() => screenfull.toggle(qr)}
onkeydown={() => screenfull.toggle(qr)}
>
- <img
- src={`/qr/${encodeURIComponent(text)}/raw`}
- class="mx-auto"
- alt="QR Code"
- />
+ <img {src} class="mx-auto" alt="QR Code" />
</div>
diff --git a/src/components/SetAmount.svelte b/src/components/SetAmount.svelte
index a453b9d1..536c8e24 100644
--- a/src/components/SetAmount.svelte
+++ b/src/components/SetAmount.svelte
@@ -6,13 +6,13 @@
let submit = $state();
let {
- currency = $bindable(),
- fiat = $bindable(),
- locale = $bindable(),
- rate = $bindable(),
+ currency = $bindable("USD"),
+ fiat = $bindable(true),
+ locale = $bindable("en-US"),
+ rate = $bindable(0),
settingAmount,
setAmount,
- newAmount = $bindable(),
+ newAmount = $bindable(0),
toggleAmount,
t,
} = $props();
diff --git a/src/lib/invoice.ts b/src/lib/invoice.ts
index d7576e82..91a69d88 100644
--- a/src/lib/invoice.ts
+++ b/src/lib/invoice.ts
@@ -15,7 +15,7 @@ export default async ({ cookies, request }) => {
items: JSON.parse(form.get("items")),
memo: form.get("memo"),
memoPrompt: form.get("memoPrompt"),
- tip: parseInt(form.get("tip")) || 0,
+ tip: form.get("tip") ?? null,
type: form.get("type"),
prompt: form.get("prompt") === "true",
rate: parseFloat(form.get("rate")) || rates[form.get("currency")],
diff --git a/src/lib/locales.ts b/src/lib/locales.ts
index b87ecc7a..68a28abd 100644
--- a/src/lib/locales.ts
+++ b/src/lib/locales.ts
@@ -1,23 +1,41 @@
-import {
- de,
- el,
- enUS as en,
- es,
- faIR as fa,
- fr,
- ja,
- pt,
- ru,
- zhCN as zh,
- arSA as ar,
- hiIN as hi,
- bnBD as bn,
- tr,
- ko,
- it,
- nl,
- th,
- pl
-} from "date-fns/locale/index.js";
+import de from "date-fns/locale/de";
+import el from "date-fns/locale/el";
+import en from "date-fns/locale/en-US";
+import es from "date-fns/locale/es";
+import fa from "date-fns/locale/fa-IR";
+import fr from "date-fns/locale/fr";
+import ja from "date-fns/locale/ja";
+import pt from "date-fns/locale/pt";
+import ru from "date-fns/locale/ru";
+import zh from "date-fns/locale/zh-CN";
+import ar from "date-fns/locale/ar-SA";
+import hi from "date-fns/locale/hi";
+import bn from "date-fns/locale/bn";
+import tr from "date-fns/locale/tr";
+import ko from "date-fns/locale/ko";
+import it from "date-fns/locale/it";
+import nl from "date-fns/locale/nl";
+import th from "date-fns/locale/th";
+import pl from "date-fns/locale/pl";
-export default { de, el, en, es, fa, fr, ja, pt, ru, zh, ar, hi, bn, tr, ko, it, nl, th, pl };
+export default {
+ de,
+ el,
+ en,
+ es,
+ fa,
+ fr,
+ ja,
+ pt,
+ ru,
+ zh,
+ ar,
+ hi,
+ bn,
+ tr,
+ ko,
+ it,
+ nl,
+ th,
+ pl
+};
diff --git a/src/lib/parse.ts b/src/lib/parse.ts
index ba264be7..9c18504a 100644
--- a/src/lib/parse.ts
+++ b/src/lib/parse.ts
@@ -8,11 +8,18 @@ export default async (s, host) => {
if (!s) return;
let t = s.trim();
let amount;
+ let invoice;
let user;
if (t.startsWith("http")) redirect(307, t);
if (t.startsWith(host)) redirect(307, `http://${t}`);
+ if (t.includes("lightning=")) {
+ const url = new URL(t);
+ const params = new URLSearchParams(url.search);
+ t = params.get("lightning");
+ }
+
if (t.startsWith("lightning:")) t = t.replace("lightning:", "");
if (t.endsWith(`@${PUBLIC_DOMAIN}`)) t = t.split("@")[0];
if (t.includes("@") && t.includes(".")) {
@@ -51,16 +58,10 @@ export default async (s, host) => {
if (t.toLowerCase().startsWith("lnurl")) redirect(307, `/ln/${t}`);
if (t.includes(":")) t = t.split(":")[1];
- if (t.includes("lightning=")) {
- const url = new URL(t);
- const params = new URLSearchParams(url.search);
- t = params.get("lightning");
- }
-
if (t.toLowerCase().startsWith("ln")) {
try {
- const inv = await get(`/invoice/${t}`);
- if (inv.user.username === "mint") throw new Error("mint payment");
+ invoice = await get(`/invoice/${t}`);
+ if (invoice.user.username === "mint") throw new Error("mint payment");
} catch (e) {
if (t.toLowerCase().startsWith("lno")) {
const { offer_amount_msat: a } = await get(`/decode/${t}`);
@@ -70,6 +71,8 @@ export default async (s, host) => {
}
}
+ if (invoice) redirect(307, `/invoice/${invoice.id}`);
+
if (t.match(/^🥜([\uFE00-\uFE0F]|[\uE0100-\uE01EF]+)$/)) {
t = Array.from(t)
.slice(1)
@@ -115,11 +118,9 @@ export default async (s, host) => {
if (fund) redirect(307, `/send/fund/${t}`);
- // invoice
- let invoice;
try {
- invoice = await get(`/invoice/${t}`);
+ invoice ||= await get(`/invoice/${t}`);
} catch (e) {}
- if (invoice) redirect(307, `/send/invoice/${invoice.id}`);
+ if (invoice) redirect(307, `/invoice/${invoice.id}`);
};
diff --git a/src/lib/random.ts b/src/lib/random.ts
new file mode 100644
index 00000000..36dde0cc
--- /dev/null
+++ b/src/lib/random.ts
@@ -0,0 +1,20 @@
+import {
+ NumberDictionary,
+ animals,
+ colors,
+ uniqueNamesGenerator,
+} from "unique-names-generator";
+
+export const randomName = () =>
+ uniqueNamesGenerator({
+ dictionaries: [animals, NumberDictionary.generate({ min: 10, max: 99 })],
+ length: 2,
+ separator: "",
+ });
+
+export const randomPassword = () =>
+ uniqueNamesGenerator({
+ dictionaries: [colors, NumberDictionary.generate({ min: 100, max: 999 })],
+ length: 2,
+ separator: "",
+ });
diff --git a/src/lib/settings.ts b/src/lib/settings.ts
index 53a25e18..6f4d3f62 100644
--- a/src/lib/settings.ts
+++ b/src/lib/settings.ts
@@ -8,7 +8,7 @@ type Error = {
export default async ({ cookies, request }) => {
const form = await fd(request);
- if (form.tab === "preferences") {
+ if (form.tab === "account") {
form.notify = form.notify === "on";
form.push = form.push === "on";
form.nip5 = form.nip5 === "on";
diff --git a/src/lib/socket.ts b/src/lib/socket.ts
index 2bb0d4f2..b87cc165 100644
--- a/src/lib/socket.ts
+++ b/src/lib/socket.ts
@@ -57,8 +57,7 @@ export const messages = (data) => ({
pathname.includes("/receive") ||
pathname.includes("/invoice")
) {
- await wait(() => !get(navigating));
- await goto(`/invoice/${iid}`);
+ if (!get(navigating)) goto(`/invoice/${iid}`);
} else success(`${confirmed ? "Received" : "Detected"} ⚡️${s(amount)}!`);
}
},
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 2e1bb840..28a219e1 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -3,6 +3,7 @@ import { goto } from "$app/navigation";
import { page } from "$app/stores";
import { PUBLIC_COINOS_NETWORK, PUBLIC_COINOS_URL } from "$env/static/public";
import { bytesToHex } from "@noble/hashes/utils";
+import { fail as svelteFail, redirect } from "@sveltejs/kit";
import { toast } from "@zerodevx/svelte-toast";
import {
differenceInDays,
@@ -456,3 +457,34 @@ export const ago = (t) => {
const days = differenceInDays(now, date);
return `${days}d`;
};
+
+export const register = async (user, ip, cookies, loginRedirect) => {
+ let error;
+
+ 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 svelteFail(400, { error });
+ redirect(303, loginRedirect || `/${user.username}`);
+};
diff --git a/src/locales/ar.json b/src/locales/ar.json
index e776ada0..8b1e7ddc 100644
--- a/src/locales/ar.json
+++ b/src/locales/ar.json
@@ -368,7 +368,19 @@
"publishNip5": "هوية نبتة",
"nip5a": "الاستخدام",
"nip5b": "\"عنوان \"نوستر",
- "name": "الاسم"
+ "name": "الاسم",
+ "NOSTR": "NOSTR",
+ "tip": "النص الأصلي",
+ "tipDesc": "النسبة المئوية للمستخدمين الموثوقين الذين يدفعون بالسيارة",
+ "connectSquare": "Connect to Square",
+ "revokeSquare": "Revoke Squanection",
+ "confirmAutoWithdrawal": "التأكد من عمليات سحب السيارات",
+ "confirmAutoWithdrawalDesc": "سحبك للسيارات تم تعطيله يرجى التحقق من عنوان الوجهة الخاصة بك قبل إعادة فتحها أو إزالة العنوان لرفض هذه الرسالة.",
+ "newPasswordDesc": "(يَتْركُ فارغاً للإبْقاء بدون تغيير)",
+ "yourUsername": "إسمك المستخدم",
+ "yourPassword": "وكلمة السر الخاصة بك",
+ "welcome": "مرحباً!",
+ "clickBelow": "انقروا من هنا لينهيوا حسابكم"
},
"support": {
"header": "الدعم",
@@ -380,7 +392,8 @@
"doOurBest": "وسنبذل قصارى جهدنا للعودة إليك في الوقت المناسب.",
"thankYou": "شكراً لك!",
"beInTouch": "شخص ما سيكون على اتصال قريبا."
- }
+ },
+ "lnurl": "رمز المدفوعات المستحقة"
},
"payments": {
"ok": "حسناً",
@@ -490,7 +503,16 @@
"advancedSettings": "البيئات المتقدمة",
"exchangeRate": "سعر الصرف",
"pending": "معلقة",
- "minted": "Minted"
+ "minted": "Minted",
+ "insufficientFunds": "عدم كفاية الأموال",
+ "lockedBalance": "الرصيد الجاهز",
+ "sweep": "وصلة Sweep",
+ "trustUser": "إرسال وإتاحة دفع رسوم السيارات",
+ "custom": "العرف",
+ "gift": "جميع الأموال",
+ "giftLink": "وصلة الهدايا",
+ "giftDesc1": "من يفتح الرابط أدناه سيتلقى جميع الأموال المتاحة.",
+ "giftDesc2": "وإذا تم تسجيلها، فإن الأموال ستدخل إلى حسابها، وإلا سينشأ حساب جديد."
},
"items": {
"add": "البند",
@@ -503,5 +525,14 @@
"notes": {
"contents": "المحتويات",
"notFound": "No notes found"
+ },
+ "incident": "والأموال المودعة قبل 10 أيار/مايو مقفلة في انتظار استردادها من حادث فقدان بيانات. إتبعنا على (نوستر) للتحديثات.",
+ "funds": {
+ "anyone": "أي شخص لديه الرابط",
+ "addManager": "المدير",
+ "manage": "Manage",
+ "withdrawAccess": "الدخول السريع",
+ "username": "المستعمل",
+ "returnToFund": "العودة إلى التمويل"
}
}
diff --git a/src/locales/bn.json b/src/locales/bn.json
index 17c367c1..cb262e64 100644
--- a/src/locales/bn.json
+++ b/src/locales/bn.json
@@ -368,7 +368,19 @@
"publishNip5": "কোনো পরিচিতি নেই",
"nip5a": "ব্যবহার",
"nip5b": "আপনার পাবলিক ঠিকানা যাচাইকারী হিসেবে",
- "name": "নাম"
+ "name": "নাম",
+ "NOSTR": "পার্সেল",
+ "tip": "ডিফল্ট",
+ "tipDesc": "স্বয়ংক্রিয়ভাবে সম্পূর্ণ করার সময় বিশ্বস্ত ব্যক্তিদের তালিকা প্রদর্শন করা হবে",
+ "connectSquare": "বর্গক্ষেত্রে সংযোগ করুন",
+ "revokeSquare": "সংযোগ প্রত্যাখ্যান করুন",
+ "confirmAutoWithdrawal": "পূর্ববর্তী বৈশিষ্ট্য নিশ্চিত করুন",
+ "confirmAutoWithdrawalDesc": "আপনার ব্যাক-আপ প্রত্যাহার নিষ্ক্রিয় করা হয়েছে । অনুগ্রহ করে এই বার্তাটি পুনরায় সক্রিয় করুন অথবা সেটিকে সঠিক ঠিকানা রূপে ধার্য করুন।.",
+ "newPasswordDesc": "(অঅপরিবর্তিত রাখা যাবে না)",
+ "yourUsername": "আপনার ব্যবহারকারী নাম",
+ "yourPassword": "আপনার পাসওয়ার্ড হল %s",
+ "welcome": "স্বাগতম!",
+ "clickBelow": "আপনার অ্যাকাউন্ট সম্পন্ন করার জন্য নীচের ক্লিক করুন"
},
"support": {
"header": "সহায়তা",
@@ -380,7 +392,8 @@
"doOurBest": "এবং সময়মতো আপনার কাছে ফিরে পেতে আমরা আমাদের যথাসাধ্য চেষ্টা করব।.",
"thankYou": "ধন্যবাদ!",
"beInTouch": "কেউ শীঘ্রই যোগাযোগ করবে।."
- }
+ },
+ "lnurl": "পুনরায় চার্জের যোগ্য কোড"
},
"payments": {
"ok": "ঠিক আছে",
@@ -490,7 +503,16 @@
"advancedSettings": "উন্নত সেটিংসমূহ",
"exchangeRate": "Exchangeের মাত্রা",
"pending": "অসমাপ্ত",
- "minted": "মিনট"
+ "minted": "মিনট",
+ "insufficientFunds": "অপর্যাপ্ত অর্থ",
+ "lockedBalance": "লক করা হয়েছে",
+ "sweep": "সুইপ লিংক",
+ "trustUser": "স্বয়ংক্রিয়ভাবে ফেরত পাঠান ও সক্রিয় করুন",
+ "custom": "স্বনির্ধারিত",
+ "gift": "উপহার দেওয়া",
+ "giftLink": "উপহার লিংক",
+ "giftDesc1": "যে কেউ এই লিঙ্কগুলো খোলে সে সব অর্থ পাবে।.",
+ "giftDesc2": "যদি তারা লগ-ইন করে, তাহলে এই অর্থ তাদের অ্যাকাউন্টে চলে যাবে, অন্যথায় নতুন অ্যাকাউন্ট তৈরি করা হবে।."
},
"items": {
"add": "বস্তু যোগ করুন",
@@ -503,5 +525,14 @@
"notes": {
"contents": "নোটের বিষয়বস্তু",
"notFound": "সহায়ক তথ্য পাওয়া যায়নি"
+ },
+ "incident": "১০ মে তারিখের আগে তহবিল জমা করা হয়েছে। আপডেটের জন্য আমাদের সাথে আসুন।.",
+ "funds": {
+ "anyone": "লিংক সহ যে কেউ এই লিংক",
+ "addManager": "ম্যানেজার যোগ করো",
+ "manage": "ব্যবস্থাপনা",
+ "withdrawAccess": "ব্যবহারের অধিকার",
+ "username": "ব্যবহারকারীর নাম",
+ "returnToFund": "অর্থ পাঠানো"
}
}
diff --git a/src/locales/de.json b/src/locales/de.json
index 8703dd19..03145427 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -318,7 +318,19 @@
"nostr": "Nostr",
"nwc": "Nostr Brieftasche verbinden",
"nwcDescription": "Aktivieren Sie Ihre nostr App, um Zahlungen von Ihrem Konto zu senden",
- "name": "Bezeichnung"
+ "name": "Bezeichnung",
+ "NOSTR": "NOSTR",
+ "tip": "Standardtipp",
+ "tipDesc": "Standard-Tip-Prozentsatz bei automatischer Bezahlung vertrauenswürdiger Benutzer",
+ "connectSquare": "Verbindung zum Quadrat",
+ "revokeSquare": "Wiederherstellung Quadratische Verbindung",
+ "confirmAutoWithdrawal": "Auto-Entzugseinstellungen bestätigen",
+ "confirmAutoWithdrawalDesc": "Ihre Autoentnahmen wurden deaktiviert. Bitte überprüfen Sie Ihre Zieladresse, bevor Sie sie erneut aktivieren oder entfernen Sie die Adresse, um diese Nachricht zu entlassen.",
+ "newPasswordDesc": "(Leave blank to keep unverändert)",
+ "yourUsername": "Ihr Benutzername ist",
+ "yourPassword": "und ihr passwort ist",
+ "welcome": "Willkommen!",
+ "clickBelow": "Klicken Sie unten, um die Einrichtung Ihres Kontos abzuschließen"
},
"support": {
"header": "Support",
@@ -349,7 +361,8 @@
"moreDetails": "Links und Details",
"url": "Öffentliche URL",
"nwc": "Nostr-Wallet verbinden",
- "loadMore": "Mehr laden"
+ "loadMore": "Mehr laden",
+ "lnurl": "Wiederverwendbarer Zahlungscode"
},
"payments": {
"header": "Transaktionen",
@@ -464,7 +477,16 @@
"advancedSettings": "Erweiterte Einstellungen",
"exchangeRate": "Wechselkurs",
"pending": "Andauernd",
- "minted": "Minted"
+ "minted": "Minted",
+ "insufficientFunds": "Unzureichende Mittel",
+ "lockedBalance": "Gesperrter Saldo",
+ "sweep": "Sweep Link",
+ "trustUser": "Senden und aktivieren Auto-Pay",
+ "custom": "Benutzerdefiniert",
+ "gift": "Alle Fonds einschenken",
+ "giftLink": "Giftlink",
+ "giftDesc1": "Wer den Link unten öffnet, erhält alle verfügbaren Mittel.",
+ "giftDesc2": "Wenn sie eingeloggt sind, gehen die Fonds auf ihr Konto, ansonsten wird ein neues Konto generiert."
},
"items": {
"add": "Artikel hinzufügen",
@@ -532,5 +554,14 @@
"notes": {
"contents": "Inhalt",
"notFound": "Keine Notizen gefunden"
+ },
+ "incident": "Die vor dem 10. Mai hinterlegten Mittel sind bis zur Wiederherstellung von Datenverlusten gesperrt. Folgen Sie uns auf Nostr für Updates.",
+ "funds": {
+ "anyone": "Jeder mit dem Link",
+ "addManager": "Manager hinzufügen",
+ "manage": "Verwaltung",
+ "withdrawAccess": "Zugriff aufheben",
+ "username": "Benutzername",
+ "returnToFund": "Zurück zum Fonds"
}
}
diff --git a/src/locales/el.json b/src/locales/el.json
index 05414cb4..8cc3637e 100644
--- a/src/locales/el.json
+++ b/src/locales/el.json
@@ -319,7 +319,19 @@
"nostr": "Νοστρ",
"nwc": "Σύνδεση πορτοφολιού Nostr",
"nwcDescription": "Ενεργοποιήστε την εφαρμογή nostr για να στείλετε πληρωμές από το λογαριασμό σας",
- "name": "Όνομα"
+ "name": "Όνομα",
+ "NOSTR": "ΝΟΣΤTR",
+ "tip": "Προκαθορισμένη άκρη",
+ "tipDesc": "Προκαθορισμένο ποσοστό πληροφοριών κατά την αυτόματη πληρωμή εμπίστων χρηστών",
+ "connectSquare": "Σύνδεση στην πλατεία",
+ "revokeSquare": "Recoke τετραγωνική σύνδεση",
+ "confirmAutoWithdrawal": "Επιβεβαίωση ρυθμίσεων αυτόματης απόσυρσης",
+ "confirmAutoWithdrawalDesc": "Οι αυτόματες αναλήψεις σας έχουν απενεργοποιηθεί. Παρακαλώ ελέγξτε εκ νέου τη διεύθυνση προορισμού σας πριν τις ξαναεγγράψετε ή αφαιρέσετε τη διεύθυνση για να απορρίψετε αυτό το μήνυμα.",
+ "newPasswordDesc": "(Άφησε το κενό για να παραμείνει αμετάβλητο)",
+ "yourUsername": "Το όνομα χρήστη είναι",
+ "yourPassword": "και ο κωδικός σας είναι",
+ "welcome": "Καλώς ήρθατε!",
+ "clickBelow": "Κάντε κλικ παρακάτω για να ολοκληρώσετε τη ρύθμιση του λογαριασμού σας"
},
"support": {
"header": "Υποστήριξη",
@@ -343,7 +355,8 @@
"moreDetails": "Σύνδεσμοι και λεπτομέρειες",
"url": "Δημόσια διεύθυνση URL",
"nwc": "Σύνδεση πορτοφολιού Nostr",
- "loadMore": "Φόρτωση περισσότερων"
+ "loadMore": "Φόρτωση περισσότερων",
+ "lnurl": "Επαναχρησιμοποιήσιμος κωδικός πληρωμής"
},
"payments": {
"amount": "Ποσό",
@@ -455,7 +468,16 @@
"advancedSettings": "Προχωρημένες ρυθμίσεις",
"exchangeRate": "Συναλλαγματική ισοτιμία",
"pending": "Σε αναμονή",
- "minted": "Λεπτό"
+ "minted": "Λεπτό",
+ "insufficientFunds": "Ανεπαρκή κεφάλαια",
+ "lockedBalance": "Κλειδωμένο υπόλοιπο",
+ "sweep": "Συνδέσεις Sweep",
+ "trustUser": "Αποστολή και ενεργοποίηση αυτόματης πληρωμής",
+ "custom": "Προσαρμοσμένο",
+ "gift": "Δώρο όλα τα κεφάλαια",
+ "giftLink": "Δεσμός δώρων",
+ "giftDesc1": "Όποιος ανοίξει τον παρακάτω σύνδεσμο θα λάβει όλα τα διαθέσιμα κεφάλαια.",
+ "giftDesc2": "Αν είναι συνδεδεμένοι, τα κεφάλαια θα πάνε στο λογαριασμό τους, διαφορετικά θα δημιουργηθεί ένας νέος λογαριασμός."
},
"items": {
"add": "Πρόσθεσε είδος",
@@ -523,5 +545,14 @@
"notes": {
"contents": "Περιεχόμενα σημείωσης",
"notFound": "Δε βρέθηκαν σημειώσεις"
+ },
+ "incident": "Τα κεφάλαια που κατατέθηκαν πριν τις 10 Μαΐου κλειδώνονται εν αναμονή της ανάκτησης από ένα περιστατικό απώλειας δεδομένων. Ακολουθήστε μας στο Nostr για ενημερώσεις.",
+ "funds": {
+ "anyone": "Οποιοσδήποτε με το σύνδεσμο",
+ "addManager": "Προσθήκη διαχειριστή",
+ "manage": "Διαχείριση",
+ "withdrawAccess": "Ανάσυρση πρόσβασης",
+ "username": "Όνομα χρήστη",
+ "returnToFund": "Επιστροφή στο κεφάλαιο"
}
}
diff --git a/src/locales/en.json b/src/locales/en.json
index 347bb129..dd6e29f1 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1,11 +1,12 @@
{
+ "incident": "NOTICE: Funds deposited before May 10 are temporarily locked while we attempt to recover from a data loss incident. We had to restore our database from a day-old backup so we need to ensure that balances and payment history are corrected to account for all activity that occurred during that period before we can release funds. We don't have an ETA at this time but we are working diligently to resolve the issue. We are sincerely sorry for the inconvenience.",
"funds": {
"anyone": "Anyone with the link",
"addManager": "Add manager",
"manage": "Manage",
"withdrawAccess": "Withdraw access",
"username": "Username",
- "returnToFund": "Back to fund"
+ "returnToFund": "Back to fund"
},
"notes": {
"contents": "Note contents",
@@ -52,7 +53,7 @@
"revealMnemonic": "Reveal seed phrase",
"regenerate": "Regenerate",
"deleteAccount": "Delete account",
- "addAccount": "Add account",
+ "addAccount": "Create a non-custodial account",
"create": "Create",
"import": "Import",
"accountSetup": "Account setup",
@@ -101,23 +102,19 @@
"header": "FAQ",
"cost": {
"question": "What does it cost?",
- "answer": "Receiving payments into your account is free. Withdrawals to external wallets incur a small fee that varies depending on network conditions."
+ "answer": "Receiving payments is free. Sending to other Coinos accounts is free. Withdrawals to external wallets are free if you use the same network you received on, otherwise there's a 0.4% fee for Bitcoin or 0.1% for Lightning or Liquid."
},
"compatibility": {
"question": "Do I need any special device or software?",
- "answer": "No, Coinos works anywhere that has Internet. Merchants might use it on a tablet or put up a QR code for customers to scan with their phone."
- },
- "safety": {
- "question": "When do my funds settle?",
- "answer": "Lightning payments are settled instantly. You can withdraw your balance to another wallet within seconds. On-chain deposits take 5-10 minutes to confirm on average."
+ "answer": "Coinos works on any device that has Internet and a web browser. You can receive payment notifications within the app, via email or push notification. We do have PoS terminals and receipt printers available for those who prefer physical notifications."
},
"pos": {
"question": "Can Coinos integrate with my existing PoS?",
- "answer": "We recommend entering sales into your existing PoS as cash transactions."
+ "answer": "We can log sales into Square automatically. On other systems you might choose to record Bitcoin sales under \"cash\"."
},
"bitcoin": {
"question": "What can I do with Bitcoin?",
- "answer": "Spend it at other merchants or pay your expenses with it by getting your staff, suppliers and landlord to take it."
+ "answer": "Spend it at other merchants, convince your staff, landlord or suppliers to let you pay your expenses with it, or hold it as an investment."
}
},
"footer": {
@@ -144,14 +141,14 @@
},
"step3": {
"header": "3. That's all!",
- "description": "Spend your coin immediately or watch it grow in value."
+ "description": "Easy, right? Tell us when you're ready for us to promote your business to the Bitcoin community!"
}
},
"invoice": {
"addTip": "Add a tip",
"addTipq": "Add a tip?",
"addMemo": "Add memo",
- "setMemo": "Set memo",
+ "setMemo": "Add a note",
"memo": "Memo",
"done": "Done",
"paymentSuccessful": "Payment received!",
@@ -239,6 +236,7 @@
"user": {
"loadMore": "Load more",
"url": "Public URL",
+ "lnurl": "Payment code",
"install": "Install app",
"moreDetails": "Profile details",
"noMessages": "You have not sent or received any messages",
@@ -294,6 +292,13 @@
"placeholder": "Enter an address, invoice or username"
},
"settings": {
+ "connectSquare": "Connect to Square",
+ "revokeSquare": "Revoke Square Connection",
+ "confirmAutoWithdrawal": "Confirm auto withdrawal settings",
+ "confirmAutoWithdrawalDesc": "Your auto withdrawals have been disabled. Please double check your destination address before re-enabling them or remove the address to dismiss this message.",
+ "tip": "Default tip",
+ "tipDesc": "Default tip percentage when auto-paying trusted users",
+ "nostr": "Nostr",
"name": "Name",
"integrations": "Integrations",
"syncWithExtension": "Sync with extension",
@@ -303,16 +308,15 @@
"memoPromptPlaceholder": "e.g. Please enter your name for the order",
"rememberFor": "Remember for",
"import": "Import secret key",
- "destination": "Autowithdrawal address",
+ "destination": "Auto withdrawal address",
"destinationDesc": "The address that withdrawals will be sent to",
"destinationPlaceholder": "Enter a Bitcoin, Liquid or Lightning address",
"reserve": "Reserve amount",
"reserveDesc": "The amount to be left in your account after the withdrawal is complete",
- "nostr": "Nostr",
"shopify": "Shopify",
- "threshold": "Autowithdrawal threshold",
+ "threshold": "Auto withdrawal threshold",
"thresholdDesc": "When your balance exceeds this amount, a withdrawal will be triggered",
- "autoWithdraw": "Autowithdrawal",
+ "autoWithdraw": "Auto withdrawal",
"autoWithdrawDescription": "Enable withdrawals to an external address when your balance reaches a given threshold",
"notifications": "Email notifications",
"pushNotifications": "Push notifications",
@@ -325,7 +329,7 @@
"nwc": "Nostr wallet connect (NWC)",
"nwcDescription": "Enable nostr apps to send payments from your account",
"seedDescription": "This 12 word seed phrase is used to derive your nostr private key and your self-custodial wallet keys which determine your auto-withdrawal addresses. Back it up and don't let anyone see it.",
- "emailDescription": "For password resets and payment notifications only",
+ "emailDescription": "For password resets and payment notifications",
"hideNsec": "Hide nostr secret key",
"saved": "Settings saved!",
"verifying": "Verification email sent",
@@ -333,19 +337,26 @@
"revealNsec": "Reveal secret key",
"hideSeed": "Hide seed phrase",
"revealSeed": "Reveal seed phrase",
- "saveSettings": "Save Settings",
+ "saveSettings": "Save settings",
"bannerImage": "Banner Image",
"profileImage": "Profile Image",
"oneTimeCode": "Enter one-time code",
"accountId": "Account Secret",
"header": "Settings",
"ACCOUNT": "PROFILE",
- "POINT_OF_SALE": "SETTINGS",
+ "POINT_OF_SALE": "ACCOUNT",
+ "NOSTR": "NOSTR",
"SECURITY": "SECURITY",
"locale": "Language",
"username": "Username",
"email": "Email",
- "newPassword": "Change Password",
+ "newPassword": "New Password",
+ "newPasswordDesc": "(Leave blank to keep unchanged)",
+ "yourUsername": "Your username is:",
+ "yourPassword": "Your password is:",
+ "welcome": "Welcome!",
+ "continueSettingUp": "Account setup",
+ "clickBelow": "Click below to finish setting up your account",
"about": "About",
"aboutPlaceholder": "A little something about you or your business address and hours",
"emailNotifications": "Email Notifications",
@@ -395,6 +406,15 @@
}
},
"payments": {
+ "custom": "Custom",
+ "trustUser": "Send and enable auto-pay",
+ "gift": "Gift all funds",
+ "giftLink": "Gift link",
+ "giftDesc1": "Whoever opens the link below will receive all the available funds.",
+ "giftDesc2": "If they are logged in, the funds will go to their account, otherwise a new account will be generated.",
+ "sweep": "Sweep link",
+ "lockedBalance": "Locked balance",
+ "insufficientFunds": "Insufficient funds",
"pending": "Pending",
"minted": "Minted",
"redeemed": "Redeemed",
@@ -403,7 +423,7 @@
"zappedEvent": "Zapped event",
"accountSettings": "Account settings",
"advancedSettings": "Advanced settings",
- "moreOptions": "More options...",
+ "moreOptions": "More options",
"other": "Other",
"ok": "Ok",
"cancel": "Cancel",
@@ -448,13 +468,12 @@
"voucher": "Payment code",
"qr": "QR",
"copy": "Copy",
- "amountPrompt": "Prompt for amount",
+ "amountPrompt": "Always prompt for amount",
"shareLink": "Share",
"copyLink": "Copy link",
"copyText": "Copy text",
"openLink": "Open",
"setAmount": "Set amount",
- "moreOptions": "More options",
"redeemVoucher": "Ecash",
"total": "Total",
"gainLoss": "Gain/Loss",
@@ -498,7 +517,6 @@
"from": "from",
"redeem": "Redeem",
"swap": "Swap",
- "redeemed": "Redeemed",
"redeemedBy": "Redeemed by",
"sendMessage": "Write a message...",
"fundsClaimed": "Funds claimed!",
diff --git a/src/locales/es.json b/src/locales/es.json
index 249491ab..664c7471 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -307,7 +307,19 @@
"nostr": "Nostr",
"nwc": "Cartera de Nostr",
"nwcDescription": "Permite que su aplicación de la nariz envíe pagos de su cuenta",
- "name": "Nombre"
+ "name": "Nombre",
+ "NOSTR": "NOSTR",
+ "tip": "Consejo predeterminado",
+ "tipDesc": "Porcentaje de punta predeterminada cuando los usuarios de confianza pagan auto",
+ "connectSquare": "Conectarse a la Plaza",
+ "revokeSquare": "Revoke Square Connection",
+ "confirmAutoWithdrawal": "Confirme la configuración de retiro automático",
+ "confirmAutoWithdrawalDesc": "Sus retiros de automóviles han sido desactivados. Por favor, compruebe su dirección de destino antes de volver a habilitarlas o retirar la dirección para desestimar este mensaje.",
+ "newPasswordDesc": "(Leave blank to keep unchanged)",
+ "yourUsername": "Tu nombre de usuario es",
+ "yourPassword": "y su contraseña es",
+ "welcome": "¡Bienvenido!",
+ "clickBelow": "Haga clic a continuación para terminar de configurar su cuenta"
},
"support": {
"header": "Soporte",
@@ -326,9 +338,11 @@
"moreDetails": "Enlaces y detalles",
"url": "URL pública",
"nwc": "Conexión de billetera Nostr",
- "loadMore": "Cargar más"
+ "loadMore": "Cargar más",
+ "lnurl": "Código de pago reutilizable"
},
"payments": {
+ "insufficientFunds": "Fondos insuficientes",
"networkFee": "Tarifa de Red",
"platformFee": "Tarifa de Plataforma",
"fastest": "Más rápido",
@@ -437,7 +451,15 @@
"advancedSettings": "Ajustes avanzados",
"exchangeRate": "Tipo de cambio",
"pending": "Pendiente",
- "minted": "Minted"
+ "minted": "Minted",
+ "lockedBalance": "Saldo bloqueado",
+ "sweep": "Sweep link",
+ "trustUser": "Enviar y activar el pago automático",
+ "custom": "Aduanas",
+ "gift": "Donar todos los fondos",
+ "giftLink": "Enlace de regalo",
+ "giftDesc1": "Quien abra el enlace de abajo recibirá todos los fondos disponibles.",
+ "giftDesc2": "Si se registran, los fondos irán a su cuenta, de lo contrario se generará una nueva cuenta."
},
"items": {
"add": "Añadir artículo",
@@ -505,5 +527,14 @@
"notes": {
"contents": "Contenido de la nota",
"notFound": "No hay notas encontradas"
+ },
+ "incident": "Los fondos depositados antes del 10 de mayo están encerrados en espera de la recuperación de un incidente de pérdida de datos. Síganos en Nostr para actualizaciones.",
+ "funds": {
+ "anyone": "Cualquiera con el enlace",
+ "addManager": "Add manager",
+ "manage": "Manage",
+ "withdrawAccess": "Acceso retirado",
+ "username": "Nombre de usuario",
+ "returnToFund": "Regreso al fondo"
}
}
diff --git a/src/locales/fa.json b/src/locales/fa.json
index 5cf31d61..61659e73 100644
--- a/src/locales/fa.json
+++ b/src/locales/fa.json
@@ -307,7 +307,19 @@
"nostr": "Nostr",
"nwc": "کیف پول Nostr اتصال",
"nwcDescription": "اجازه دهید برنامه nostr خود را برای ارسال پرداخت از حساب شما",
- "name": "نام"
+ "name": "نام",
+ "NOSTR": "NOSTR",
+ "tip": "شکست",
+ "tipDesc": "درصد نهایی زمانی که کاربران قابل اعتماد به پول نقد",
+ "connectSquare": "اتصال به میدان",
+ "revokeSquare": "بازی Square Connection",
+ "confirmAutoWithdrawal": "تایید تنظیمات خروج خودکار",
+ "confirmAutoWithdrawalDesc": "ماشین های شما غیرفعال شده اند. لطفاً آدرس مقصد خود را قبل از اینکه دوباره آنها را باز کنید یا آدرس را برای رد این پیام حذف کنید، بررسی کنید.",
+ "newPasswordDesc": "(تغییرمسیر از)",
+ "yourUsername": "نام کاربری شما",
+ "yourPassword": "و رمز عبور شما",
+ "welcome": "خوش آمدید!",
+ "clickBelow": "در زیر کلیک کنید تا حساب خود را تنظیم کنید"
},
"support": {
"header": "حمایت کردن",
@@ -326,7 +338,8 @@
"moreDetails": "لینک ها و جزئیات",
"url": "URL عمومی",
"nwc": "اتصال کیف پول Nostr",
- "loadMore": "بار بیشتر"
+ "loadMore": "بار بیشتر",
+ "lnurl": "کد پرداخت مناسب"
},
"payments": {
"networkFee": "هزینه شبکه",
@@ -437,7 +450,16 @@
"advancedSettings": "تنظیمات پیشرفته",
"exchangeRate": "نرخ تبادل",
"pending": "بازگشت",
- "minted": "Minted"
+ "minted": "Minted",
+ "insufficientFunds": "صندوق های بی کفایت",
+ "lockedBalance": "تعادل قفل شده",
+ "sweep": "پیوند",
+ "trustUser": "ارسال و فعال کردن خودکار پرداخت",
+ "custom": "سفارشی",
+ "gift": "هدیه همه وجوه",
+ "giftLink": "پیوند هدیه",
+ "giftDesc1": "هر کس لینک زیر را باز کند، تمام وجوه موجود را دریافت خواهد کرد.",
+ "giftDesc2": "اگر آنها وارد شوند، وجوه به حساب خود می آیند، در غیر این صورت حساب جدید تولید خواهد شد."
},
"items": {
"add": "این مورد را اضافه کنید",
@@ -505,5 +527,14 @@
"notes": {
"contents": "مطالب توجه",
"notFound": "هیچ یادداشتی پیدا نشد"
+ },
+ "incident": "صندوق های سپرده شده قبل از 10 می در انتظار بازیابی از یک حادثه از دست دادن داده ها هستند. ما را برای به روز رسانی دنبال کنید.",
+ "funds": {
+ "anyone": "هر کسی با لینک",
+ "addManager": "اضافه کردن مدیر",
+ "manage": "مدیریت",
+ "withdrawAccess": "دسترسی با",
+ "username": "Username",
+ "returnToFund": "بازگشت به صندوق"
}
}
diff --git a/src/locales/fr.json b/src/locales/fr.json
index 494c78e5..7ae6896b 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -174,10 +174,10 @@
"accountBalance": "Solde",
"conversionFeeWarning": "Il y a des frais de 1% pour convertir entre bitcoin, Liquid et Lightning.",
"fetchingRate": "recherche du taux de change...",
- "send": "Envoie",
+ "send": "Envoyer",
"withdraw": "Retrait",
"go": "Allez",
- "receive": "Recevoir un Paiement",
+ "receive": "Recevoir",
"deposit": "Déposer",
"voucher": "Code promo",
"checkout": "Encaisser",
@@ -276,7 +276,19 @@
"nostr": "Numéro",
"nwc": "Le portefeuille Nost se connecte",
"nwcDescription": "Activez votre app nost pour envoyer des paiements depuis votre compte",
- "name": "Nom"
+ "name": "Nom",
+ "NOSTR": "NOSTR",
+ "tip": "Conseil par défaut",
+ "tipDesc": "Pourcentage de pointe par défaut lors du paiement automatique d'utilisateurs de confiance",
+ "connectSquare": "Se connecter à Square",
+ "revokeSquare": "Renverser la connexion carrée",
+ "confirmAutoWithdrawal": "Confirmer les paramètres de retrait automatique",
+ "confirmAutoWithdrawalDesc": "Vos retraits automatiques ont été désactivés. Veuillez vérifier à nouveau votre adresse de destination avant de les renflouer ou supprimer l'adresse pour rejeter ce message.",
+ "newPasswordDesc": "(Laisser blanc pour garder inchangé)",
+ "yourUsername": "Votre nom d'utilisateur",
+ "yourPassword": "et votre mot de passe est",
+ "welcome": "Bienvenue !",
+ "clickBelow": "Cliquez ci-dessous pour terminer la configuration de votre compte"
},
"support": {
"header": "Assistance",
@@ -322,7 +334,8 @@
"moreDetails": "Liens et détails",
"url": "URL publique",
"nwc": "Connexion au portefeuille Nostr",
- "loadMore": "Charger plus"
+ "loadMore": "Charger plus",
+ "lnurl": "Code de paiement réutilisable"
},
"news": {
"header": "Nouvelles",
@@ -437,7 +450,16 @@
"advancedSettings": "Paramètres avancés",
"exchangeRate": "Taux de change",
"pending": "En attente",
- "minted": "Mentionné"
+ "minted": "Mentionné",
+ "insufficientFunds": "Insuffisance des fonds",
+ "lockedBalance": "Solde verrouillé",
+ "sweep": "Lien de balayage",
+ "trustUser": "Envoyer et activer auto-payer",
+ "custom": "Personnalisé",
+ "gift": "Donner tous les fonds",
+ "giftLink": "Lien cadeau",
+ "giftDesc1": "Quiconque ouvre le lien ci-dessous recevra tous les fonds disponibles.",
+ "giftDesc2": "S'ils sont connectés, les fonds vont à leur compte, sinon un nouveau compte sera généré."
},
"items": {
"add": "Ajouter un article",
@@ -505,5 +527,14 @@
"notes": {
"contents": "Table des matières",
"notFound": "Aucune note trouvée"
+ },
+ "incident": "Les fonds déposés avant le 10 mai sont verrouillés en attendant la récupération d'un incident de perte de données. Suivez-nous sur Nostra pour les mises à jour.",
+ "funds": {
+ "anyone": "N'importe qui avec le lien",
+ "addManager": "Ajouter un gestionnaire",
+ "manage": "Gérer",
+ "withdrawAccess": "Retrait de l'accès",
+ "username": "Nom d'utilisateur",
+ "returnToFund": "Retour au fonds"
}
}
diff --git a/src/locales/hi.json b/src/locales/hi.json
index 33753f4c..5baa6e3a 100644
--- a/src/locales/hi.json
+++ b/src/locales/hi.json
@@ -368,7 +368,19 @@
"publishNip5": "नोस्ट्रिक पहचान प्रकाशित करें",
"nip5a": "उपयोग",
"nip5b": "अपने सार्वजनिक रूप से सत्यापित nostr पता के रूप में",
- "name": "नाम"
+ "name": "नाम",
+ "NOSTR": "NOSTR",
+ "tip": "डिफ़ॉल्ट टिप",
+ "tipDesc": "डिफ़ॉल्ट टिप प्रतिशत जब ऑटो भुगतान विश्वसनीय उपयोगकर्ताओं",
+ "connectSquare": "स्क्वायर से कनेक्ट करें",
+ "revokeSquare": "स्क्वायर कनेक्शन",
+ "confirmAutoWithdrawal": "ऑटो वापसी सेटिंग्स की पुष्टि करें",
+ "confirmAutoWithdrawalDesc": "आपकी ऑटो निकासी अक्षम हो गई है। कृपया अपने गंतव्य पते को फिर से सक्षम करने से पहले डबल चेक करें या इस संदेश को खारिज करने के लिए पते को हटा दें।.",
+ "newPasswordDesc": "(बदलने के लिए खाली छोड़ दें)",
+ "yourUsername": "आपका उपयोगकर्ता नाम है",
+ "yourPassword": "आपका पासवर्ड है",
+ "welcome": "स्वागत!",
+ "clickBelow": "अपने खाते की स्थापना समाप्त करने के लिए नीचे क्लिक करें"
},
"support": {
"header": "समर्थन",
@@ -380,7 +392,8 @@
"doOurBest": "और हम आपको समय पर ढंग से वापस पाने के लिए अपनी पूरी कोशिश करेंगे।.",
"thankYou": "धन्यवाद!",
"beInTouch": "कुछ लोग शीघ्र ही स्पर्श में रहेंगे।."
- }
+ },
+ "lnurl": "भुगतान कोड"
},
"payments": {
"ok": "ठीक है",
@@ -490,7 +503,16 @@
"advancedSettings": "उन्नत सेटिंग्स",
"exchangeRate": "विनिमय दर",
"pending": "लंबित",
- "minted": "मिंट"
+ "minted": "मिंट",
+ "insufficientFunds": "अपर्याप्त निधि",
+ "lockedBalance": "बंद संतुलन",
+ "sweep": "स्वीप लिंक",
+ "trustUser": "ऑटो-पे",
+ "custom": "कस्टम",
+ "gift": "सभी फंड",
+ "giftLink": "उपहार लिंक",
+ "giftDesc1": "जो भी नीचे दिए गए लिंक को खोलता है उसे सभी उपलब्ध फंड प्राप्त होंगे।.",
+ "giftDesc2": "यदि वे लॉग इन हैं, तो धन उनके खाते में जाएगा, अन्यथा एक नया खाता उत्पन्न किया जाएगा।."
},
"items": {
"add": "आइटम जोड़ें",
@@ -524,5 +546,14 @@
"notes": {
"contents": "नोट सामग्री",
"notFound": "कोई नोट नहीं मिला"
+ },
+ "incident": "मई 10 से पहले जमा की गई निधियों को डेटा हानि घटना से लंबित वसूली को बंद कर दिया गया है। अद्यतन के लिए Nostr पर हमें का पालन करें।.",
+ "funds": {
+ "anyone": "लिंक के साथ कोई भी",
+ "addManager": "प्रबंधक जोड़ें",
+ "manage": "प्रबंधन",
+ "withdrawAccess": "निकासी",
+ "username": "उपयोगकर्ता नाम",
+ "returnToFund": "फंड वापस"
}
}
diff --git a/src/locales/it.json b/src/locales/it.json
index 45bfbf2a..5886c3bd 100644
--- a/src/locales/it.json
+++ b/src/locales/it.json
@@ -368,7 +368,19 @@
"publishNip5": "Pubblica identità Nostr",
"nip5a": "Usa",
"nip5b": "come indirizzo nostr pubblicamente verificato",
- "name": "Nome"
+ "name": "Nome",
+ "NOSTR": "NO",
+ "tip": "Punta predefinita",
+ "tipDesc": "Percentuale di punta predefinito quando gli utenti fidati di pagamento automatico",
+ "connectSquare": "Collegamento a Piazza",
+ "revokeSquare": "Connessione quadrata",
+ "confirmAutoWithdrawal": "Confermare le impostazioni di prelievo automatico",
+ "confirmAutoWithdrawalDesc": "I vostri prelievi auto sono stati disabilitati. Si prega di controllare doppio l'indirizzo di destinazione prima di riabilitare o rimuovere l'indirizzo per annullare questo messaggio.",
+ "newPasswordDesc": "(Lasciare vuoto per mantenere invariato)",
+ "yourUsername": "Il tuo nome utente è",
+ "yourPassword": "e la tua password è",
+ "welcome": "Benvenuto!",
+ "clickBelow": "Clicca qui sotto per completare l'impostazione del tuo account"
},
"support": {
"header": "Supporto",
@@ -380,7 +392,8 @@
"doOurBest": "e faremo del nostro meglio per tornare a te in modo tempestivo.",
"thankYou": "Grazie!",
"beInTouch": "Qualcuno sarà in contatto tra poco."
- }
+ },
+ "lnurl": "Codice di pagamento riutilizzabile"
},
"payments": {
"ok": "Ok",
@@ -490,7 +503,16 @@
"advancedSettings": "Impostazioni avanzate",
"exchangeRate": "Tasso di cambio",
"pending": "Finanziamenti",
- "minted": "Minted"
+ "minted": "Minted",
+ "insufficientFunds": "Fondi insufficienti",
+ "lockedBalance": "Bilancia chiusa",
+ "sweep": "Collegamento di nuoto",
+ "trustUser": "Inviare e abilitare il pagamento automatico",
+ "custom": "Personale",
+ "gift": "Regalare tutti i fondi",
+ "giftLink": "Link regalo",
+ "giftDesc1": "Chi apre il link qui sotto riceverà tutti i fondi disponibili.",
+ "giftDesc2": "Se sono connessi, i fondi andranno al loro account, altrimenti verrà generato un nuovo account."
},
"items": {
"add": "Aggiungi articolo",
@@ -524,5 +546,14 @@
"notes": {
"contents": "Contenuto della nota",
"notFound": "Nessun commento trovato"
+ },
+ "incident": "I fondi depositati prima del 10 maggio sono bloccati in attesa di recupero da un incidente di perdita di dati. Seguici su Nostr per aggiornamenti.",
+ "funds": {
+ "anyone": "Chiunque con il link",
+ "addManager": "Aggiungi manager",
+ "manage": "Gestione",
+ "withdrawAccess": "Ritiro l'accesso",
+ "username": "Nome utente",
+ "returnToFund": "Torna a fondo"
}
}
diff --git a/src/locales/ja.json b/src/locales/ja.json
index 62a9f4a7..16524a10 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -307,7 +307,19 @@
"nostr": "ノストール",
"nwc": "Nostrウォレットは接続します",
"nwcDescription": "あなたのnostrアプリを有効にして、あなたのアカウントから支払いを送信",
- "name": "お名前 (必須)"
+ "name": "お名前 (必須)",
+ "NOSTR": "ログイン",
+ "tip": "デフォルトチップ",
+ "tipDesc": "信頼できるユーザーを自動支払するときのデフォルトのヒントパーセンテージ",
+ "connectSquare": "スクエアに接続する",
+ "revokeSquare": "Revokeスクエア接続",
+ "confirmAutoWithdrawal": "自動出金設定の確認",
+ "confirmAutoWithdrawalDesc": "自動出金が無効になっています。 アドレスを再エナブルするか、このメッセージを却下するアドレスを削除する前に、宛先アドレスをダブルクリックしてください.",
+ "newPasswordDesc": "(変更されないままにブランクを残す)",
+ "yourUsername": "あなたのユーザー名は",
+ "yourPassword": "パスワードは",
+ "welcome": "お問い合わせ!",
+ "clickBelow": "アカウントの設定を完了するには、下記をクリックしてください"
},
"support": {
"header": "サポート",
@@ -326,7 +338,8 @@
"moreDetails": "リンクと詳細",
"url": "公開URL",
"nwc": "Nostrウォレットコネクト",
- "loadMore": "もっと見る"
+ "loadMore": "もっと見る",
+ "lnurl": "再利用可能な決済コード"
},
"payments": {
"networkFee": "ネットワーク料金",
@@ -437,7 +450,16 @@
"advancedSettings": "高度な設定",
"exchangeRate": "為替レート",
"pending": "ペンディング",
- "minted": "ログイン"
+ "minted": "ログイン",
+ "insufficientFunds": "十分な資金",
+ "lockedBalance": "ロックバランス",
+ "sweep": "スイープリンク",
+ "trustUser": "オートペイの送信と有効化",
+ "custom": "カスタム",
+ "gift": "すべての資金をギフト",
+ "giftLink": "ギフトリンク",
+ "giftDesc1": "下記のリンクを開いたら、利用可能なすべての資金を受け取ることになります.",
+ "giftDesc2": "彼らがログインしている場合, 資金は、自分のアカウントに行きます, それ以外の場合は、新しいアカウントが生成されます."
},
"items": {
"add": "アイテムの追加",
@@ -505,5 +527,14 @@
"notes": {
"contents": "コンテンツ",
"notFound": "ノートが見つかりません"
+ },
+ "incident": "5月10日以前に入金された資金は、データ損失事件から保留中の回復をロックされています。 最新情報は、Nostr をご覧ください.",
+ "funds": {
+ "anyone": "リンクのある方",
+ "addManager": "マネージャーの追加",
+ "manage": "マネージング",
+ "withdrawAccess": "アクセスを撤退",
+ "username": "ユーザ名",
+ "returnToFund": "資金を調達する"
}
}
diff --git a/src/locales/ko.json b/src/locales/ko.json
index a77b8631..7e2e25d2 100644
--- a/src/locales/ko.json
+++ b/src/locales/ko.json
@@ -368,7 +368,19 @@
"publishNip5": "Nostr 정체성",
"nip5a": "제품 정보",
"nip5b": "공개적으로 검증된 nostr 주소",
- "name": "이름 *"
+ "name": "이름 *",
+ "NOSTR": "사이트맵",
+ "tip": "기본 팁",
+ "tipDesc": "자동 결제가 가능한 경우 기본 팁 비율",
+ "connectSquare": "Square에 연결",
+ "revokeSquare": "Revoke 광장 연결",
+ "confirmAutoWithdrawal": "자동 인출 설정 확인",
+ "confirmAutoWithdrawalDesc": "자동 인출이 비활성화되었습니다. 해당 이용 후기에 달린 코멘트가 없습니다.",
+ "newPasswordDesc": "(변경되지 않도록 공백을 사용하십시오)",
+ "yourUsername": "사용자 이름",
+ "yourPassword": "비밀번호",
+ "welcome": "제품 정보!",
+ "clickBelow": "계정 설정하기"
},
"support": {
"header": "지원하다",
@@ -380,7 +392,8 @@
"doOurBest": "그리고 우리는 적시에 당신에게 다시 얻을 수있는 최선을 다할 것입니다.",
"thankYou": "감사합니다!",
"beInTouch": "누군가가 곧 연락 할 것입니다."
- }
+ },
+ "lnurl": "Reusable 지불 부호"
},
"payments": {
"ok": "·",
@@ -490,7 +503,16 @@
"advancedSettings": "고급 설정",
"exchangeRate": "환율",
"pending": "관련 기사",
- "minted": "이름 *"
+ "minted": "이름 *",
+ "insufficientFunds": "충분한 자금",
+ "lockedBalance": "Locked 균형",
+ "sweep": "Sweep 링크",
+ "trustUser": "자동 결제 가능",
+ "custom": "제품 정보",
+ "gift": "모든 자금",
+ "giftLink": "선물 링크",
+ "giftDesc1": "아래 링크를 열면 모든 가능한 자금을 받게됩니다.",
+ "giftDesc2": "그들이 로그인한 경우, 자금은 계정으로 갈 것입니다. 그렇지 않으면 새로운 계정이 생성됩니다."
},
"items": {
"add": "더 보기",
@@ -524,5 +546,14 @@
"notes": {
"contents": "공지사항",
"notFound": "발견 된 노트"
+ },
+ "incident": "5월 10일 이전에 입금된 기금은 데이터 손실 사건에서 폐기된 회복입니다. Nostr에서 업데이트를 받으십시오.",
+ "funds": {
+ "anyone": "링크를 가진 누구나",
+ "addManager": "회사 소개",
+ "manage": "계정 관리",
+ "withdrawAccess": "비밀번호",
+ "username": "사용자 이름",
+ "returnToFund": "자금 지원"
}
}
diff --git a/src/locales/nl.json b/src/locales/nl.json
index 2aa3dd01..c8f5d926 100644
--- a/src/locales/nl.json
+++ b/src/locales/nl.json
@@ -368,7 +368,19 @@
"publishNip5": "Nostr-identiteit publiceren",
"nip5a": "Gebruik",
"nip5b": "als jouw publiekelijk geverifieerd nostr adres",
- "name": "Naam"
+ "name": "Naam",
+ "NOSTR": "NOST",
+ "tip": "Standaard tip",
+ "tipDesc": "Standaard tippercentage bij het automatisch betalen van vertrouwde gebruikers",
+ "connectSquare": "Verbinden met vierkant",
+ "revokeSquare": "Vierkante verbinding ongedaan maken",
+ "confirmAutoWithdrawal": "Auto-opnameinstellingen bevestigen",
+ "confirmAutoWithdrawalDesc": "Je auto opnames zijn uitgeschakeld. Controleer uw bestemmingsadres voordat u ze opnieuw opent of verwijder het adres om dit bericht te verwerpen.",
+ "newPasswordDesc": "(Laat leeg om ongewijzigd te blijven)",
+ "yourUsername": "Uw gebruikersnaam is",
+ "yourPassword": "en uw wachtwoord is",
+ "welcome": "Welkom!",
+ "clickBelow": "Klik hieronder om uw account in te stellen"
},
"support": {
"header": "Ondersteuning",
@@ -380,7 +392,8 @@
"doOurBest": "en we zullen ons best doen om tijdig bij je terug te komen",
"thankYou": "Dank je!",
"beInTouch": "Er neemt zo iemand contact op."
- }
+ },
+ "lnurl": "Herbruikbare betalingscode"
},
"payments": {
"ok": "Ok",
@@ -490,7 +503,16 @@
"advancedSettings": "Geavanceerde instellingen",
"exchangeRate": "Wisselkoers",
"pending": "In afwachting",
- "minted": "Mint"
+ "minted": "Mint",
+ "insufficientFunds": "Onvoldoende middelen",
+ "lockedBalance": "Gesloten balans",
+ "sweep": "Veegverbinding",
+ "trustUser": "Automatisch betalen verzenden en inschakelen",
+ "custom": "Aangepast",
+ "gift": "Alle fondsen cadeau",
+ "giftLink": "Kadokoppeling",
+ "giftDesc1": "Wie de onderstaande link opent, ontvangt alle beschikbare fondsen.",
+ "giftDesc2": "Als ze ingelogd zijn, gaan de fondsen naar hun account, anders wordt er een nieuwe account gegenereerd."
},
"items": {
"add": "Item toevoegen",
@@ -524,5 +546,14 @@
"notes": {
"contents": "Inhoud notitie",
"notFound": "Geen notities gevonden"
+ },
+ "incident": "Geld gestort vóór 10 mei zijn vergrendeld in afwachting van herstel van een gegevensverlies incident. Volg ons op Nostr voor updates.",
+ "funds": {
+ "anyone": "Iedereen met de link",
+ "addManager": "Manager toevoegen",
+ "manage": "Beheer",
+ "withdrawAccess": "Toegang intrekken",
+ "username": "Gebruikersnaam",
+ "returnToFund": "Terug naar financiering"
}
}
diff --git a/src/locales/pl.json b/src/locales/pl.json
index cb117725..05215519 100644
--- a/src/locales/pl.json
+++ b/src/locales/pl.json
@@ -368,7 +368,19 @@
"publishNip5": "Publikuj tożsamość nostr",
"nip5a": "Stosowanie",
"nip5b": "jako publicznie zweryfikowany adres nostr",
- "name": "Nazwa"
+ "name": "Nazwa",
+ "NOSTR": "NOSTR",
+ "tip": "Domyślny napiwek",
+ "tipDesc": "Domyślny procent napiwku przy autopłatnych zaufanych użytkowników",
+ "connectSquare": "Połącz z kwadratem",
+ "revokeSquare": "Połączenie kwadratowe Revoke",
+ "confirmAutoWithdrawal": "Potwierdź ustawienia automatycznego wycofania",
+ "confirmAutoWithdrawalDesc": "Wypłaty samochodów zostały wyłączone. Proszę sprawdzić adres docelowy przed ponownym włączeniem lub usunięciem adresu do odrzucenia tej wiadomości.",
+ "newPasswordDesc": "(Pozostawić puste, aby pozostać niezmienione)",
+ "yourUsername": "Twoja nazwa użytkownika jest",
+ "yourPassword": "twoje hasło jest",
+ "welcome": "Witamy!",
+ "clickBelow": "Kliknij poniżej, aby skonfigurować konto"
},
"support": {
"header": "Wsparcie",
@@ -380,7 +392,8 @@
"doOurBest": "i zrobimy co w naszej mocy, by wrócić do ciebie w odpowiednim czasie.",
"thankYou": "Dziękuję!",
"beInTouch": "Ktoś wkrótce będzie w kontakcie."
- }
+ },
+ "lnurl": "Kod płatności wielokrotnego użytku"
},
"payments": {
"ok": "Ok",
@@ -490,7 +503,16 @@
"advancedSettings": "Ustawienia zaawansowane",
"exchangeRate": "Kurs wymiany",
"pending": "Oczekiwanie",
- "minted": "Puste"
+ "minted": "Puste",
+ "insufficientFunds": "Niewystarczające fundusze",
+ "lockedBalance": "Saldo zablokowane",
+ "sweep": "Połączenie zamiatające",
+ "trustUser": "Wysyłanie i włączanie auto- pay",
+ "custom": "Własny",
+ "gift": "Dawać wszystkie fundusze",
+ "giftLink": "Link prezent",
+ "giftDesc1": "Ktokolwiek otworzy poniższy link otrzyma wszystkie dostępne fundusze.",
+ "giftDesc2": "Jeśli są one zalogowane, fundusze trafią na ich konto, w przeciwnym razie powstanie nowe konto."
},
"items": {
"add": "Dodaj element",
@@ -524,5 +546,14 @@
"notes": {
"contents": "Zawartość notatki",
"notFound": "Nie znaleziono notatek"
+ },
+ "incident": "Fundusze zdeponowane przed 10 maja są zablokowane do czasu odzyskania z incydentu utraty danych. Podążaj za nami na Nostr po aktualizacje.",
+ "funds": {
+ "anyone": "Każdy z linkiem",
+ "addManager": "Dodaj menedżera",
+ "manage": "Zarządzanie",
+ "withdrawAccess": "Wycofanie dostępu",
+ "username": "Nazwa użytkownika",
+ "returnToFund": "Powrót do funduszu"
}
}
diff --git a/src/locales/pt.json b/src/locales/pt.json
index 4d5a944c..907498e7 100644
--- a/src/locales/pt.json
+++ b/src/locales/pt.json
@@ -326,7 +326,19 @@
"nostr": "Não",
"nwc": "Nostr carteira conectar",
"nwcDescription": "Habilite seu aplicativo nostr para enviar pagamentos de sua conta",
- "name": "Nome"
+ "name": "Nome",
+ "NOSTR": "NOSTR",
+ "tip": "Dica padrão",
+ "tipDesc": "Percentagem de dica padrão ao pagar automaticamente usuários confiáveis",
+ "connectSquare": "Ligar ao Quadrado",
+ "revokeSquare": "Revogar a Ligação Quadrada",
+ "confirmAutoWithdrawal": "Confirmar as configurações de retirada automática",
+ "confirmAutoWithdrawalDesc": "As suas retiradas automáticas foram desactivadas. Por favor, verifique o seu endereço de destino antes de voltar a acedê-los ou remova o endereço para descartar esta mensagem.",
+ "newPasswordDesc": "(Deixe em branco para manter inalterada)",
+ "yourUsername": "Seu nome de usuário é",
+ "yourPassword": "e sua senha é",
+ "welcome": "Bem-vindos!",
+ "clickBelow": "Clique abaixo para terminar de configurar sua conta"
},
"support": {
"header": "Suporte",
@@ -347,7 +359,8 @@
"moreDetails": "Links e detalhes",
"url": "URL pública",
"nwc": "Nostr carteira conectar",
- "loadMore": "Carregar mais"
+ "loadMore": "Carregar mais",
+ "lnurl": "Código de pagamento reutilizável"
},
"payments": {
"amount": "Quantidade",
@@ -459,7 +472,16 @@
"advancedSettings": "Definições avançadas",
"exchangeRate": "Taxa de câmbio",
"pending": "Pendente",
- "minted": "Minado"
+ "minted": "Minado",
+ "insufficientFunds": "Fundos insuficientes",
+ "lockedBalance": "Saldo bloqueado",
+ "sweep": "Varrer a ligação",
+ "trustUser": "Enviar e ativar o pagamento automático",
+ "custom": "Personalizado",
+ "gift": "Dar todos os fundos",
+ "giftLink": "Ligação do presente",
+ "giftDesc1": "Quem abrir o link abaixo receberá todos os fundos disponíveis.",
+ "giftDesc2": "Se eles estiverem conectados, os fundos irão para sua conta, caso contrário uma nova conta será gerada."
},
"items": {
"add": "Adicionar item",
@@ -527,5 +549,14 @@
"notes": {
"contents": "Conteúdo de nota",
"notFound": "Nenhuma nota encontrada"
+ },
+ "incident": "Os fundos depositados antes de 10 de maio estão bloqueados até a recuperação de um incidente de perda de dados. Siga-nos no Nostr para atualizações.",
+ "funds": {
+ "anyone": "Qualquer pessoa com o link",
+ "addManager": "Adicionar gerenciador",
+ "manage": "Gerenciar",
+ "withdrawAccess": "Retirar o acesso",
+ "username": "Utilizador",
+ "returnToFund": "Voltar ao fundo"
}
}
diff --git a/src/locales/ru.json b/src/locales/ru.json
index 04c5d642..759d3caf 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -307,7 +307,19 @@
"nostr": "Nostr",
"nwc": "Кошелек Nostr подключается",
"nwcDescription": "Включите приложение Nostr для отправки платежей с вашего счета",
- "name": "Имя"
+ "name": "Имя",
+ "NOSTR": "НОСТР",
+ "tip": "Подсказка по умолчанию",
+ "tipDesc": "Процент отказов при автоплатежах доверенных пользователей",
+ "connectSquare": "Подключение к Square",
+ "revokeSquare": "Отменить Square Connection",
+ "confirmAutoWithdrawal": "Подтвердите настройки автозабора",
+ "confirmAutoWithdrawalDesc": "Ваш отзыв авто был отключен. Пожалуйста, дважды проверьте адрес назначения, прежде чем повторно включить их или удалить адрес, чтобы отклонить это сообщение.",
+ "newPasswordDesc": "(Оставьте пустой, чтобы остаться неизменным)",
+ "yourUsername": "Ваше имя пользователя",
+ "yourPassword": "ваш пароль является",
+ "welcome": "Добро пожаловать!",
+ "clickBelow": "Нажмите ниже, чтобы закончить настройку вашей учетной записи"
},
"support": {
"header": "Поддержка",
@@ -326,7 +338,8 @@
"moreDetails": "Ссылки и подробности",
"url": "Публичный URL-адрес",
"nwc": "Nostr кошелек подключить",
- "loadMore": "Загрузить больше"
+ "loadMore": "Загрузить больше",
+ "lnurl": "Код многоразового платежа"
},
"payments": {
"networkFee": "Сетевая комиссия",
@@ -437,7 +450,16 @@
"advancedSettings": "Расширенные параметры",
"exchangeRate": "Обменный курс",
"pending": "В ожидании",
- "minted": "Отчеканенный"
+ "minted": "Отчеканенный",
+ "insufficientFunds": "Недостаточно средств",
+ "lockedBalance": "Замкнутый баланс",
+ "sweep": "Связь со свистом",
+ "trustUser": "Отправить и включить автоплатеж",
+ "custom": "Обычай",
+ "gift": "Подарите все средства",
+ "giftLink": "Подарочная ссылка",
+ "giftDesc1": "Тот, кто откроет ссылку ниже, получит все доступные средства.",
+ "giftDesc2": "Если они вошли в систему, средства пойдут на их счет, в противном случае будет создан новый счет."
},
"items": {
"add": "Добавить элемент",
@@ -505,5 +527,14 @@
"notes": {
"contents": "Содержание заметок",
"notFound": "Заметок не найдено"
+ },
+ "incident": "Средства, депонированные до 10 мая, блокируются в ожидании восстановления после инцидента с потерей данных. Следуйте за нами на Nostr для обновлений.",
+ "funds": {
+ "anyone": "Любой, у кого есть ссылка",
+ "addManager": "Добавить менеджера",
+ "manage": "Управление",
+ "withdrawAccess": "Отозвать доступ",
+ "username": "Имя пользователя",
+ "returnToFund": "Назад в фонд"
}
}
diff --git a/src/locales/th.json b/src/locales/th.json
index edeb07da..35cedc2c 100644
--- a/src/locales/th.json
+++ b/src/locales/th.json
@@ -368,7 +368,19 @@
"publishNip5": "การแสดงตัวของผู้เผยแพร่",
"nip5a": "ใช้",
"nip5b": "ในฐานะที่อยู่พม่าของคุณ",
- "name": "ชื่อ"
+ "name": "ชื่อ",
+ "NOSTR": "NOSR",
+ "tip": "เคล็ดลับปริยาย",
+ "tipDesc": "เปอร์เซ็นต์ของทิปเมื่อผู้ใช้ที่เชื่อถือได้อัตโนมัติ",
+ "connectSquare": "เชื่อมต่อไปยังสแควร์",
+ "revokeSquare": "การเชื่อมต่อแบบ Reviok Square",
+ "confirmAutoWithdrawal": "ยืนยันการเลิกทําอัตโนมัติ",
+ "confirmAutoWithdrawalDesc": "การถอนเงินอัตโนมัติของคุณถูกยกเลิก กรุณาตรวจสอบที่อยู่ปลายทางของคุณสองครั้ง ก่อนที่จะทําการเปิดใช้งานมันใหม่อีกครั้ง หรือลบที่อยู่เพื่อยกเลิกข้อความนี้.",
+ "newPasswordDesc": "(ปล่อยช่องว่างไว้ไม่ให้เปลี่ยน)",
+ "yourUsername": "ชื่อผู้ใช้ของคุณ",
+ "yourPassword": "และรหัสผ่านของคุณ",
+ "welcome": "ยินดีต้อนรับ!",
+ "clickBelow": "คลิกด้านล่างนี้เพื่อทําบัญชีให้เสร็จ"
},
"support": {
"header": "สนับสนุน",
@@ -380,7 +392,8 @@
"doOurBest": "และเราจะทําอย่างดีที่สุด เพื่อกลับไปหาคุณในเวลาที่เหมาะสม.",
"thankYou": "ขอบคุณ!",
"beInTouch": "เดี๋ยวจะมีคนติดต่อไป."
- }
+ },
+ "lnurl": "รหัสการชําระหนี้ที่ใช้ได้"
},
"payments": {
"ok": "โอเค",
@@ -490,7 +503,16 @@
"advancedSettings": "ตั้งค่าขั้นสูง",
"exchangeRate": "อัตราแลกเปลี่ยน",
"pending": "จํานวนครั้งที่เล่น",
- "minted": "ย่อหาย"
+ "minted": "ย่อหาย",
+ "insufficientFunds": "เงินทุนไม่เพียงพอ",
+ "lockedBalance": "สมดุลที่ล็อค",
+ "sweep": "ล้างลิงก์",
+ "trustUser": "ส่งและเปิดใช้งานการชําระอัตโนมัติ",
+ "custom": "กําหนดเอง",
+ "gift": "นําเสนอกองทุนทั้งหมด",
+ "giftLink": "เชื่อมโยงของพรสวรรค์",
+ "giftDesc1": "ใครก็ตามที่เปิดลิงก์ด้านล่าง จะได้รับเงินทั้งหมดที่มี.",
+ "giftDesc2": "ถ้าพวกเขาถูกล็อกอิน กองทุนก็จะไปที่บัญชีของพวกเขา มิฉะนั้นจะสร้างบัญชีใหม่."
},
"items": {
"add": "เพิ่มรายการ",
@@ -524,5 +546,14 @@
"notes": {
"contents": "ข้อความบันทึกย่อ",
"notFound": "ไม่พบบันทึกย่อ"
+ },
+ "incident": "กองทุนที่ฝากไว้ก่อนที่ 10 พฤษภาคม จะถูกล็อก ระหว่างการฟื้นตัว จากอุบัติเหตุข้อมูลสูญหาย ตามเราเรื่องนอสเตอร์เรื่องความคืบหน้า.",
+ "funds": {
+ "anyone": "ใครก็ตามที่มีการเชื่อมโยง",
+ "addManager": "เพิ่มตัวจัดการ",
+ "manage": "จัดการ",
+ "withdrawAccess": "กําลังดึงข้อมูลการเข้าใช้",
+ "username": "ชื่อผู้ใช้",
+ "returnToFund": "กลับไปยังกองทุน"
}
}
diff --git a/src/locales/tr.json b/src/locales/tr.json
index 1fe730a7..92a165f6 100644
--- a/src/locales/tr.json
+++ b/src/locales/tr.json
@@ -368,7 +368,19 @@
"publishNip5": "Kaslish nostr kimlik",
"nip5a": "Use",
"nip5b": "sizin kamuya açık olarak doğrulanmamış nostr adresi",
- "name": "Name"
+ "name": "Name",
+ "NOSTR": "NOSTR",
+ "tip": "Varsayılan ipucu",
+ "tipDesc": "Otomatik ücretli güvenilen kullanıcıların ortalama yüzdesi",
+ "connectSquare": "Square",
+ "revokeSquare": "Revoke Square Connection",
+ "confirmAutoWithdrawal": "Otomatik geri çekilme ayarlarını onaylayın",
+ "confirmAutoWithdrawalDesc": "Otomatik geri çekilmeleriniz devre dışı bırakıldı. Lütfen hedef adresinizi yeniden etkinleştirin veya bu mesajı reddetme adresi ortadan kaldırın.",
+ "newPasswordDesc": "(Değişmemek için boş tut)",
+ "yourUsername": "Kullanıcınız Kullanıcınızdır",
+ "yourPassword": "ve şifreniziz",
+ "welcome": "Hoşgeldiniz!",
+ "clickBelow": "Hesabınızı kurmak için aşağıda tıklayın"
},
"support": {
"header": "Destek Desteği Destek Desteği",
@@ -380,7 +392,8 @@
"doOurBest": "ve size zamanında geri dönmek için elimizden geleni yapacağız.",
"thankYou": "Teşekkür ederim!",
"beInTouch": "Birisi kısa bir süre içinde dokunacaktır."
- }
+ },
+ "lnurl": "Reusable ödeme kodu"
},
"payments": {
"ok": "Tamam",
@@ -490,7 +503,16 @@
"advancedSettings": "Gelişmiş ayarlar",
"exchangeRate": "Değişim oranı",
"pending": "Pending",
- "minted": "Minted"
+ "minted": "Minted",
+ "insufficientFunds": "Yetersiz fonlarda",
+ "lockedBalance": "Locked bakiye",
+ "sweep": "Sweep link",
+ "trustUser": "Gönder ve otomatik ödeme sağlar",
+ "custom": "Özel",
+ "gift": "Hediye tüm fonlar",
+ "giftLink": "Hediye linki",
+ "giftDesc1": "Aşağıdaki bağlantıyı açan herkes mevcut tüm fonları alacak.",
+ "giftDesc2": "Eğer giriş yapılırsa, fonlar hesabına gidecek, aksi takdirde yeni bir hesap oluşturulacaktır."
},
"items": {
"add": "Ekle item",
@@ -524,5 +546,14 @@
"notes": {
"contents": "Not Content",
"notFound": "Hiçbir not bulunamadı"
+ },
+ "incident": "10 Mayıs'tan önce yapılan fonlar bir veri kaybı olayından kurtarma için kilitlenir. Güncellemeler için Nostr'de bizi takip edin.",
+ "funds": {
+ "anyone": "Bağlantı ile herkes",
+ "addManager": "Ekle yönetici ekle",
+ "manage": "Yönetin Yöneti Yönetimi",
+ "withdrawAccess": "Withdraw access",
+ "username": "Username",
+ "returnToFund": "Geri finanse etmek için"
}
}
diff --git a/src/locales/zh.json b/src/locales/zh.json
index b35284c3..d08ca41d 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -307,7 +307,19 @@
"nostr": "鼻头",
"nwc": "鼻孔钱包连接",
"nwcDescription": "启用您的鼻孔应用程序从您的账户发送付款",
- "name": "名称"
+ "name": "名称",
+ "NOSTR": "无",
+ "tip": "默认提示",
+ "tipDesc": "自动付费信任用户时默认提示百分比",
+ "connectSquare": "连接到平方",
+ "revokeSquare": "撤销正方形连接",
+ "confirmAutoWithdrawal": "确认自动退出设置",
+ "confirmAutoWithdrawalDesc": "您的自动取款被禁用 。 在重新启用之前, 请重复检查您的地址, 或是删除地址以删除此信件 .",
+ "newPasswordDesc": "(空出以不变)",
+ "yourUsername": "您的用户名是",
+ "yourPassword": "而您的密码是",
+ "welcome": "欢迎光临!",
+ "clickBelow": "点击下面完成您的账户设置"
},
"support": {
"header": "支持",
@@ -326,7 +338,8 @@
"moreDetails": "链接和详细信息",
"url": "公共 URL",
"nwc": "Nostr 钱包连接",
- "loadMore": "装入更多"
+ "loadMore": "装入更多",
+ "lnurl": "可重复使用的付款代码"
},
"payments": {
"networkFee": "网络费",
@@ -437,7 +450,16 @@
"advancedSettings": "高级设置",
"exchangeRate": "汇率",
"pending": "待决",
- "minted": "已剪切"
+ "minted": "已剪切",
+ "insufficientFunds": "资金不足",
+ "lockedBalance": "锁定余额",
+ "sweep": "扫描链接",
+ "trustUser": "发送和启用自动支付",
+ "custom": "自定义",
+ "gift": "赠与所有基金",
+ "giftLink": "礼品链接",
+ "giftDesc1": "任何人打开以下链接,都将获得所有可用资金.",
+ "giftDesc2": "如果登录,资金将存入其账户,否则将产生一个新的账户."
},
"items": {
"add": "新增项目",
@@ -505,5 +527,14 @@
"notes": {
"contents": "说明内容",
"notFound": "未找到笔记"
+ },
+ "incident": "5月10日之前存入的资金被锁定,等待从数据损失事件中追回。 跟着我们了解最新消息.",
+ "funds": {
+ "anyone": "有联系的人",
+ "addManager": "添加管理器",
+ "manage": "管理",
+ "withdrawAccess": "撤销访问",
+ "username": "用户名",
+ "returnToFund": "回到资金"
}
}
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>