aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-03-04 23:06:35 -0800
committerAdam Soltys <[email protected]>2026-03-04 23:06:35 -0800
commite5f37b003d93a17c2a9958697269b2d3228d8830 (patch)
treee2c3a3c4d1bec48d323e830f5be35d5832845a9a /src/components
parente6882c6a8987e23215a2538b242431ae603c7101 (diff)
downloadcoinos-ui-e5f37b003d93a17c2a9958697269b2d3228d8830.tar.gz
coinos-ui-e5f37b003d93a17c2a9958697269b2d3228d8830.zip
tuning
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Account.svelte75
-rw-r--r--src/components/AppHeader.svelte12
-rw-r--r--src/components/InvoiceActions.svelte73
-rw-r--r--src/components/InvoiceData.svelte11
-rw-r--r--src/components/WalletPass.svelte100
5 files changed, 97 insertions, 174 deletions
diff --git a/src/components/Account.svelte b/src/components/Account.svelte
index e29e1765..2d4b1e48 100644
--- a/src/components/Account.svelte
+++ b/src/components/Account.svelte
@@ -23,27 +23,27 @@
let arkBalance = $state(0);
let arkLoading = $state(false);
- let tryUnlockArk = async (): Promise<string | false> => {
- const deriveArkKey = async (key: ArrayBuffer): Promise<string> => {
- const [{ HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([
- import("@scure/bip32"),
- import("@scure/bip39"),
- import("@scure/bip39/wordlists/english.js"),
- ]);
- let mnemonicStr: string;
- if (account.seed && isPrfEncrypted(account.seed)) {
- const ent = await prfDecrypt(key, account.seed);
- mnemonicStr = entropyToMnemonic(ent, wordlist);
- } else {
- const entropy = new Uint8Array(key).slice(0, 16);
- mnemonicStr = entropyToMnemonic(entropy, wordlist);
- }
- const s = await mnemonicToSeed(mnemonicStr);
- const master = HDKey.fromMasterSeed(s, versions);
- const arkChild = master.derive("m/86'/0'/0'/0/0");
- return bytesToHex(arkChild.privateKey!);
- };
+ let deriveArkKey = async (key: ArrayBuffer): Promise<string> => {
+ const [{ HDKey }, { entropyToMnemonic, mnemonicToSeed }, { wordlist }] = await Promise.all([
+ import("@scure/bip32"),
+ import("@scure/bip39"),
+ import("@scure/bip39/wordlists/english.js"),
+ ]);
+ let mnemonicStr: string;
+ if (account.seed && isPrfEncrypted(account.seed)) {
+ const ent = await prfDecrypt(key, account.seed);
+ mnemonicStr = entropyToMnemonic(ent, wordlist);
+ } else {
+ const entropy = new Uint8Array(key).slice(0, 16);
+ mnemonicStr = entropyToMnemonic(entropy, wordlist);
+ }
+ const s = await mnemonicToSeed(mnemonicStr);
+ const master = HDKey.fromMasterSeed(s, versions);
+ const arkChild = master.derive("m/86'/0'/0'/0/0");
+ return bytesToHex(arkChild.privateKey!);
+ };
+ let tryUnlockArk = async (): Promise<string | false> => {
// Try cached PRF key or silent nsec
const { getWalletEntropy, deriveNostrEntropy } = await import("$lib/walletEntropy");
const entropy = await getWalletEntropy();
@@ -65,24 +65,38 @@
}
};
- let setAccount = async (event, url) => {
+ let aidParam = $derived(`aid=${id}`);
+ let withAid = (url: string) => url + (url.includes("?") ? "&" : "?") + aidParam;
+
+ let setAccount = async (event, url, requireKey = false) => {
event.preventDefault();
event.stopPropagation();
- document.cookie = `aid=${id}; path=/; max-age=86400`;
if (isArk) {
if ($arkkey && $arkaid === id) {
- await goto(url);
- } else {
+ await goto(withAid(url));
+ } else if (requireKey) {
const key = await tryUnlockArk();
if (key) {
- await goto(url);
+ await goto(withAid(url));
$arkkey = key;
$arkaid = id;
}
+ } else {
+ // Try silent unlock only, navigate regardless
+ const { getWalletEntropy } = await import("$lib/walletEntropy");
+ const entropy = await getWalletEntropy();
+ if (entropy) {
+ try {
+ const key = await deriveArkKey(entropy);
+ $arkkey = key;
+ $arkaid = id;
+ } catch {}
+ }
+ goto(withAid(url));
}
} else {
$arkkey = undefined;
- goto(url);
+ goto(withAid(url));
}
};
@@ -106,8 +120,7 @@
let goSettings = (e) => {
e.preventDefault();
e.stopPropagation();
- document.cookie = `aid=${id}; path=/; max-age=86400`;
- goto(`/account/${id}`);
+ goto(`/account/${id}?${aidParam}`);
};
</script>
@@ -145,7 +158,7 @@
<!-- <PhClockCounterClockwiseBold width="38" /> -->
<!-- </a> -->
<a
- href={`/account/${id}`}
+ href={`/account/${id}?${aidParam}`}
class="hover:opacity-80 relative w-12 h-12 flex items-center justify-center"
aria-label="Account settings"
onclick={goSettings}
@@ -165,14 +178,14 @@
</div>
<div class="flex w-full text-xl gap-2">
- <a href={receiveUrl} class="contents" data-sveltekit-preload-data="tap" onclick={(e) => setAccount(e, receiveUrl)}>
+ <a href={receiveUrl} class="contents" onclick={(e) => e.stopPropagation()}>
<button class="btn !w-auto flex-1" data-testid="account-receive">
<PhHandCoinsBold width="32" style="transform: scaleX(-1)" />
<div class="my-auto">{$t("user.dashboard.receive")}</div>
</button>
</a>
- <a href={`/send`} class="contents" data-sveltekit-preload-data="tap" onclick={(e) => setAccount(e, "/send")}>
+ <a href={withAid("/send")} class="contents" onclick={(e) => setAccount(e, "/send")}>
<button type="button" class="btn !w-auto flex-1">
<PhPaperPlaneRightBold width="32" />
<div class="my-auto">{$t("user.dashboard.send")}</div>
diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte
index a12c656e..0d70bf49 100644
--- a/src/components/AppHeader.svelte
+++ b/src/components/AppHeader.svelte
@@ -4,12 +4,12 @@
import { goto } from "$app/navigation";
import Avatar from "$comp/Avatar.svelte";
import Menu from "$comp/Menu.svelte";
- import { loading, t } from "$lib/translations";
+ import { t } from "$lib/translations";
import { page } from "$app/stores";
let { user, subject = $bindable() } = $props();
- let w: number | undefined = $state();
+ let w: number = $state(globalThis.innerWidth || 1024);
$effect(() => (subject = subject || user));
let bg = $derived(
@@ -69,7 +69,6 @@
<svelte:window bind:innerWidth={w} />
-{#if !$loading}
<header
class="bg-gradient-to-r bg-primary bg-accent h-[175px] w-full relative bg-cover mb-20 !z-30"
style:background-image={bg}
@@ -79,7 +78,7 @@
{#each links as { href, icon: Icon, label }}
<a {href} data-sveltekit-preload-data="tap" aria-label={label}>
<button class="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 {opacity(href)}" aria-label={label}>
- <Icon width={(w ?? 0) > 640 ? 32 : 24} />
+ <Icon width={w > 640 ? 32 : 24} />
</button>
</a>
{/each}
@@ -90,15 +89,14 @@
</a>
{/if}
</nav>
- {#if subject}
+ {#if subject || user}
<div
class="absolute md:w-[64px] md:mx-auto lg:left-[154px] xl:left-[194px] left-[calc(50vw-64px)] -bottom-[64px] z-30"
>
- <Avatar user={subject} />
+ <Avatar user={subject || user} />
</div>
{/if}
</header>
-{/if}
<style>
header {
diff --git a/src/components/InvoiceActions.svelte b/src/components/InvoiceActions.svelte
index 1c34d5ac..65d218fd 100644
--- a/src/components/InvoiceActions.svelte
+++ b/src/components/InvoiceActions.svelte
@@ -24,6 +24,9 @@
} = $props();
let { aid, account, address_type, type } = $derived(invoice);
+ let isOwn = $derived(user?.id === invoice?.user?.id);
+ let isCustodial = $derived(aid === user?.id);
+ let showTypeSwitch = $derived(isCustodial);
</script>
<div class="text-secondary space-y-2 text-xl pt-2">
@@ -50,7 +53,7 @@
</button>
<div class="flex flex-wrap gap-2 justify-around text-secondary w-full">
- {#if user?.id === invoice?.user?.id}
+ {#if isOwn}
<button class="btn grow !w-auto" onclick={toggleAmount}>
<PhPencilSimpleBold width="32" />
<div class="my-auto">{t("payments.setAmount")}</div>
@@ -70,43 +73,43 @@
</button>
</div>
-
-
- <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">
- <PhLightningFill width="24" class="text-yellow-300" />
- </div>
- </div>
- <div class="my-auto text-lg">Lightning</div>
- </button>
-
- {#if !account?.seed && !(account?.pubkey && account?.fingerprint) && account?.type !== "ark"}
+ {#if showTypeSwitch}
+ <div class="flex gap-2">
<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)}
+ class:bg-base-300={type === types.lightning}
+ class:text-secondary={type === types.lightning}
+ onclick={() => {
+ $amountPrompt = false;
+ newAmount = undefined;
+ setType(types.lightning);
+ }}
>
- <img src="/images/bitcoin.svg" class="w-8" alt="Bitcoin" />
- <div class="my-auto text-lg">Bitcoin</div>
+ <div class="bg-black rounded-full w-8 h-8 items-center justify-center flex">
+ <div class="m-auto">
+ <PhLightningFill width="24" class="text-yellow-300" />
+ </div>
+ </div>
+ <div class="my-auto text-lg">Lightning</div>
</button>
- {/if}
- </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" alt="Ecash" />
- <div class="my-auto">{t("payments.moreOptions")}</div>
- </button>
+ {#if !account?.seed && !(account?.pubkey && account?.fingerprint) && account?.type !== "ark"}
+ <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>
+ {/if}
+ </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" alt="Ecash" />
+ <div class="my-auto">{t("payments.moreOptions")}</div>
+ </button>
+ {/if}
</div>
diff --git a/src/components/InvoiceData.svelte b/src/components/InvoiceData.svelte
index ece30457..a613256b 100644
--- a/src/components/InvoiceData.svelte
+++ b/src/components/InvoiceData.svelte
@@ -17,6 +17,7 @@
tip,
rate,
align = "",
+ qrDataUrl,
t,
}: any = $props();
@@ -24,9 +25,17 @@
let { memo } = $derived(invoice);
let qrSrc = $derived(`/qr/${encodeURIComponent(invoice.text)}/raw`);
- let srcs: string[] = $state([]);
+ let srcs: string[] = $state(qrDataUrl ? [qrDataUrl] : []);
+ let lastDataUrl = qrDataUrl;
$effect(() => {
+ // Update if the server-provided data URL changed (e.g. new invoice)
+ if (qrDataUrl && qrDataUrl !== lastDataUrl) {
+ lastDataUrl = qrDataUrl;
+ srcs = [qrDataUrl];
+ return;
+ }
+ if (qrDataUrl && srcs.length) return;
const next = qrSrc;
if (!srcs.length) {
srcs = [next];
diff --git a/src/components/WalletPass.svelte b/src/components/WalletPass.svelte
deleted file mode 100644
index a3f00994..00000000
--- a/src/components/WalletPass.svelte
+++ /dev/null
@@ -1,100 +0,0 @@
-<script lang="ts">
- import PhEyeBold from "virtual:icons/ph/eye-bold";
- import PhEyeSlashBold from "virtual:icons/ph/eye-slash-bold";
- import { preventDefault } from "svelte/legacy";
-
- import { focus } from "$lib/utils";
- import { t } from "$lib/translations";
- import {
- rememberForOptions,
- defaultRememberForMs,
- rememberWalletPassword,
- forgetWalletPassword,
- } from "$lib/passwordCache";
- let { cancel = $bindable(), password = $bindable(), submit } = $props();
- let revealPassword = $state();
- let rememberForMs = $state(defaultRememberForMs);
-
- let handleSubmit = async () => {
- if (rememberForMs) {
- rememberWalletPassword(password, rememberForMs);
- } else {
- forgetWalletPassword();
- }
- await submit();
- };
-</script>
-
-<div class="fixed inset-0 bg-gray-600/50 overflow-y-auto h-full w-full z-20">
- <div class="relative top-1/3 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white space-y-5">
- <h1 class="text-center text-2xl font-semibold">{$t("payments.enterWalletPass")}</h1>
- <form onsubmit={preventDefault(handleSubmit)}>
- <div class="relative mb-5">
- {#if revealPassword}
- <input
- name="password"
- data-testid="walletpass-input"
- type="text"
- required
- bind:value={password}
- autocapitalize="none"
- use:focus
- />
- {:else}
- <input
- name="password"
- data-testid="walletpass-input"
- type="password"
- required
- bind:value={password}
- autocapitalize="none"
- use:focus
- />
- {/if}
- <button
- type="button"
- onclick={() => (revealPassword = !revealPassword)}
- class="absolute right-5 top-1/2"
- aria-label={revealPassword ? "Hide password" : "Show password"}
- >
- {#if revealPassword}
- <PhEyeBold width="32" />
- {:else}
- <PhEyeSlashBold width="32" />
- {/if}
- </button>
- </div>
- <div class="mb-5 space-y-2">
- <label for="rememberFor" class="text-sm text-gray-500">Remember for</label>
- <select
- id="rememberFor"
- class="w-full"
- value={rememberForMs}
- onchange={(e) => (rememberForMs = Number((e.target as HTMLSelectElement).value))}
- >
- {#each rememberForOptions as option}
- <option value={option.ms}>{option.label}</option>
- {/each}
- </select>
- </div>
- <div class="w-full flex justify-center gap-2">
- <button
- type="button"
- data-testid="walletpass-cancel"
- class="border rounded-2xl font-semibold mx-auto py-5 px-6 hover:opacity-80 mx-auto grow"
- onclick={cancel}
- onkeydown={cancel}
- >
- <div class="my-auto">Cancel</div>
- </button>
- <button
- type="submit"
- data-testid="walletpass-submit"
- class="border rounded-2xl font-semibold mx-auto py-5 px-6 hover:opacity-80 mx-auto grow"
- >
- <div class="my-auto">Submit</div>
- </button>
- </div>
- </form>
- </div>
-</div>