aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes')
-rw-r--r--src/routes/(app)/+error.svelte38
-rw-r--r--src/routes/(app)/+layout.server.js2
-rw-r--r--src/routes/(app)/+layout.svelte2
-rw-r--r--src/routes/(app)/[username]/(profile)/+layout.svelte3
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.server.js29
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.svelte229
-rw-r--r--src/routes/(app)/[username]/(profile)/messages/+page.svelte123
-rw-r--r--src/routes/(app)/[username]/(profile)/request/+page.server.js6
-rw-r--r--src/routes/(app)/[username]/dashboard/+page.svelte1
-rw-r--r--src/routes/(app)/[username]/invoice/[hash]/+page.svelte75
-rw-r--r--src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte29
-rw-r--r--src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte57
-rw-r--r--src/routes/(app)/[username]/messages/+page.server.js (renamed from src/routes/(app)/[username]/(profile)/messages/+page.server.js)7
-rw-r--r--src/routes/(app)/[username]/messages/+page.svelte171
-rw-r--r--src/routes/(app)/[username]/payments/[...page]/+page.svelte14
-rw-r--r--src/routes/(app)/[username]/receive/[...param]/+page.svelte5
-rw-r--r--src/routes/(app)/[username]/settings/[...tab]/+page.server.js7
-rw-r--r--src/routes/(app)/buy/+page.server.js27
-rw-r--r--src/routes/(app)/buy/+page.svelte150
-rw-r--r--src/routes/(app)/launch/thanks/[asset]/+page.server.js1
-rw-r--r--src/routes/(app)/ln/[lnurl]/+page.server.js12
-rw-r--r--src/routes/(app)/login/+page.server.js4
-rw-r--r--src/routes/(app)/login/+page.svelte1
-rw-r--r--src/routes/(app)/payment/[id]/+page.server.js4
-rw-r--r--src/routes/(app)/payment/[id]/+page.svelte82
-rw-r--r--src/routes/(app)/pot/[name]/withdraw/+page.server.js5
-rw-r--r--src/routes/(app)/register/+page.server.js4
-rw-r--r--src/routes/(app)/send/[...text]/+page.server.js5
-rw-r--r--src/routes/(app)/send/[...text]/+page.svelte3
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js22
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte110
-rw-r--r--src/routes/(app)/send/invoice/[id]/+page.server.js9
-rw-r--r--src/routes/(app)/send/invoice/[id]/+page.svelte1
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/+page.server.js14
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/+page.svelte6
-rw-r--r--src/routes/(app)/send/pot/[name]/[...amount]/+page.server.js2
-rw-r--r--src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte2
-rw-r--r--src/routes/(app)/send/user/[username]/[...amount]/+page.server.js10
-rw-r--r--src/routes/(app)/send/user/[username]/[...amount]/+page.svelte6
-rw-r--r--src/routes/(app)/sent/+page.svelte26
-rw-r--r--src/routes/(app)/support/+page.server.js16
-rw-r--r--src/routes/(app)/support/+page.svelte57
-rw-r--r--src/routes/(app)/support/+server.js9
-rw-r--r--src/routes/(static)/+layout.server.js16
-rw-r--r--src/routes/(static)/+layout.svelte6
-rw-r--r--src/routes/(static)/bounties/+page.svelte38
-rw-r--r--src/routes/(static)/docs/+page.server.js4
-rw-r--r--src/routes/(static)/docs/+page.svelte92
-rw-r--r--src/routes/+page.server.js2
49 files changed, 1057 insertions, 487 deletions
diff --git a/src/routes/(app)/+error.svelte b/src/routes/(app)/+error.svelte
new file mode 100644
index 00000000..36726e26
--- /dev/null
+++ b/src/routes/(app)/+error.svelte
@@ -0,0 +1,38 @@
+<script>
+ import { back } from '$lib/utils';
+ import { Icon } from '$comp';
+ import { page } from '$app/stores';
+
+ export let data;
+ let { user } = data;
+
+ let link = user ? `/${user.username}` : '/';
+</script>
+
+<div class="container px-4 max-w-xl mx-auto mt-10 space-y-5">
+ <div class="w-full flex">
+ <a href="/" class="mx-auto">
+ <Icon icon="logo" />
+ </a>
+ </div>
+
+ <div class="flex justify-center items-center">
+ <div class="shadow-xl rounded-3xl px-10 pt-5 pb-10 space-y-5 w-full mx-5">
+ <div class="relative">
+ <h1 class="text-2xl md:text-3xl font-semibold text-center">Error</h1>
+ </div>
+ <p class="text-secondary text-center">Something went wrong</p>
+ <p class="text-secondary text-center">{$page.error.message}</p>
+
+ <div class="flex justify-center py-24">
+ <a href={link}>
+ <button
+ class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 text-xl"
+ >
+ Back
+ </button>
+ </a>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/routes/(app)/+layout.server.js b/src/routes/(app)/+layout.server.js
index ee4ce550..3442081b 100644
--- a/src/routes/(app)/+layout.server.js
+++ b/src/routes/(app)/+layout.server.js
@@ -19,7 +19,7 @@ export async function load({ cookies, request, url, params }) {
throw redirect(307, `/migrate`);
}
- if (user && ['/', '/login', '/register'].includes(pathname) && request.method === 'GET') {
+ if (user && ['/login', '/register'].includes(pathname) && request.method === 'GET') {
throw redirect(307, `/${user.username}`);
}
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index 2a122209..45469bdb 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -58,7 +58,7 @@
onDestroy(() => browser && clearTimeout(timer));
</script>
-{#if $passwordPrompt}
+{#if browser && $passwordPrompt}
<Password {user} />
{/if}
diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte
index dfa25b78..133930b1 100644
--- a/src/routes/(app)/[username]/(profile)/+layout.svelte
+++ b/src/routes/(app)/[username]/(profile)/+layout.svelte
@@ -4,7 +4,6 @@
import { Icon } from '$comp';
import { t } from '$lib/translations';
import { sign, send } from '$lib/nostr';
- import { v4 } from 'uuid';
export let data;
let events, user, subject, src, text;
@@ -111,7 +110,7 @@
{/if}
<div class="w-full flex">
- <a href={`/${subject.pubkey}/messages`} class="mx-auto">
+ <a href={`/${subject.username}/messages`} class="mx-auto">
<button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60">
<div class="mx-auto flex">
<Icon icon="support" style="mr-2 my-auto" />
diff --git a/src/routes/(app)/[username]/(profile)/+page.server.js b/src/routes/(app)/[username]/(profile)/+page.server.js
index c68cfecc..d6cf3984 100644
--- a/src/routes/(app)/[username]/(profile)/+page.server.js
+++ b/src/routes/(app)/[username]/(profile)/+page.server.js
@@ -1,22 +1,35 @@
import { auth, get } from '$lib/utils';
export async function load({ cookies, params, parent, url }) {
- let { subject } = await parent();
+ let { user, subject } = await parent();
let { pubkey } = subject;
+ let { since = 0 } = params;
- let events = [];
+ let messages, invoices, sent, received;
+ messages = invoices = sent = received = [];
+
+ if (user) {
+ try {
+ messages = await get(`/${user.pubkey}/${since}/messages`);
+ messages = messages.sort((a, b) => b.created_at - a.created_at);
+ } catch (e) {
+ console.log(`failed to fetch nostr messages`, e);
+ }
+
+ ({ invoices, sent, received } = await get('/requests', auth(cookies)));
+ }
+
+ let notes = [];
try {
- events = await get(`/${pubkey}/notes`);
+ notes = await get(`/${pubkey}/notes`);
} catch (e) {
- console.log(`failed to fetch nostr events for ${pubkey}`, e);
+ console.log(`failed to fetch nostr notes for ${pubkey}`, e);
}
- events.map((e) => {
+ notes.map((e) => {
e.seen = e.created_at;
});
- let { invoices, sent, received } = await get('/requests', auth(cookies));
-
- return { invoices, events, sent, received };
+ return { invoices, messages, notes, sent, received };
}
diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte
index baab8ba1..168556e4 100644
--- a/src/routes/(app)/[username]/(profile)/+page.svelte
+++ b/src/routes/(app)/[username]/(profile)/+page.svelte
@@ -1,76 +1,197 @@
<script>
- import { Avatar, Balance, Feed } from '$comp';
+ import { onMount } from 'svelte';
+ import { Icon, Avatar, Balance, Feed } from '$comp';
import { t } from '$lib/translations';
import { f, sat, sats } from '$lib/utils';
+ import { sign, send, encrypt, decrypt } from '$lib/nostr';
+ import { event as e, password } from '$lib/store';
+ import { browser } from '$app/environment';
export let data;
- let { events, invoices, sent, received, subject, user } = data;
- console.log(invoices);
+ let { messages, notes, invoices, sent, received, subject, user } = data;
+ $: refresh(data);
+ let refresh = (d) => ({ messages, notes, invoices, sent, received, subject, user } = d);
+
+ let keys = new Set();
+ let latest = [];
+ let i = 0;
+ let ready;
+
+ e.subscribe(async (event) => {
+ if (!(event && ready)) return;
+ if (event.recipient.id === user?.id && !~latest.findIndex((m) => m.id === event.id)) {
+ event.content = await decrypt({ event, user });
+
+ let i = latest.findIndex((m) => m.pubkey === event.pubkey);
+ let popped;
+ if (~i) popped = latest.splice(i, 1);
+ else popped = latest.pop();
+
+ latest.unshift(event);
+ latest = latest;
+ }
+ });
+
+ $: initialize($password);
+ let initialize = async (p) => {
+ ready = false;
+ if (!p) return;
+ while (latest.length < 3 && i < messages.length) {
+ let event = messages[i];
+ let k = event.author.id === user.id ? event.recipient.pubkey : event.author.pubkey;
+ if (!keys.has(k)) {
+ keys.add(k);
+ event.content = await decrypt({ event, user });
+ if (event.content) latest.push(event);
+ }
+ i++;
+ }
+
+ latest = latest;
+ ready = true;
+ };
</script>
-<div class="flex justify-center lg:justify-start">
+<div class="space-y-8">
{#if user?.id === subject.id}
- <Balance {user} />
- {/if}
-</div>
+ <div>
+ <div class="flex justify-center lg:justify-start">
+ <Balance {user} />
+ </div>
+ {#if !user.balance}
+ <div class="space-y-8 mt-8">
+ <p class="text-secondary text-lg">
+ Welcome! Your account balance is empty. You can start by making a deposit or by asking
+ someone to pay you.
+ </p>
-<div>
- {#each invoices as { amount, currency, rate, request: { requester, recipient }, hash }}
- {@const requested = user.username === requester?.username}
- <a href={`/${recipient.username}/invoice/${hash}` + (requested ? '/tip' : '')}>
- <div class="border-b p-2 last:border-b-0 hover:bg-gray-100">
- <div class="flex gap-2">
- <div>
- {#if requested}
- <div class="flex">
- <Avatar user={recipient} size={20} />
- <div class="my-auto text-left">
- <p class="text-secondary">{$t('payments.invoiceFrom')}</p>
- <p class="ml-1 text-lg break-words">{recipient.username}</p>
+ <div class="flex gap-4 justify-center w-full">
+ <a href={`/${user.username}/receive`}>
+ <button
+ class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60 bg-black text-white"
+ >
+ <div class="mx-auto flex">
+ <Icon icon="numpad" style="my-auto h-6 mr-2 invert" />
+ <div class="my-auto mt-1">Request Payment</div>
</div>
+ </button>
+ </a>
+
+ {#if user.eligible}
+ <a href={`/buy`}>
+ <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60">
+ <div class="mx-auto flex">
+ <!-- <Icon icon="plus" style="my-auto h-6 mr-2" /> -->
+
+ <img
+ src="/images/bitcoin.svg"
+ class="w-8 border-4 border-transparent mr-2"
+ alt="Bitcoin"
+ />
+ <div class="my-auto mt-1">Buy Bitcoin</div>
+ </div>
+ </button>
+ </a>
+ {/if}
+ </div>
+ </div>
+ {/if}
+ </div>
+ {/if}
+
+ {#if invoices.length}
+ <div>
+ <h1 class="text-secondary text-xl">Invoices</h1>
+ {#each invoices as { amount, currency, rate, request: { requester, recipient }, hash }}
+ {@const requested = user.username === requester?.username}
+ <a href={`/${recipient.username}/invoice/${hash}` + (requested ? '/tip' : '')}>
+ <div class="border-b p-2 last:border-b-0 hover:bg-gray-100">
+ <div class="flex gap-2">
+ <div>
+ {#if requested}
+ <div class="flex">
+ <Avatar user={recipient} size={20} />
+ <div class="my-auto text-left">
+ <p class="text-secondary">{$t('payments.invoiceFrom')}</p>
+ <p class="ml-1 text-lg break-words">{recipient.username}</p>
+ </div>
+ </div>
+ {:else}
+ <div class="flex">
+ <Avatar user={requester} size={20} />
+ <div class="my-auto text-left">
+ <p class="text-secondary">{$t('payments.awaiting')}</p>
+ <p class="ml-1 text-lg break-words">{requester.username}</p>
+ </div>
+ </div>
+ {/if}
</div>
- {:else}
- <div class="flex">
- <Avatar user={requester} size={20} />
- <div class="my-auto text-left">
- <p class="text-secondary">{$t('payments.awaiting')}</p>
- <p class="ml-1 text-lg break-words">{requester.username}</p>
+ <div class="whitespace-nowrap my-auto ml-auto flex gap-2">
+ <div class="font-bold">
+ {f(amount * (rate / sats), currency)}
</div>
+
+ <div class="text-secondary">{sat(amount)}</div>
</div>
- {/if}
- </div>
- <div class="whitespace-nowrap my-auto ml-auto flex gap-2">
- <div class="font-bold">
- {f(amount * (rate / sats), currency)}
</div>
-
- <div class="text-secondary">{sat(amount)}</div>
</div>
- </div>
- </div>
- </a>
- {/each}
+ </a>
+ {/each}
+ </div>
+ {/if}
+ {#if received.length}
+ <div>
+ <h1 class="text-secondary text-xl">Requests</h1>
- {#each received as { id, invoice, memo, requester: r }}
- <a href={`/${user.username}/receive/${id}`}>
- <div class="border-b p-2 last:border-b-0 hover:bg-gray-100">
- <div class="flex">
- <div>
+ {#each received as { id, invoice, memo, requester: r }}
+ <a href={`/${user.username}/receive/${id}`}>
+ <div class="border-b p-2 last:border-b-0 hover:bg-gray-100">
<div class="flex">
- <Avatar user={r} size={20} />
- <div class="my-auto text-left">
- <p class="ml-1 text-lg break-words">{r.username}</p>
- <p class="ml-1 text-secondary">{memo}</p>
+ <div>
+ <div class="flex">
+ <Avatar user={r} size={20} />
+ <div class="my-auto text-left">
+ <p class="ml-1 text-lg break-words">{r.username}</p>
+ <p class="ml-1 text-secondary">{memo}</p>
+ </div>
+ </div>
+ </div>
+ <div class="whitespace-nowrap my-auto ml-auto flex gap-2">
+ <button class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full"
+ >Invoice</button
+ >
</div>
</div>
</div>
- <div class="whitespace-nowrap my-auto ml-auto flex gap-2">
- <button class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full"
- >Invoice</button
- >
+ </a>
+ {/each}
+ </div>
+ {/if}
+
+ {#if latest.length && user?.id === subject.id}
+ <div class="relative">
+ {#each latest as { content, pubkey, author, recipient }}
+ {@const user = author.id === user.id ? recipient : author}
+ <a href={`/${user.username}/messages`}>
+ <div class="flex hover:bg-gray-100 p-4 rounded-2xl">
+ <div class="my-auto">
+ <Avatar {user} size={'20'} disabled={true} />
+ </div>
+ <div class="my-auto truncate">
+ <div class="my-auto ml-1 text-lg font-bold">{user.username}</div>
+ <div class="my-auto ml-1 text-secondary text-lg truncate">
+ {content}
+ </div>
+ </div>
</div>
- </div>
- </div>
- </a>
- {/each}
+ </a>
+ {/each}
+ </div>
+ {/if}
</div>
+
+<style>
+ h1 {
+ @apply mb-2;
+ }
+</style>
diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.svelte b/src/routes/(app)/[username]/(profile)/messages/+page.svelte
deleted file mode 100644
index db7b1ced..00000000
--- a/src/routes/(app)/[username]/(profile)/messages/+page.svelte
+++ /dev/null
@@ -1,123 +0,0 @@
-<script>
- import { browser } from '$app/environment';
- import { t } from '$lib/translations';
- import { format, parseISO } from 'date-fns';
- import { scale } from 'svelte/transition';
- import { enhance } from '$app/forms';
- import { Icon } from '$comp';
- import { back, fail, focus } from '$lib/utils';
- import { sign, send, encrypt, decrypt } from '$lib/nostr';
- import { messages, password } from '$lib/store';
- import { tick, onMount } from 'svelte';
- import { calculateId } from 'nostr';
-
- export let data;
-
- let { newMessages, subject, user } = data;
- let input, pane;
-
- $: initialize($password);
- let initialize = async (p) => {
- if (!p) return;
- $messages[user.pubkey] = $messages[user.pubkey] || {};
- $messages[user.pubkey][subject.pubkey] = $messages[user.pubkey][subject.pubkey] || {};
-
- for (let m of newMessages) {
- if (!$messages[user.pubkey][subject.pubkey][m.id]) {
- m.message = await decrypt({ event: m, user });
- $messages[user.pubkey][subject.pubkey][m.id] = m;
- tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
- }
- }
- tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
- };
-
- $: events =
- ($messages[user.pubkey] &&
- $messages[user.pubkey][subject.pubkey] &&
- Object.values($messages[user.pubkey][subject.pubkey]).sort(
- (a, b) => a.created_at - b.created
- )) ||
- [];
-
- let sent, submitting, message;
- let submit = async () => {
- submitting = true;
-
- let event = {
- pubkey: user.pubkey,
- created_at: Math.floor(Date.now() / 1000),
- kind: 4,
- tags: [['p', subject.pubkey]]
- };
-
- event.message = message;
- event.id = await calculateId(event);
- $messages[user.pubkey][subject.pubkey][event.id] = event;
- $messages = $messages;
- tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
-
- try {
- event.content = await encrypt({ message, recipient: subject.pubkey, user });
- await sign({ event, user });
- await send(event);
-
- sent = true;
- } catch (e) {
- fail('Failed to send message');
- delete $messages[user.pubkey][subject.pubkey][event.id];
- $messages = $messages;
- }
-
- message = '';
- submitting = false;
- };
-</script>
-
-<div class="container max-w-lg px-4 mx-auto mt-10 space-y-5">
- <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold">
- {$t('payments.sendMessage')}
- </h1>
-
- <div class="max-h-[calc(50vh)] overflow-y-scroll p-4" bind:this={pane}>
- {#each events as data}
- {@const ours = data.pubkey === user.pubkey}
- {@const theirs = !ours}
- <div class="rounded-2xl p-4 max-w-[300px] mb-1 text-md" class:ours class:theirs>
- {#if data.message}
- {data.message}
- {:else}
- -
- {/if}
- </div>
- <div class="text-sm text-gray-400 mb-6" class:text-right={ours}>
- {format(new Date(data.created_at * 1000), 'MMM d')},
- {format(new Date(data.created_at * 1000), 'h:mm aa')}
- </div>
- {/each}
- </div>
-
- <form method="POST" class="space-y-5" on:submit|preventDefault={submit}>
- <input type="hidden" name="requester_id" value={user.id} />
- <input type="hidden" name="recipient" value={subject.username} />
-
- <input
- use:focus
- name="message"
- placeholder={$t('payments.sendMessage')}
- class="w-full p-4 border rounded-xl"
- bind:value={message}
- bind:this={input}
- />
- </form>
-</div>
-
-<style>
- .ours {
- @apply bg-gray-100 rounded-br-none ml-auto;
- }
-
- .theirs {
- @apply bg-gray-200 rounded-bl-none;
- }
-</style>
diff --git a/src/routes/(app)/[username]/(profile)/request/+page.server.js b/src/routes/(app)/[username]/(profile)/request/+page.server.js
index 2c1d6bde..e7ad6a8f 100644
--- a/src/routes/(app)/[username]/(profile)/request/+page.server.js
+++ b/src/routes/(app)/[username]/(profile)/request/+page.server.js
@@ -1,15 +1,15 @@
import { redirect } from '@sveltejs/kit';
-import { auth, post } from '$lib/utils';
+import { fd, auth, post } from '$lib/utils';
export const actions = {
create: async ({ cookies, params, request, url }) => {
- let form = Object.fromEntries(await request.formData());
+ let form = await fd(request);
let { id, requester } = await post('/requests', form, auth(cookies));
throw redirect(307, `/${params.username}/request/${id}`);
},
delete: async ({ cookies, request }) => {
- let form = Object.fromEntries(await request.formData());
+ let form = await fd(request);
let r = await post('/requests/delete', form, auth(cookies));
}
};
diff --git a/src/routes/(app)/[username]/dashboard/+page.svelte b/src/routes/(app)/[username]/dashboard/+page.svelte
index 499034e6..d8761ea9 100644
--- a/src/routes/(app)/[username]/dashboard/+page.svelte
+++ b/src/routes/(app)/[username]/dashboard/+page.svelte
@@ -7,7 +7,6 @@
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
import { events } from '$lib/store';
- import { v4 } from 'uuid';
export let data;
$: ({ invoices, requests } = data);
diff --git a/src/routes/(app)/[username]/invoice/[hash]/+page.svelte b/src/routes/(app)/[username]/invoice/[hash]/+page.svelte
index ad9721e7..fff2b22c 100644
--- a/src/routes/(app)/[username]/invoice/[hash]/+page.svelte
+++ b/src/routes/(app)/[username]/invoice/[hash]/+page.svelte
@@ -1,12 +1,13 @@
<script>
+ import { enhance } from '$app/forms';
import { send } from '$lib/socket';
- import { back, copy, f, get, sat, reverseFormat, s, sats } from '$lib/utils';
+ import { post, back, copy, f, get, types, sat, reverseFormat, s, sats } from '$lib/utils';
import { tick, onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
import { last } from '$lib/store';
import { Avatar, Icon, Heart, Image } from '$comp';
import { t } from '$lib/translations';
- import screenfull from 'screenfull';
+ import { goto, invalidate } from '$app/navigation';
export let data;
@@ -21,18 +22,19 @@
prompt,
text,
tip,
+ request_id,
user: { username, currency }
} = invoice;
let showQr = !amount;
- let src = sm;
+ $: src = sm;
let qr;
let tipPercent = 0;
let refresh = (data) => {
- ({ invoice, id } = data);
+ ({ invoice, id, user, sm, lg } = data);
({
amount,
hash,
@@ -64,31 +66,54 @@
$: link = type === 'bitcoin' ? text : `lightning:${text}`;
$: txt = type === 'bitcoin' ? hash : text;
- let full;
- let toggle = () => {
- screenfull.toggle(qr);
- full = !full;
- if (full) src = lg;
- else src = sm;
+ let toggleType = async () => {
+ invoice.type = invoice.type === types.lightning ? types.bitcoin : types.lightning;
+ ({ hash } = await post(`/${username}/invoice`, {
+ invoice,
+ user: { username, currency }
+ }));
+
+ goto(`./${hash}`, { invalidateAll: true });
};
</script>
<div class="container mx-auto max-w-lg px-4 space-y-5">
- <div class="relative flex">
- <div class="flex mx-auto w-[360px] h-[360px]">
+ <div class="whitespace-nowrap my-auto ml-auto flex gap-2">
+ <button
+ class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full"
+ class:bg-black={type === types.lightning}
+ class:text-white={type === types.lightning}
+ on:click={toggleType}
+ >
+ ⚡️ Lightning
+ </button>
+
+ <button
+ class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full flex justify-center "
+ class:bg-black={type === types.bitcoin}
+ class:text-white={type === types.bitcoin}
+ on:click={toggleType}
+ >
<img
- {src}
- class:p-4={full}
- class="w-[300px] h-[300px] mx-auto z-10 mt-[20px]"
- bind:this={qr}
- on:click={toggle}
- on:keydown={toggle}
- alt={txt}
+ src="/images/bitcoin.svg"
+ class="my-auto w-8 border-4 border-transparent"
+ alt="Bitcoin"
/>
- </div>
- <div
- class="absolute m-auto left-0 right-0 w-[340px] h-[340px] rounded-full bg-gradient-to-r from-[#F2F6FC] to-[#E1E3FF] z-0"
- />
+ <div class="my-auto">Bitcoin</div>
+ </button>
+ </div>
+
+ <div>
+ <a href={invoice.type === 'bitcoin' ? invoice.text : 'lightning:' + invoice.text}>
+ <div class="relative flex">
+ <div class="flex mx-auto w-[360px] h-[360px]">
+ <img {src} class="w-[300px] h-[300px] mx-auto z-10 mt-[20px]" bind:this={qr} alt={txt} />
+ </div>
+ <div
+ class="absolute m-auto left-0 right-0 w-[340px] h-[340px] rounded-full bg-gradient-to-r from-[#F2F6FC] to-[#E1E3FF] z-0"
+ />
+ </div>
+ </a>
</div>
{#if amount > 0}
@@ -115,9 +140,9 @@
{/if}
<div class="w-full flex justify-center">
- <button class="flex rounded-full border py-2 px-5 hover:opacity-80" on:click={() => copy(txt)}>
+ <button class="flex rounded-full border py-3 px-5 hover:opacity-80" on:click={() => copy(txt)}>
<Icon icon="copy" style="mr-1" />
- <div class="text-secondary">{txt.substr(0, 10)}..{txt.substr(-6)}</div></button
+ <div class="text-secondary">{txt.substr(0, 10)}...{txt.substr(-10)}</div></button
>
</div>
</div>
diff --git a/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte b/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte
index 432b137a..3a281728 100644
--- a/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte
+++ b/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte
@@ -4,7 +4,7 @@
import { toast } from '@zerodevx/svelte-toast';
import { browser } from '$app/environment';
import { t } from '$lib/translations';
- import { f, s, sat, sats } from '$lib/utils';
+ import { fiat, f, s, sat, sats } from '$lib/utils';
import { goto } from '$app/navigation';
export let data;
@@ -12,10 +12,6 @@
$: data && ({ amount, currency, rate, received, pending, tip, user, id } = data.invoice);
toast.pop(0);
-
- $: amountFiat = parseFloat((((received - tip) * rate) / sats).toFixed(2));
- $: pendingFiat = parseFloat((((pending - tip) * rate) / sats).toFixed(2));
- $: tipFiat = parseFloat(((tip * rate) / sats).toFixed(2));
</script>
<div class="container px-4 text-center mx-auto">
@@ -25,15 +21,20 @@
{#if received}
<h1 class="text-3xl md:text-4xl font-bold mb-6">{$t('invoice.paymentSuccessful')}</h1>
<h2 class="text-2xl md:text-3xl font-semibold">
- {f(amountFiat, currency)}
+ {f(fiat(received - tip, rate), currency)}
{#if tip}
- + {f(tipFiat, currency)}
+ <span class="text-lg">
+ + {f(fiat(tip, rate), currency)}
+ </span>
{/if}
</h2>
<h3 class="text-secondary md:text-lg mb-6 mt-1">
- {sat(received - tip)}
+ ⚡️{s(received - tip)}
+
{#if tip}
- +{sat(tip)}
+ <span class="text-lg">
+ + ⚡️{s(tip)}
+ </span>
{/if}
</h3>
{/if}
@@ -41,15 +42,17 @@
{#if pending}
<h1 class="text-3xl md:text-4xl font-bold mb-6">Payment detected</h1>
<h2 class="text-2xl md:text-3xl font-semibold">
- {f(pendingFiat, currency)}
+ {f(fiat(pending, rate), currency)}
{#if tip}
- + {f(tipFiat, currency)}
+ + {f(fiat(tip, rate), currency)}
{/if}
</h2>
<h3 class="text-secondary md:text-lg mb-6 mt-1">
- {sat(pending - tip)}
+ ⚡️{s(pending - tip)}
{#if tip}
- +{sat(tip)}
+ <span class="text-lg">
+ + ⚡️{s(tip)}
+ </span>
{/if}
</h3>
{/if}
diff --git a/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte b/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte
index d033c413..296ef9c6 100644
--- a/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte
+++ b/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte
@@ -3,7 +3,7 @@
import { browser } from '$app/environment';
import { invoice as inv, request } from '$lib/store';
import { copy, focus, f, sat, get, reverseFormat, s, sats } from '$lib/utils';
- import { Icon } from '$comp';
+ import { Icon, Slider } from '$comp';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
import { enhance } from '$app/forms';
@@ -37,9 +37,6 @@
customInput.value = '';
}
tipPercent = e.target.value;
-
- clearTimeout(timeout);
- timeout = setTimeout(update, 800);
};
let submitting;
@@ -56,10 +53,6 @@
};
let submit;
- let update = async () => {
- await tick();
- //submit.click();
- };
export let data;
$: refresh(data);
@@ -123,16 +116,7 @@
{#if !showCustomAmount}
<div class="my-2 text-xl">{Math.round(tipPercent)}%</div>
- <input
- id="slider"
- type="range"
- min="0"
- max="100"
- value={tipPercent}
- class="px-0 py-0 my-5"
- style="--bgPercent: {tipPercent}%;"
- on:input={handleSlide}
- />
+ <Slider handle={handleSlide} bind:value={tipPercent} />
<div class="flex mb-8 text-lg">
<div>
@@ -201,43 +185,6 @@
</div>
<style>
- #slider {
- -webkit-appearance: none;
- width: 100%;
- height: 6px;
- border-radius: 5px;
- background: linear-gradient(
- to right,
- black 0%,
- black var(--bgPercent),
- #f5f7fa var(--bgPercent),
- #f5f7fa 100%
- );
- outline: none;
- }
-
- #slider:active {
- background-color: black;
- }
-
- #slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 30px;
- height: 30px;
- border-radius: 50%;
- background: black;
- cursor: pointer;
- }
-
- #slider::-moz-range-thumb {
- width: 30px;
- height: 30px;
- border-radius: 50%;
- background: black;
- cursor: pointer;
- }
-
.active {
@apply !bg-black !text-white;
}
diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.server.js b/src/routes/(app)/[username]/messages/+page.server.js
index 63116099..7b651389 100644
--- a/src/routes/(app)/[username]/(profile)/messages/+page.server.js
+++ b/src/routes/(app)/[username]/messages/+page.server.js
@@ -7,10 +7,13 @@ export async function load({ params, parent, url }) {
let messages = [];
try {
- messages = await get(`/nostr/${user.pubkey}/${subject.pubkey}/${since}/messages`);
+ messages = await get(`/${user.pubkey}/${since}/messages`);
+ messages = messages
+ .filter((m) => m.recipient.id === subject.id || m.author.id === subject.id)
+ .sort((a, b) => a.created_at - b.created_at);
} catch (e) {
console.log(`failed to fetch nostr messages`, e);
}
- return { newMessages: messages };
+ return { messages, user };
}
diff --git a/src/routes/(app)/[username]/messages/+page.svelte b/src/routes/(app)/[username]/messages/+page.svelte
new file mode 100644
index 00000000..7a10d605
--- /dev/null
+++ b/src/routes/(app)/[username]/messages/+page.svelte
@@ -0,0 +1,171 @@
+<script>
+ import { cubicInOut } from 'svelte/easing';
+ import { browser } from '$app/environment';
+ import { t } from '$lib/translations';
+ import { format } from 'date-fns';
+ import { scale, fade, fly } from 'svelte/transition';
+ import { enhance } from '$app/forms';
+ import { Avatar, Icon } from '$comp';
+ import { back, fail, focus } from '$lib/utils';
+ import { sign, send, encrypt, decrypt } from '$lib/nostr';
+ import { event as e, password } from '$lib/store';
+ import { tick, onMount } from 'svelte';
+ import { calculateId } from 'nostr';
+
+ export let data;
+
+ function fadeScale(node, { delay = 0, duration = 200, easing = (x) => x, baseScale = 0 }) {
+ const o = +getComputedStyle(node).opacity;
+ const m = getComputedStyle(node).transform.match(/scale\(([0-9.]+)\)/);
+ const s = m ? m[1] : 1;
+ const is = 1 - baseScale;
+
+ return {
+ delay,
+ duration,
+ css: (t) => {
+ const eased = easing(t);
+ return `opacity: ${eased * o}; transform: scale(${eased * s * is + baseScale})`;
+ }
+ };
+ }
+
+ let { messages, subject, user } = data;
+ let input, pane;
+
+ $: initialize($password);
+ let initialize = async (p) => {
+ if (!p) return;
+ await Promise.all(
+ messages.map(async (event) => (event.message = await decrypt({ event, user })))
+ );
+
+ messages = messages;
+ tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
+ };
+
+ e.subscribe(async (event) => {
+ let found = ~messages.findIndex((m) => m.id === event?.id);
+ if (event?.recipient.id === user.id && !found) {
+ event.message = await decrypt({ event, user });
+ messages.push(event);
+ messages = messages;
+ tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
+ }
+ });
+
+ let sent, submitting, message;
+ let submit = async () => {
+ submitting = true;
+
+ let event = {
+ pubkey: user.pubkey,
+ created_at: Math.floor(Date.now() / 1000),
+ kind: 4,
+ tags: [['p', subject.pubkey]]
+ };
+
+ event.message = message;
+
+ message = '';
+
+ event.author = user;
+ event.recipient = subject;
+
+ messages.push(event);
+ messages = messages;
+ tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
+
+ try {
+ event.id = await calculateId(event);
+ event.content = await encrypt({ message: event.message, recipient: subject.pubkey, user });
+ await sign({ event, user });
+ await send(event);
+
+ sent = true;
+ } catch (e) {
+ console.log(e);
+ fail('Failed to send message');
+ }
+
+ message = '';
+ submitting = false;
+ };
+
+ let keydown = (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ submit();
+ }
+ };
+</script>
+
+<div class="container max-w-xl mx-auto px-4 space-y-5">
+ <div
+ class="h-[50vh] max-h-[50vh] overflow-y-scroll scrollbar-thin scrollbar-thumb-[#F2F6FC] scrollbar-track-white pr-8"
+ bind:this={pane}
+ >
+ {#each messages as { id, author, message, created_at, pubkey }}
+ {@const ours = pubkey === user.pubkey}
+ {@const theirs = !ours}
+
+ {#if message}
+ <div
+ class="flex gap-2"
+ class:flex-row-reverse={theirs}
+ class:justify-end={theirs}
+ in:fadeScale={{
+ easing: cubicInOut,
+ baseScale: 0.5
+ }}
+ >
+ <div
+ class="rounded-2xl px-4 py-2 max-w-[300px] mb-1 text-lg"
+ class:ours
+ class:theirs
+ :key={id}
+ >
+ {message}
+ </div>
+ <div class="mt-auto">
+ <Avatar user={author} size={'12'} />
+ </div>
+ </div>
+ <div class="text-sm text-gray-400 mb-6" class:text-right={ours}>
+ {format(new Date(created_at * 1000), 'MMM d')},
+ {format(new Date(created_at * 1000), 'h:mm aa')}
+ </div>
+ {/if}
+ {/each}
+ </div>
+
+ <form
+ method="POST"
+ class="w-full border rounded-xl outline-none gap-4 flex p-0 pr-2"
+ on:submit|preventDefault={submit}
+ >
+ <input type="hidden" name="requester_id" value={user.id} />
+ <input type="hidden" name="recipient" value={subject.username} />
+
+ <div
+ use:focus
+ contenteditable
+ class="grow break-all py-4 outline-none mt-0 pl-4"
+ bind:innerHTML={message}
+ on:keydown={keydown}
+ />
+ <button type="submit" class="my-auto shrink-0">
+ <Icon icon="send" style="w-8" />
+ </button>
+ </form>
+</div>
+
+<style>
+ .ours {
+ @apply bg-gradient-to-r from-[#F2F6FC] to-[#E1E3FF] text-black rounded-br-none ml-auto;
+ }
+
+ .theirs {
+ @apply bg-gray-100 text-black rounded-bl-none;
+ }
+</style>
diff --git a/src/routes/(app)/[username]/payments/[...page]/+page.svelte b/src/routes/(app)/[username]/payments/[...page]/+page.svelte
index 142be4c4..a502f8c3 100644
--- a/src/routes/(app)/[username]/payments/[...page]/+page.svelte
+++ b/src/routes/(app)/[username]/payments/[...page]/+page.svelte
@@ -1,7 +1,7 @@
<script>
import { Avatar, Icon } from '$comp';
import { onMount } from 'svelte';
- import { format, parseISO } from 'date-fns';
+ import { format } from 'date-fns';
import { newPayment, payments } from '$lib/store';
import { t } from '$lib/translations';
import { get, f, s, sat, sats } from '$lib/utils';
@@ -180,14 +180,12 @@
</div>
</a>
{:else if p.with}
- <a href={`/${p.with.username}`}>
- <div class="flex">
- <div class="my-auto">
- <Avatar user={p.with} size={12} />
- </div>
- <div class="my-auto ml-1 text-secondary">{p.with.username}</div>
+ <div class="flex">
+ <div class="my-auto">
+ <Avatar user={p.with} size={16} disabled={true} />
</div>
- </a>
+ <div class="my-auto ml-1 text-secondary">{p.with.username}</div>
+ </div>
{:else if p.type === types.classic}
<img
src="/images/classic.png"
diff --git a/src/routes/(app)/[username]/receive/[...param]/+page.svelte b/src/routes/(app)/[username]/receive/[...param]/+page.svelte
index 99d95be0..95495368 100644
--- a/src/routes/(app)/[username]/receive/[...param]/+page.svelte
+++ b/src/routes/(app)/[username]/receive/[...param]/+page.svelte
@@ -46,10 +46,5 @@
>
{amount ? $t('payments.next') : $t('payments.sendersChoice')}
</button>
-
- <div class="w-full flex justify-center">
- <span class="font-bold mr-2">{type[0].toUpperCase() + type.substring(1, type.length)}</span>
- <Toggle id="prompt" checked={lightning} bind:value={lightning} />
- </div>
</div>
</form>
diff --git a/src/routes/(app)/[username]/settings/[...tab]/+page.server.js b/src/routes/(app)/[username]/settings/[...tab]/+page.server.js
index 361a9f85..535c588f 100644
--- a/src/routes/(app)/[username]/settings/[...tab]/+page.server.js
+++ b/src/routes/(app)/[username]/settings/[...tab]/+page.server.js
@@ -1,5 +1,5 @@
import { fail, redirect } from '@sveltejs/kit';
-import { auth, get, post } from '$lib/utils';
+import { fd, auth, get, post } from '$lib/utils';
export function load({ params, url }) {
if (url.pathname.endsWith('settings')) throw redirect(307, url.pathname + '/account');
@@ -7,8 +7,9 @@ export function load({ params, url }) {
}
export const actions = {
- save: async ({ cookies, request }) => {
- let form = Object.fromEntries(await request.formData());
+ default: async ({ cookies, request }) => {
+ let form = await fd(request);
+ form.prompt = form.prompt === 'on';
let user = { ...(await get('/me', auth(cookies))), ...form };
try {
diff --git a/src/routes/(app)/buy/+page.server.js b/src/routes/(app)/buy/+page.server.js
new file mode 100644
index 00000000..fc7ae473
--- /dev/null
+++ b/src/routes/(app)/buy/+page.server.js
@@ -0,0 +1,27 @@
+import { env } from '$env/dynamic/private';
+import { error, fail, redirect } from '@sveltejs/kit';
+import { post, auth } from '$lib/utils';
+import { createTicket } from '$lib/ticket';
+
+export const actions = {
+ default: async ({ cookies, request }) => {
+ let form = await request.formData();
+
+ let amount = parseFloat(form.get('amount'));
+ if (!amount || amount < 0 || amount > 500) return fail(400, { message: 'Invalid amount' });
+
+ await post(
+ '/buy',
+ {
+ amount: form.get('amount'),
+ number: form.get('number').replace(/\s+/g, ''),
+ month: form.get('expiry').slice(0, 2),
+ year: form.get('expiry').slice(-2),
+ cvc: form.get('cvc')
+ },
+ auth(cookies)
+ );
+
+ throw redirect(307, `/${form.get('username')}/payments`);
+ }
+};
diff --git a/src/routes/(app)/buy/+page.svelte b/src/routes/(app)/buy/+page.svelte
new file mode 100644
index 00000000..2f556ada
--- /dev/null
+++ b/src/routes/(app)/buy/+page.svelte
@@ -0,0 +1,150 @@
+<script>
+ import { enhance } from '$app/forms';
+ import { Icon, Lock, Card, Calendar, Spinner } from '$comp';
+ import { animatedRate, selectedRate } from '$lib/store';
+ import { f } from '$lib/utils';
+
+ export let data, form;
+
+ let { username, currency } = data.user;
+ let loading;
+
+ let amount, n, d, cvc;
+
+ let ease = (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t);
+ let i;
+
+ let o = $selectedRate;
+ $: animate($selectedRate);
+ let animate = (n) => {
+ clearInterval(i);
+
+ let t = 0;
+ let d = o - n;
+
+ i = setInterval(() => {
+ $animatedRate = (o - d * ease(t / 100)).toFixed(2);
+ if (t > 80) (o = n) && clearInterval(i);
+ t++;
+ }, 10);
+ };
+</script>
+
+<div class="container px-4 max-w-xl mx-auto mt-10 space-y-5">
+ <div class="w-full flex">
+ <a href="/" class="mx-auto">
+ <Icon icon="logo" />
+ </a>
+ </div>
+
+ <div class="flex justify-center items-center">
+ <div class="shadow-xl rounded-3xl px-10 pt-5 pb-10 space-y-5 w-full mx-5">
+ <div class="relative">
+ <h1 class="text-2xl md:text-3xl font-semibold text-center">Buy Bitcoin</h1>
+ </div>
+ <p class="text-secondary text-center">Purchase up to $500 worth of Bitcoin</p>
+
+ {#if form?.message}
+ <div class="text-red-600 text-center">
+ {form.message}
+ </div>
+ {/if}
+
+ <form
+ method="POST"
+ class="flex flex-wrap gap-5 w-full p-5 mt-20"
+ use:enhance={() => {
+ loading = true;
+ return ({ update }) => update() && (loading = false);
+ }}
+ >
+ <input type="hidden" name="username" value={username} />
+
+ <label class="relative w-full flex flex-col">
+ <span class="font-bold mb-3">Amount</span>
+ <input
+ class="rounded-md peer pl-12 pr-2 py-2 border-2 border-gray-200 placeholder-gray-300"
+ type="text"
+ name="amount"
+ placeholder="&bull;&bull;&bull;"
+ bind:value={amount}
+ />
+ <span
+ class="absolute bottom-[9px] left-0 translate-x-1/2 w-6 text-black peer-placeholder-shown:text-gray-300
+ ">$</span
+ >
+ </label>
+
+ <label class="relative w-full flex flex-col">
+ <span class="font-bold mb-3">Card number</span>
+ <input
+ class="rounded-md peer pl-12 pr-2 py-2 border-2 border-gray-200 placeholder-gray-300"
+ type="text"
+ name="number"
+ placeholder="0000 0000 0000 0000"
+ bind:value={n}
+ />
+ <Card />
+ </label>
+
+ <label class="relative flex-1 flex flex-col">
+ <span class="font-bold mb-3">Expire date</span>
+ <input
+ class="rounded-md peer pl-12 pr-2 py-2 border-2 border-gray-200 placeholder-gray-300"
+ type="text"
+ name="expiry"
+ placeholder="MM/YY"
+ bind:value={d}
+ />
+ <Calendar />
+ </label>
+
+ <label class="relative flex-1 flex flex-col">
+ <span class="font-bold flex items-center gap-3 mb-3">
+ CVC/CVV
+ <span class="relative group">
+ <span
+ class="hidden group-hover:flex justify-center items-center px-2 py-1 text-xs absolute -right-2 transform translate-x-full -translate-y-1/2 w-max top-1/2 bg-black text-white"
+ >
+ 3-digit number on the back of your card</span
+ >
+ </span>
+ </span>
+ <input
+ class="rounded-md peer pl-12 pr-2 py-2 border-2 border-gray-200 placeholder-gray-300"
+ type="text"
+ name="cvc"
+ placeholder="&bull;&bull;&bull;"
+ bind:value={cvc}
+ />
+ <Lock />
+ </label>
+
+ <div class="w-full flex mt-8">
+ <button
+ class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 mx-auto text-xl"
+ type="submit"
+ disabled={loading}
+ >
+ {#if loading}
+ <Spinner />
+ {:else}
+ Submit
+ {/if}
+ </button>
+ </div>
+ </form>
+
+ <div class="flex">
+ <div class="text-secondary flex mr-auto">
+ <div class="flex mr-1">
+ <div class="my-auto mr-1">1</div>
+ <img src="/images/bitcoin.svg" class="w-5 my-auto" alt="Bitcoin" />
+ </div>
+ <div>= {f($animatedRate, currency)}</div>
+ </div>
+ <p class="text-secondary">3% credit card fee applies</p>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/src/routes/(app)/launch/thanks/[asset]/+page.server.js b/src/routes/(app)/launch/thanks/[asset]/+page.server.js
deleted file mode 100644
index e9e01376..00000000
--- a/src/routes/(app)/launch/thanks/[asset]/+page.server.js
+++ /dev/null
@@ -1 +0,0 @@
-// export const load = ({ parent }) => parent();
diff --git a/src/routes/(app)/ln/[lnurl]/+page.server.js b/src/routes/(app)/ln/[lnurl]/+page.server.js
index a12b063c..ddd714bf 100644
--- a/src/routes/(app)/ln/[lnurl]/+page.server.js
+++ b/src/routes/(app)/ln/[lnurl]/+page.server.js
@@ -1,4 +1,4 @@
-import { auth, get, post } from '$lib/utils';
+import { fd, auth, get, post } from '$lib/utils';
import { error, fail, redirect } from '@sveltejs/kit';
export async function load({ params, parent }) {
@@ -24,10 +24,7 @@ export const actions = {
pay: async ({ cookies, fetch, request }) => {
let error;
- let { callback, amount, minSendable, maxSendable } = Object.fromEntries(
- await request.formData()
- );
-
+ let { callback, amount, minSendable, maxSendable } = await fd(request);
minSendable = Math.round(minSendable / 1000);
maxSendable = Math.round(maxSendable / 1000);
@@ -42,8 +39,9 @@ export const actions = {
withdraw: async ({ cookies, fetch, request }) => {
let error;
- let { callback, amount, username, currency, minWithdrawable, maxWithdrawable, k1 } =
- Object.fromEntries(await request.formData());
+ let { callback, amount, username, currency, minWithdrawable, maxWithdrawable, k1 } = await fd(
+ request
+ );
minWithdrawable = Math.round(minWithdrawable / 1000);
maxWithdrawable = Math.round(maxWithdrawable / 1000);
diff --git a/src/routes/(app)/login/+page.server.js b/src/routes/(app)/login/+page.server.js
index a6a7120a..67f686e0 100644
--- a/src/routes/(app)/login/+page.server.js
+++ b/src/routes/(app)/login/+page.server.js
@@ -1,5 +1,5 @@
import { fail, redirect } from '@sveltejs/kit';
-import { login } from '$lib/utils';
+import { login, fd } from '$lib/utils';
export const load = async ({ parent }) => {
let { user } = await parent();
@@ -8,7 +8,7 @@ export const load = async ({ parent }) => {
export const actions = {
default: async ({ cookies, request }) => {
- let form = Object.fromEntries(await request.formData());
+ let form = await fd(request);
let { username, password, token, loginRedirect } = form;
let user = { username, password, token };
diff --git a/src/routes/(app)/login/+page.svelte b/src/routes/(app)/login/+page.svelte
index 9a1e41da..75998cd0 100644
--- a/src/routes/(app)/login/+page.svelte
+++ b/src/routes/(app)/login/+page.svelte
@@ -1,5 +1,4 @@
<script>
- import { messages } from '$lib/store';
import { RegisterOrLogin } from '$comp';
export let form;
diff --git a/src/routes/(app)/payment/[id]/+page.server.js b/src/routes/(app)/payment/[id]/+page.server.js
index cbd7355e..1c90ab69 100644
--- a/src/routes/(app)/payment/[id]/+page.server.js
+++ b/src/routes/(app)/payment/[id]/+page.server.js
@@ -1,6 +1,6 @@
import { auth, get } from '$lib/utils';
export async function load({ cookies, params }) {
- let tx = await get(`/payments/${params.id}`, auth(cookies));
- return { tx };
+ let payment = await get(`/payments/${params.id}`, auth(cookies));
+ return { payment };
}
diff --git a/src/routes/(app)/payment/[id]/+page.svelte b/src/routes/(app)/payment/[id]/+page.svelte
index a961d14d..2e170a43 100644
--- a/src/routes/(app)/payment/[id]/+page.svelte
+++ b/src/routes/(app)/payment/[id]/+page.svelte
@@ -1,62 +1,98 @@
<script>
import { t } from '$lib/translations';
- import { back, copy, f, s, sats } from '$lib/utils';
+ import { back, copy, f, s, fiat, sats } from '$lib/utils';
import { Avatar, Icon } from '$comp';
import { format } from 'date-fns';
import { PUBLIC_EXPLORER as expl } from '$env/static/public';
export let data;
- let { user, tx } = data;
+ let { user, payment: p } = data;
let { username } = user;
- let { id, amount, created, rate, type, ref, fee, currency } = tx;
- fee = fee || 0;
+ let { id, amount, created, rate, type, ref, tip, ourfee, fee, currency } = p;
+ let a = Math.abs(amount);
- let fiat = (Math.abs(amount) * rate) / sats;
+ fee = fee || 0;
</script>
<button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" on:click={back}>
<Icon icon="arrow-left" style="w-10" />
</button>
-<div class="container mx-auto max-w-lg px-4 space-y-2 sm:space-y-5 break-all">
+<div class="container mx-auto max-w-lg px-4 space-y-8 break-all text-2xl">
<h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold mb-10">
{$t(amount < 0 ? 'payments.sent' : 'payments.received')}
</h1>
+ {#if p.with}
+ <div>
+ <span class="text-lg text-secondary my-auto mr-2">{amount > 0 ? 'From' : 'To'}</span>
+ <a href={`/${p.with.username}`}>
+ <div class="flex">
+ <div class="my-auto">
+ <Avatar user={p.with} size={20} />
+ </div>
+ <div class="my-auto ml-1">{p.with.username}</div>
+ </div>
+ </a>
+ </div>
+ {/if}
+
<div>
- <div class="font-bold">{$t('payments.amount')}</div>
- <div class="text-xl">
- {f(fiat, currency)}
- <span class="text-secondary">⚡️{`${s(Math.abs(amount) + fee)}`}</span>
+ <span class="text-lg text-secondary">Amount</span>
+ <div>
+ {f(fiat(a, rate), currency)}
+ {#if tip}
+ <span class="text-lg">
+ + {f(fiat(tip, rate), currency)}
+ </span>
+ {/if}
+ <span class="text-secondary"
+ >⚡️{`${s(a)}`}
+
+ {#if tip}
+ <span class="text-lg">
+ + ⚡️{s(tip)}
+ </span>
+ {/if}
+ </span>
</div>
</div>
<div>
- <div class="font-bold">{$t('payments.date')}</div>
+ <span class="text-lg text-secondary">Network fee</span>
<div>
- {format(new Date(created), 'MMMM d')},
- {format(new Date(created), 'h:mm aaa')}
+ {f(fiat(fee, rate), currency)}
+ <span class="text-secondary">⚡️{`${s(fee)}`}</span>
</div>
</div>
- {#if type === 'lightning'}
+ {#if ourfee > 0}
<div>
- <div class="font-bold">{$t('payments.preimage')}</div>
+ <span class="text-lg text-secondary">Platform fee</span>
<div>
- {ref}
+ {f(fiat(ourfee, rate), currency)}
+ <span class="text-secondary">⚡️{`${s(ourfee)}`}</span>
</div>
</div>
- {:else}
+ {/if}
+
+ <div>
+ <span class="text-lg text-secondary">Date</span>
+ <div>
+ {format(new Date(created), 'MMMM d')},
+ {format(new Date(created), 'h:mm aaa')}
+ </div>
+ </div>
+
+ {#if type === 'bitcoin'}
<div>
- <div class="font-bold">{$t('payments.id')}</div>
+ <span class="text-lg text-secondary">Txid</span>
<div class="flex">
<div>
- <a href={`${expl}/tx/${id}`} target="_blank" rel="noreferrer" class="text-blue-600"
- >{id}</a
- >
+ <a href={`${expl}/tx/${id}`} target="_blank" rel="noreferrer">{id}</a>
</div>
- <button class="flex font-bold hover:opacity-80 mb-auto" on:click={() => copy(id)}
- ><Icon icon="copy" style="mr-1" />
+ <button class="flex font-bold hover:opacity-80 mb-auto my-auto" on:click={() => copy(id)}
+ ><Icon icon="copy" style="ml-2 w-20 my-auto" />
</button>
</div>
</div>
diff --git a/src/routes/(app)/pot/[name]/withdraw/+page.server.js b/src/routes/(app)/pot/[name]/withdraw/+page.server.js
index ab56867d..ac33e940 100644
--- a/src/routes/(app)/pot/[name]/withdraw/+page.server.js
+++ b/src/routes/(app)/pot/[name]/withdraw/+page.server.js
@@ -1,5 +1,5 @@
import { fail, redirect } from '@sveltejs/kit';
-import { auth, post } from '$lib/utils';
+import { fd, auth, post } from '$lib/utils';
export async function load({ parent }) {
let { user } = await parent();
@@ -8,8 +8,7 @@ export async function load({ parent }) {
export const actions = {
default: async ({ cookies, request }) => {
- let body = Object.fromEntries(await request.formData());
-
+ let body = await fd(request);
try {
await post('/take', body, auth(cookies));
} catch (e) {
diff --git a/src/routes/(app)/register/+page.server.js b/src/routes/(app)/register/+page.server.js
index 7799d5ae..e71b2a60 100644
--- a/src/routes/(app)/register/+page.server.js
+++ b/src/routes/(app)/register/+page.server.js
@@ -1,5 +1,5 @@
import { fail, redirect } from '@sveltejs/kit';
-import { post, login } from '$lib/utils';
+import { fd, post, login } from '$lib/utils';
export const load = async ({ parent }) => {
let { user } = await parent();
@@ -8,7 +8,7 @@ export const load = async ({ parent }) => {
export const actions = {
default: async ({ cookies, request }) => {
- let form = Object.fromEntries(await request.formData());
+ let form = await fd(request);
let { username, password, cipher, salt, pubkey, loginRedirect } = form;
let user = { username, password, cipher, salt, pubkey };
let error;
diff --git a/src/routes/(app)/send/[...text]/+page.server.js b/src/routes/(app)/send/[...text]/+page.server.js
index d09b4da9..9c7d4185 100644
--- a/src/routes/(app)/send/[...text]/+page.server.js
+++ b/src/routes/(app)/send/[...text]/+page.server.js
@@ -45,14 +45,15 @@ let parse = async (t, host) => {
// bitcoin
if (validate(t)) {
try {
- ({ id } = await get(`/invoice/${t}`));
+ ({ id, user } = await get(`/invoice/${t}`));
} catch (e) {
let r = `/send/bitcoin/${t}`;
if (amount) r += '/' + amount;
throw redirect(307, r);
}
- if (id) throw redirect(307, `/send/${id}`);
+ if (user) throw redirect(307, `/send/${user.username}`);
+ else if (id) throw redirect(307, `/send/${id}`);
}
// user
diff --git a/src/routes/(app)/send/[...text]/+page.svelte b/src/routes/(app)/send/[...text]/+page.svelte
index 5a376f9f..3abc5a1a 100644
--- a/src/routes/(app)/send/[...text]/+page.svelte
+++ b/src/routes/(app)/send/[...text]/+page.svelte
@@ -7,7 +7,6 @@
import { t } from '$lib/translations';
import { AppHeader, Avatar, Icon, Spinner } from '$comp';
import { back, fail } from '$lib/utils';
- import { format, parseISO } from 'date-fns';
export let data;
export let form;
@@ -99,7 +98,7 @@
<div>
{#each contacts as c}
<a href={`/send/${c.username}`}>
- <div class="border-b p-2 last:border-b-0 hover:bg-gray-100">
+ <div class="border-b p-2 last:border-b-0 hover:bg-gray-100 rounded-2xl">
<div class="flex">
<div>
<div class="flex">
diff --git a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js
index 59e9326d..92a754ae 100644
--- a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js
+++ b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.server.js
@@ -1,23 +1,29 @@
import { fail, redirect } from '@sveltejs/kit';
-import { auth, post } from '$lib/utils';
+import { auth, post, fd } from '$lib/utils';
let seen = {};
export const actions = {
default: async ({ cookies, request }) => {
try {
- let feeRate, subtract;
- let { address, amount, pin, ts } = Object.fromEntries(await request.formData());
-
+ let { address, amount, confirmed, feeRate, subtract, pin, stale, ts } = await await fd(
+ request
+ );
if (seen[ts]) return fail(400, { message: 'Duplicate form submission' });
- seen[ts] = 1;
- setTimeout(() => delete seen[ts], 30000);
- let { tx } = await post(
+ let estimate = await post(
'/bitcoin/fee',
{ address, amount, feeRate, subtract },
auth(cookies)
);
- await post('/bitcoin/send', { pin, tx }, auth(cookies));
+
+ let { tx } = estimate;
+
+ if (stale || !confirmed) return fail(400, estimate);
+
+ seen[ts] = 1;
+ setTimeout(() => delete seen[ts], 30000);
+
+ await post('/bitcoin/send', { pin, subtract, tx }, auth(cookies));
} catch (e) {
return fail(400, { message: e.message });
}
diff --git a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte
index 6b216cb3..77d5fde6 100644
--- a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte
+++ b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte
@@ -1,24 +1,48 @@
<script>
+ import { tick } from 'svelte';
import { t } from '$lib/translations';
import { enhance } from '$app/forms';
- import { Icon, Numpad, Spinner } from '$comp';
+ import { Toggle, Slider, Icon, Numpad, Spinner } from '$comp';
import { page } from '$app/stores';
- import { back } from '$lib/utils';
- import { pin } from '$lib/store';
+ import { fiat as toFiat, f, back, s, sat } from '$lib/utils';
+ import { pin, selectedRate } from '$lib/store';
export let data;
export let form;
let { address, amount } = $page.params;
- let { currency } = data.user;
- let loading, submit;
+ let { balance, currency } = data.user;
+ let loading, submit, fiat, confirmed, feeRate, min, max, fee, ourfee, stale, subtract, adjusting;
let toggle = () => (loading = true);
+ let toggleAdjusting = () => (adjusting = !adjusting);
$: update(form);
let update = () => {
- if (form?.message.includes('pin')) $pin = undefined;
loading = false;
+ if (form?.message?.includes('pin')) $pin = undefined;
+ if (form) ({ min, max, feeRate, fee, ourfee, subtract } = form);
+ if (!ourfee) ourfee = 0;
+ if (feeRate) confirmed = true;
+ stale = false;
+ };
+
+ let setMax = () => {
+ fiat = false;
+ amount = balance;
+ subtract = true;
+ };
+
+ $: if (amount + fee > balance) subtract = true;
+
+ let timeout;
+ let handle = (e) => {
+ stale = true;
+ feeRate = e.target.value;
+ clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ tick().then(() => submit.click());
+ }, 200);
};
</script>
@@ -32,30 +56,82 @@
</div>
{/if}
-<div class="container px-4 mt-20 max-w-xl mx-auto">
- <div class="text-center mb-8">
- <h1 class="text-3xl md:text-4xl font-semibold mb-2">{$t('payments.sendTo')}</h1>
- <p class="text-lg text-secondary">{address}</p>
- </div>
- <Numpad bind:amount {currency} {submit} />
+<div class="container px-4 mt-20 max-w-xl mx-auto space-y-5 text-center">
+ <h1 class="text-3xl md:text-4xl font-semibold mb-2">{$t('payments.send')}</h1>
+
+ <div class="text-lg text-secondary break-all">{address}</div>
+
+ {#if confirmed}
+ <div class="text-center">
+ <h2 class="text-2xl md:text-3xl font-semibold">
+ {f(toFiat(amount, $selectedRate), currency)}
+ </h2>
+ <h3 class="text-secondary md:text-lg mb-6 mt-1">⚡️{s(amount)}</h3>
+ </div>
+
+ <div class="text-center">
+ <h2 class="text-secondary mb-5 text-lg">with fee</h2>
+
+ <div class="relative">
+ <h2 class="text-xl font-semibold">
+ {f(toFiat(fee + ourfee, $selectedRate), currency)}
+ </h2>
+ <h3 class="text-secondary mb-6 mt-1">⚡️{s(fee + ourfee)}</h3>
+ <button class="absolute right-1/4 top-0 text-secondary p-4" on:click={toggleAdjusting}
+ ><Icon icon="settings" style="opacity-30" /></button
+ >
+ </div>
+ </div>
+ {:else}
+ <Numpad bind:amount bind:fiat {currency} {submit} />
+ {/if}
<form method="POST" use:enhance on:submit={toggle}>
<input name="ts" value={Date.now()} type="hidden" />
<input name="address" value={address} type="hidden" />
<input name="amount" value={amount} type="hidden" />
<input name="pin" value={$pin} type="hidden" />
+ <input name="confirmed" value={confirmed} type="hidden" />
+ <input name="feeRate" value={feeRate} type="hidden" />
+ <input name="stale" value={stale} type="hidden" />
+ <input name="subtract" value={subtract} type="hidden" />
+
+ {#if confirmed && adjusting}
+ <div class="relative mt-10 mb-20">
+ <div class="absolute top-5 left-0 text-secondary">Slower (~40 blocks)</div>
+ <div class="absolute top-5 right-0 text-secondary">Faster (~1 block)</div>
+ <Slider bind:value={feeRate} {handle} {min} {max} />
+ </div>
+ <div class="flex justify-center gap-2 mb-10">
+ <div class="my-auto">
+ <Toggle id="subtract" checked={subtract} bind:value={subtract} />
+ </div>
+ <label for="subtract" class="text-secondary text-lg my-auto mb-2"
+ >Take fee from amount</label
+ >
+ </div>
+ {/if}
+
+ <div class="flex justify-center gap-2">
+ {#if !confirmed}
+ <button
+ type="button"
+ class="hover:opacity-80 bg-black text-white rounded-2xl py-3 px-4 mt-2 border font-bold"
+ on:click|preventDefault={setMax}
+ on:keydown={setMax}>Max ⚡️{s(balance)}</button
+ >
+ {/if}
- <div class="flex w-full">
<button
bind:this={submit}
type="submit"
- class="opacity-100 hover:opacity-80'} rounded-2xl border py-3 font-bold mx-auto mt-2 bg-black text-white px-4 w-24"
+ class="opacity-100 hover:opacity-80 rounded-2xl border py-3 font-bold mt-2 bg-black text-white px-4 w-24"
disabled={loading}
>
- {#if loading}
- <Spinner />
- {:else}
+ {#if confirmed}
{$t('payments.send')}
+ {:else}
+ {$t('payments.next')}
{/if}
</button>
</div>
diff --git a/src/routes/(app)/send/invoice/[id]/+page.server.js b/src/routes/(app)/send/invoice/[id]/+page.server.js
index 2df19985..3db03123 100644
--- a/src/routes/(app)/send/invoice/[id]/+page.server.js
+++ b/src/routes/(app)/send/invoice/[id]/+page.server.js
@@ -1,5 +1,5 @@
import { error, fail, redirect } from '@sveltejs/kit';
-import { btc as asset, auth, get, post } from '$lib/utils';
+import { fd, auth, get, post } from '$lib/utils';
export async function load({ params: { id }, parent }) {
let { user } = await parent();
@@ -13,15 +13,10 @@ export async function load({ params: { id }, parent }) {
export const actions = {
default: async ({ cookies, params: { hash }, request }) => {
try {
- let body = Object.fromEntries(await request.formData());
- let { confirmed } = body;
+ let body = await fd(request);
body.amount = parseInt(body.amount);
body.hash = hash;
- if (!confirmed) {
- return fail(400, { amount, confirm: true });
- }
-
await post('/payments', body, auth(cookies));
} catch (e) {
return fail(400, { message: e.message });
diff --git a/src/routes/(app)/send/invoice/[id]/+page.svelte b/src/routes/(app)/send/invoice/[id]/+page.svelte
index 27837bca..7570a61a 100644
--- a/src/routes/(app)/send/invoice/[id]/+page.svelte
+++ b/src/routes/(app)/send/invoice/[id]/+page.svelte
@@ -69,7 +69,6 @@
<input name="amount" value={amount} type="hidden" />
<input name="payreq" value={payreq} type="hidden" />
<input name="username" value={recipient.username} type="hidden" />
- <input name="confirmed" value={form?.confirm} type="hidden" />
<input name="pin" value={$pin} type="hidden" />
<div class="flex w-full">
diff --git a/src/routes/(app)/send/lightning/[payreq]/+page.server.js b/src/routes/(app)/send/lightning/[payreq]/+page.server.js
index 90e6c0cc..94231376 100644
--- a/src/routes/(app)/send/lightning/[payreq]/+page.server.js
+++ b/src/routes/(app)/send/lightning/[payreq]/+page.server.js
@@ -1,23 +1,17 @@
import { fail, redirect } from '@sveltejs/kit';
-import { btc as asset, auth, post } from '$lib/utils';
+import { fd, auth, post } from '$lib/utils';
export async function load({ params }) {
return post('/parse', params);
}
export const actions = {
- setAmount: async ({ cookies, request }) => {
- return Object.fromEntries(await request.formData());
- },
+ setAmount: async ({ cookies, request }) => fd(request),
send: async ({ cookies, request }) => {
try {
- let body = Object.fromEntries(await request.formData());
- let { amount, confirmed } = body;
-
- if (!confirmed) {
- return fail(400, { amount, confirm: true });
- }
+ let body = await fd(request);
+ let { amount } = body;
await post('/payments', body, auth(cookies));
} catch (e) {
diff --git a/src/routes/(app)/send/lightning/[payreq]/+page.svelte b/src/routes/(app)/send/lightning/[payreq]/+page.svelte
index 6ae699d2..af07a565 100644
--- a/src/routes/(app)/send/lightning/[payreq]/+page.svelte
+++ b/src/routes/(app)/send/lightning/[payreq]/+page.svelte
@@ -13,10 +13,11 @@
let { alias } = data;
let { currency } = data.user;
- let amount = form?.amount || data.amount;
let a;
let maxfee = 100;
+ $: amount = form?.amount || data.amount;
+
let loading;
let submit = () => (loading = true);
$: update(form);
@@ -53,7 +54,6 @@
<form method="POST" use:enhance on:submit={submit} action="?/send" class="space-y-5">
<input name="payreq" value={payreq} type="hidden" />
<input name="amount" value={amount} type="hidden" />
- <input name="confirmed" value={form?.confirm} type="hidden" />
<input name="pin" value={$pin} type="hidden" />
<input name="maxfee" value={maxfee} type="hidden" />
@@ -89,7 +89,7 @@
</div>
{/if}
{:else}
- <form method="POST" action="?/setAmount" class="w-[300px] mx-auto">
+ <form method="POST" action="?/setAmount" class="w-[300px] mx-auto" use:enhance>
<input type="hidden" value={a} name="amount" />
<Numpad bind:amount={a} {currency} />
<button
diff --git a/src/routes/(app)/send/pot/[name]/[...amount]/+page.server.js b/src/routes/(app)/send/pot/[name]/[...amount]/+page.server.js
index 0e1b7f5b..6c11811c 100644
--- a/src/routes/(app)/send/pot/[name]/[...amount]/+page.server.js
+++ b/src/routes/(app)/send/pot/[name]/[...amount]/+page.server.js
@@ -3,7 +3,7 @@ import { auth, post } from '$lib/utils';
export const actions = {
default: async ({ cookies, request }) => {
- let body = Object.fromEntries(await request.formData());
+ let body = await fd(request);
let p;
try {
diff --git a/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte b/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte
index bb11ddde..da761cc2 100644
--- a/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte
+++ b/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte
@@ -16,7 +16,7 @@
let submit = () => (loading = true);
$: update(form);
let update = () => {
- if (form?.message.includes('pin')) $pin = undefined;
+ if (form?.message?.includes('pin')) $pin = undefined;
loading = false;
};
</script>
diff --git a/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js b/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js
index 42b47c8f..18eda839 100644
--- a/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js
+++ b/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js
@@ -1,5 +1,5 @@
import { error, fail, redirect } from '@sveltejs/kit';
-import { btc as asset, auth, get, post } from '$lib/utils';
+import { fd, auth, get, post } from '$lib/utils';
export async function load({ params, parent }) {
let { user } = await parent();
@@ -12,14 +12,10 @@ export async function load({ params, parent }) {
export const actions = {
default: async ({ cookies, request }) => {
try {
- let body = Object.fromEntries(await request.formData());
- let { amount, confirmed } = body;
+ let body = await fd(request);
+ let { amount } = body;
amount = parseInt(amount);
- if (!confirmed) {
- return fail(400, { amount, confirm: true });
- }
-
await post('/payments', body, auth(cookies));
} catch (e) {
console.log(e);
diff --git a/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte b/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte
index 9704e3c6..74da1fa8 100644
--- a/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte
+++ b/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte
@@ -16,10 +16,11 @@
let amount = form?.amount || data.amount;
let a,
af,
- amountFiat = amount * ($selectedRate / sats),
fiat = !amount,
hash;
+ $: amountFiat = amount * ($selectedRate / sats);
+
let setAmount = async () => {
amount = a;
amountFiat = af;
@@ -39,7 +40,7 @@
$: update(form);
let update = () => {
- if (form?.message.includes('pin')) $pin = undefined;
+ if (form?.message?.includes('pin')) $pin = undefined;
loading = false;
};
</script>
@@ -68,7 +69,6 @@
<form method="POST" use:enhance on:submit={submit}>
<input name="amount" value={amount} type="hidden" />
<input name="username" value={subject.username} type="hidden" />
- <input name="confirmed" value={form?.confirm} type="hidden" />
<input name="pin" value={$pin} type="hidden" />
<input name="hash" value={hash} type="hidden" />
diff --git a/src/routes/(app)/sent/+page.svelte b/src/routes/(app)/sent/+page.svelte
index 79a0aa6e..760d2e1d 100644
--- a/src/routes/(app)/sent/+page.svelte
+++ b/src/routes/(app)/sent/+page.svelte
@@ -1,5 +1,5 @@
<script>
- import { f, s, sat, sats } from '$lib/utils';
+ import { fiat, f, s, sat, sats } from '$lib/utils';
import { scale } from 'svelte/transition';
import { Icon } from '$comp';
import { toast } from '@zerodevx/svelte-toast';
@@ -7,13 +7,12 @@
export let data;
let { payments, user } = data;
+ let { currency, username } = user;
let payment = payments[0];
- let { amount, rate } = payment;
+ let { amount, rate, tip } = payment;
amount = Math.abs(amount);
- let fiat = (amount * rate) / sats;
-
toast.pop(0);
</script>
@@ -23,12 +22,25 @@
</div>
<h1 class="text-3xl md:text-4xl font-bold mb-6">Sent!</h1>
<h2 class="text-2xl md:text-3xl font-semibold">
- {f(fiat, user.currency)}
+ {f(fiat(amount, rate), currency)}
+ {#if tip}
+ <span class="text-lg">
+ + {f(fiat(tip, rate), currency)}
+ </span>
+ {/if}
</h2>
- <h3 class="text-secondary md:text-lg mb-6 mt-1">{sat(amount)}</h3>
+ <h3 class="text-secondary md:text-lg mb-6 mt-1">
+ ⚡️{s(amount)}
+
+ {#if tip}
+ <span class="text-lg">
+ + ⚡️{s(tip)}
+ </span>
+ {/if}
+ </h3>
</div>
-<a href={`/${user.username}/payments`}>
+<a href={`/${username}/payments`}>
<div class="opacity-0 w-screen h-screen fixed top-0 left-0 z-50" />
</a>
diff --git a/src/routes/(app)/support/+page.server.js b/src/routes/(app)/support/+page.server.js
deleted file mode 100644
index 5d3bf988..00000000
--- a/src/routes/(app)/support/+page.server.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { post } from '$lib/utils';
-import { fail } from '@sveltejs/kit';
-
-export const actions = {
- default: async ({ cookies, request }) => {
- let form = Object.fromEntries(await request.formData());
-
- try {
- await post('/email', form);
- } catch (e) {
- return fail(400, { error: e.message });
- }
-
- return { success: true };
- }
-};
diff --git a/src/routes/(app)/support/+page.svelte b/src/routes/(app)/support/+page.svelte
index d00cc069..8dbfc1af 100644
--- a/src/routes/(app)/support/+page.svelte
+++ b/src/routes/(app)/support/+page.svelte
@@ -1,10 +1,10 @@
<script>
- import { enhance } from '$app/forms';
+ import { fail, success, post } from '$lib/utils';
import { scale } from 'svelte/transition';
import { AppHeader, Icon } from '$comp';
import { t } from '$lib/translations';
- export let data, form;
+ export let data;
let { user } = data;
$: if (user) {
@@ -13,41 +13,58 @@
let email;
let message;
+ let sent;
+
+ let submit = (e) => {
+ e.preventDefault();
+ grecaptcha.ready(() => {
+ grecaptcha
+ .execute('6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM', { action: 'submit' })
+ .then((token) =>
+ post('/support', { email, message, token })
+ .then(() => (sent = true))
+ .catch(() => fail('problem submitting'))
+ );
+ });
+ };
</script>
+<svelte:head>
+ <script
+ src="https://www.google.com/recaptcha/api.js?render=6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM"
+ ></script>
+</svelte:head>
+
{#if user}
<AppHeader {data} />
{/if}
-{#if form?.error}
- <div class="text-red-600 text-center">
- {form.error}
- </div>
-{/if}
+<div class="container px-4 max-w-lg mx-auto mt-20 space-y-8">
+ {#if sent}
+ <h1 class="text-center text-3xl md:text-4xl font-semibold">Thank you!</h1>
+ <div class="text-center text-lg text-secondary">Someone will be in touch shortly.</div>
-<div class="container px-4 max-w-lg mx-auto mt-20 space-y-5">
- {#if form?.success}
- <h1 class="text-center text-3xl md:text-4xl font-semibold mb-8">Thank you!</h1>
- <p class="text-center mb-8">Someone will be in touch shortly.</p>
-
- <a href={user ? `/${user.username}` : '/'}>
- <button class="rounded-full border py-2 px-5 font-bold hover:opacity-80 w-full mb-2"
- >Done</button
- >
- </a>
+ <div>
+ <a href={user ? `/${user.username}` : '/'}>
+ <button
+ class="rounded-full border py-2 px-5 font-bold hover:opacity-80 w-full bg-black text-white"
+ >Ok</button
+ >
+ </a>
+ </div>
{:else}
<h1 class="text-center text-3xl md:text-4xl font-semibold mb-8">
{$t('user.support.header')}
</h1>
- <p class="text-secondary">
+ <p class="text-secondary text-lg">
Fill out this form or email us directly at <a
class="underline"
> and we'll do our best to get back to you in a timely manner.
</p>
- <form method="POST" use:enhance>
+ <form on:submit={submit}>
<div class="mb-4">
<label for="account" class="font-semibold">{$t('user.support.accountName')}</label>
<input
@@ -79,7 +96,7 @@
<button
type="submit"
disabled={!email || !message}
- class="{!email || !message
+ class="g-recaptcha {!email || !message
? 'opacity-50'
: 'opacity-100'} bg-black text-white font-bold rounded-xl py-3 w-full mx-auto {email &&
message
diff --git a/src/routes/(app)/support/+server.js b/src/routes/(app)/support/+server.js
new file mode 100644
index 00000000..6b1778a8
--- /dev/null
+++ b/src/routes/(app)/support/+server.js
@@ -0,0 +1,9 @@
+import { json } from '@sveltejs/kit';
+import { auth, post } from '$lib/utils';
+
+export const prerender = false;
+
+export async function POST({ cookies, request }) {
+ let result = await post('/email', await request.json(), auth(cookies));
+ return json(result);
+}
diff --git a/src/routes/(static)/+layout.server.js b/src/routes/(static)/+layout.server.js
new file mode 100644
index 00000000..2deeebc5
--- /dev/null
+++ b/src/routes/(static)/+layout.server.js
@@ -0,0 +1,16 @@
+import { get, post, auth, protectedRoutes } from '$lib/utils';
+
+export async function load({ cookies, request, url, params }) {
+ let token = cookies.get('token');
+ let rate,
+ rates = { USD: 1 };
+
+ let user;
+ if (token) {
+ try {
+ user = await get('/me', auth(cookies));
+ } catch (e) {}
+ }
+
+ return { user };
+}
diff --git a/src/routes/(static)/+layout.svelte b/src/routes/(static)/+layout.svelte
index 6e31f17e..3d596dff 100644
--- a/src/routes/(static)/+layout.svelte
+++ b/src/routes/(static)/+layout.svelte
@@ -1,8 +1,12 @@
<script>
+ import { SvelteToast } from '@zerodevx/svelte-toast';
import { Footer, LandingHeader } from '$comp';
+ export let data;
+ $: ({ user } = data);
</script>
-<LandingHeader />
+<SvelteToast options={{ reversed: true, intro: { y: 192 } }} />
+<LandingHeader {user} />
<main class="container flex px-10 md:px-40 mx-auto min-h-[600px] pb-20">
<slot />
</main>
diff --git a/src/routes/(static)/bounties/+page.svelte b/src/routes/(static)/bounties/+page.svelte
index 140b9c16..e3670e78 100644
--- a/src/routes/(static)/bounties/+page.svelte
+++ b/src/routes/(static)/bounties/+page.svelte
@@ -1,27 +1,30 @@
<div class="space-y-5">
- <h1 class="text-3xl mb-6 pt-20">Bounties</h1>
+ <h1 class="text-3xl mb-6 pt-28 md:pt-12">Bounties</h1>
- <p class="text-secondary">
+ <p>
We're offering <b>⚡️1,000,000</b> to the first <b>10 people</b> that help us onboard businesses
in the following categories to take bitcoin payments:
</p>
- <ul class="text-xl ml-20">
- <li>Grocery or Produce Store</li>
- <li>Cafe</li>
+ <ul>
+ <li>Grocery / Produce Store</li>
+ <li>
+ Cafe &mdash; Claimed by <a href="/reverendhodl">ReverendHodl</a> for
+ <a href="https://btcmap.org/map?lat=49.2814113&long=-123.049572">Laughing Bean Coffee Co.</a>
+ </li>
<li>Bakery</li>
<li>Butcher Shop</li>
- <li>Laundromat or Dry Cleaner</li>
- <li>Dollar Store, Book Store, or Gift Shop</li>
+ <li>Laundromat / Dry Cleaner</li>
+ <li>Dollar Store / Book Store / Gift Shop</li>
<li>Sushi Restaurant</li>
- <li>Pizza by the Slice Joint</li>
- <li>Liquor Store or Gas Station</li>
- <li>Bubble Tea or Ice Cream Parlour</li>
+ <li>Pizza by the Slice</li>
+ <li>Liquor Store / Gas Station</li>
+ <li>Bubble Tea / Ice Cream Parlour</li>
</ul>
- <h2 class="text-2xl">Terms and Conditions</h2>
+ <h2 class="text-xl">Terms and Conditions</h2>
- <ul class="ml-20">
+ <ul>
<li>The business must have a physical location that takes walk-in customers</li>
<li>
They must be located within Vancouver, North Vancouver, West Vancouver, Burnaby, New
@@ -51,7 +54,7 @@
<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"
+ <a href="https://btcmap.org/leaderboard" class="text-black hover:underline" target="_blank" rel="noreferrer"
>shadowy supertaggers</a
> to check up on them periodically.
</p>
@@ -61,7 +64,7 @@
applicants without cause
</p>
- <h2 class="text-2xl">Ready to begin!?</h2>
+ <h2 class="text-xl">Ready to begin!?</h2>
<p>
To make a referral, reach out on one of our socials or email us at <a
@@ -79,10 +82,7 @@
</div>
<style>
- p {
- @apply text-secondary;
- }
- b {
- @apply text-black text-xl;
+ a {
+ @apply font-bold;
}
</style>
diff --git a/src/routes/(static)/docs/+page.server.js b/src/routes/(static)/docs/+page.server.js
new file mode 100644
index 00000000..e80365fa
--- /dev/null
+++ b/src/routes/(static)/docs/+page.server.js
@@ -0,0 +1,4 @@
+export let load = async ({ cookies }) => {
+ let token = cookies.get('token');
+ return { token };
+};
diff --git a/src/routes/(static)/docs/+page.svelte b/src/routes/(static)/docs/+page.svelte
index c634cd76..cb82bd05 100644
--- a/src/routes/(static)/docs/+page.svelte
+++ b/src/routes/(static)/docs/+page.svelte
@@ -1,17 +1,77 @@
-Coming soon
-<!-- <h1>API Examples</h1> -->
-<!-- <pre><code class="whitespace-pre-wrap" -->
-<!-- >{` -->
-<!-- api="https://coinos.io/api" -->
-<!-- json="Content-Type: application/json" -->
-<!-- -->
-<!-- username=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) -->
-<!-- password="pw" -->
-<!-- -->
-<!-- echo Create user $username with password $password -->
-<!-- curl -s "$api/register" -H "$json" -d '{"user": {"username": "'$username'", "password": "'$password'", "confirm": "'$password'"}}' > /dev/null -->
-<!-- echo "" -->
-<!-- -->
+<script>
+ import { copy } from '$lib/utils';
+ import { Code, Icon } from '$comp';
+ import { PUBLIC_COINOS_URL } from '$env/static/public';
+ export let data;
+ let { token } = data;
+ let api = PUBLIC_COINOS_URL + '/api';
+
+ if (!token) token = '<login to see your token here>';
+ let tokenSample = `export token="${token}"`;
+</script>
+
+<div class="space-y-8 lg:text-xl mt-20 w-full max-w-full">
+ <h1 class="text-4xl">API Documentation</h1>
+
+ <p class="text-secondary">
+ Coinos has a simple REST API that can be used to register accounts, fetch data, and make
+ payments
+ </p>
+
+ <p class="text-secondary">* = required parameter</p>
+
+ <h2 class="text-2xl">Base URL</h2>
+ <div class="bg-black text-white rounded-lg p-4 flex gap-4">
+ <div>{api}</div>
+ <button class="ml-auto my-auto invert opacity-90" on:click={() => copy(api)}
+ ><Icon icon="copy" style="w-10 max-w-none" /></button
+ >
+ </div>
+
+ <h2 class="text-2xl">Auth Token</h2>
+ <p class="text-secondary">This JWT authorizes you to use the API</p>
+ <div class="bg-black text-white rounded-lg p-4 flex gap-4">
+ <div class="w-full break-all">{tokenSample}</div>
+ <button class="ml-auto my-auto invert opacity-90" on:click={() => copy(tokenSample)}
+ ><Icon icon="copy" style="w-10 max-w-none" /></button
+ >
+ </div>
+
+ <h2 class="text-2xl">POST /register</h2>
+ <p class="text-secondary">Register a new user account with a username and password</p>
+ <Code sample="register" />
+
+ <h2 class="text-2xl">POST /login</h2>
+ <p class="text-secondary">Login to an account to get its auth token</p>
+ <Code sample="login" />
+
+ <h2 class="text-2xl">POST /invoice</h2>
+ <p class="text-secondary">Create an invoice.</p>
+ <div>
+ <div class="text-secondary">
+ <span class="font-mono font-bold">type</span>: bitcoin or lightning
+ </div>
+ <div class="text-secondary">
+ <span class="font-mono font-bold">amount</span>: amount in satoshis
+ </div>
+ <div class="text-secondary">
+ <span class="font-mono font-bold">webhook</span>: an endpoint to hit when the invoice is paid
+ </div>
+ <div class="text-secondary">
+ <span class="font-mono font-bold">secret</span>: a secret string for the webhook to check
+ </div>
+ </div>
+
+ hey
+ <Code sample="invoice" />
+
+ <h2 class="text-2xl">GET /invoice/:hash</h2>
+ <p class="text-secondary">
+ Fetch an invoice by passing a bitcoin address or lightning payment hash
+ </p>
+ <Code sample="fetchInvoice" />
+</div>
+
<!-- echo Login and get an API token -->
<!-- token=$(curl -s "$api/taboggan" -H "$json" -d '{"username": "'$username'", "password": "'$password'"}' | jq -r .token) -->
<!-- echo $token -->
@@ -73,5 +133,5 @@ Coming soon
<!-- -->
<!-- curl -s "$api/bitcoin/send" -H "$json" -H "$auth" -d '{"address": "bc1qqc9azhdjjpdxenrndt8cw4tw9del5pyrjn0wln", "tx": { "hex": "'$hex'", "fee": "'$fee'" }}' | jq -r '{ amount, fee, balance: (.account.balance)}' -->
<!-- echo "" -->
-<!-- `}</code -->
-<!-- ></pre> -->
+
+<!-- `} -->
diff --git a/src/routes/+page.server.js b/src/routes/+page.server.js
index 4bcc6089..7936e88b 100644
--- a/src/routes/+page.server.js
+++ b/src/routes/+page.server.js
@@ -1,6 +1,6 @@
import { get } from '$lib/utils';
-export const prerender = true;
+// export const prerender = true;
export let load = async () => {
let data = { locations: [] };