aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
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/components
parent2abcf6e435fca5d86960f3ba4228002bfb0a19e6 (diff)
parent9277b2921e09ee9dd5f39cf4af30a2330f4d95d1 (diff)
downloadcoinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.tar.gz
coinos-ui-0c69c9848c6b98c92609d209103efc60003c9723.zip
Merge branch 'coinos:master' into master
Diffstat (limited to 'src/components')
-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
13 files changed, 403 insertions, 286 deletions
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();