aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.html3
-rw-r--r--src/components/AppHeader.svelte3
-rw-r--r--src/components/Avatar.svelte3
-rw-r--r--src/components/Calendar.svelte14
-rw-r--r--src/components/Card.svelte14
-rw-r--r--src/components/Lock.svelte14
-rw-r--r--src/components/Pin.svelte35
-rw-r--r--src/components/Qr.svelte4
-rw-r--r--src/components/RegisterOrLogin.svelte197
-rw-r--r--src/components/Tip.svelte4
-rw-r--r--src/components/index.js4
-rw-r--r--src/hooks.server.js4
-rw-r--r--src/lib/socket.js9
-rw-r--r--src/lib/store.js14
-rw-r--r--src/lib/ticket.js31
-rw-r--r--src/lib/tickets.js52
-rw-r--r--src/lib/utils.js25
-rw-r--r--src/locales/en.json4
-rw-r--r--src/locales/es.json2
-rw-r--r--src/routes/+layout.server.js10
-rw-r--r--src/routes/+layout.svelte15
-rw-r--r--src/routes/[username]/+page.server.js1
-rw-r--r--src/routes/[username]/dashboard/+page.server.js1
-rw-r--r--src/routes/[username]/settings/+page.server.js4
-rw-r--r--src/routes/[username]/settings/+page.svelte41
-rw-r--r--src/routes/[username]/settings/_account.svelte35
-rw-r--r--src/routes/[username]/settings/_security.svelte109
-rw-r--r--src/routes/invoice/[id]/+page.svelte4
-rw-r--r--src/routes/invoice/[id]/paid/+page.svelte2
-rw-r--r--src/routes/launch/+page.server.js6
-rw-r--r--src/routes/launch/+page.svelte91
-rw-r--r--src/routes/launch/purchase/+page.server.js45
-rw-r--r--src/routes/launch/purchase/+page.svelte91
-rw-r--r--src/routes/launch/purchase/card/+page.server.js44
-rw-r--r--src/routes/launch/purchase/card/+page.svelte77
-rw-r--r--src/routes/launch/thanks/[asset]/+page.server.js1
-rw-r--r--src/routes/launch/thanks/[asset]/+page.svelte29
-rw-r--r--src/routes/launch/ticket/+page.server.js9
-rw-r--r--src/routes/launch/ticket/+page.svelte35
-rw-r--r--src/routes/login/+page.server.js10
-rw-r--r--src/routes/login/+page.svelte1
-rw-r--r--src/routes/register/+page.server.js4
-rw-r--r--src/routes/register/+page.svelte2
-rw-r--r--src/routes/scan/+page.svelte2
-rw-r--r--src/routes/send/+layout.svelte18
-rw-r--r--src/routes/send/[...text]/+page.server.js2
-rw-r--r--src/routes/send/[...text]/+page.svelte4
-rw-r--r--src/routes/send/[uuid=uuid]/+page.server.js15
-rw-r--r--src/routes/send/[uuid=uuid]/+page.svelte15
-rw-r--r--src/routes/send/bitcoin/[address]/+page.server.js13
-rw-r--r--src/routes/send/bitcoin/[address]/+page.svelte16
-rw-r--r--src/routes/send/lightning/[payreq]/+page.server.js16
-rw-r--r--src/routes/send/lightning/[payreq]/+page.svelte14
-rw-r--r--src/routes/send/user/[username]/[...amount]/+page.server.js15
-rw-r--r--src/routes/send/user/[username]/[...amount]/+page.svelte16
-rw-r--r--src/routes/user/+server.js1
56 files changed, 979 insertions, 271 deletions
diff --git a/src/app.html b/src/app.html
index d8545747..57ffbcce 100644
--- a/src/app.html
+++ b/src/app.html
@@ -4,7 +4,8 @@
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
- <meta name="description" content="Easy to use bitcoin point of sale app and web wallet." />
+ <meta name="description" content="An easy bitcoin web wallet" />
+ <meta name="keywords" content="open source bitcoin lightning web wallet" />
<meta property="og:image" content="/icons/logo.svg" />
<title>coinos</title>
%sveltekit.head%
diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte
index f9413efb..302a9c7a 100644
--- a/src/components/AppHeader.svelte
+++ b/src/components/AppHeader.svelte
@@ -7,7 +7,6 @@
import { page } from '$app/stores';
export let data;
- export let avatarPosition = 'left-[calc(50vw-64px)] md:left-[calc(15vw-48px)] bottom-[-64px]';
let { subject, user } = data;
$: update(data);
@@ -35,7 +34,7 @@
$tempProfileFiles && $tempProfileFiles.banner
? `url(${$tempProfileFiles.banner})`
: subject?.banner
- ? `url(/api/public/${subject.username}-banner.png)`
+ ? `url(/api/public/${subject.username}-banner.webp)`
: undefined;
</script>
diff --git a/src/components/Avatar.svelte b/src/components/Avatar.svelte
index ed1a499a..c7339bf5 100644
--- a/src/components/Avatar.svelte
+++ b/src/components/Avatar.svelte
@@ -2,7 +2,6 @@
import { colorTheme, tempProfileFiles } from '$lib/store';
import { Icon } from '$comp';
- export let avatarPosition;
export let user;
</script>
@@ -14,7 +13,7 @@
<img
src={$tempProfileFiles && $tempProfileFiles.profile
? $tempProfileFiles.profile
- : `/api/public/${user.username}-profile.png`}
+ : `/api/public/${user.username}-profile.webp`}
class="w-full h-full object-cover object-center overflow-hidden"
alt={user.username}
/>
diff --git a/src/components/Calendar.svelte b/src/components/Calendar.svelte
new file mode 100644
index 00000000..7504f48e
--- /dev/null
+++ b/src/components/Calendar.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="absolute bottom-0 left-0 -mb-0.5 transform translate-x-1/2 -translate-y-1/2 text-black peer-placeholder-shown:text-gray-300 h-6 w-6"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"
+ d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
+ />
+</svg>
diff --git a/src/components/Card.svelte b/src/components/Card.svelte
new file mode 100644
index 00000000..44ab635f
--- /dev/null
+++ b/src/components/Card.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="absolute bottom-0 left-0 -mb-0.5 transform translate-x-1/2 -translate-y-1/2 text-black peer-placeholder-shown:text-gray-300 h-6 w-6"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"
+ d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
+ />
+</svg>
diff --git a/src/components/Lock.svelte b/src/components/Lock.svelte
new file mode 100644
index 00000000..11475acf
--- /dev/null
+++ b/src/components/Lock.svelte
@@ -0,0 +1,14 @@
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ class="absolute bottom-0 left-0 -mb-0.5 transform translate-x-1/2 -translate-y-1/2 text-black peer-placeholder-shown:text-gray-300 h-6 w-6"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke="currentColor"
+>
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"
+ d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
+ />
+</svg>
diff --git a/src/components/Pin.svelte b/src/components/Pin.svelte
new file mode 100644
index 00000000..8f33a7f3
--- /dev/null
+++ b/src/components/Pin.svelte
@@ -0,0 +1,35 @@
+<script>
+ import { tick } from 'svelte';
+ import { pin } from '$lib/store';
+ import { Pincode, PincodeInput } from 'svelte-pincode';
+ let pinCode = [];
+ let pinInput;
+
+ $: pinInput && tick().then(pinInput && pinInput.focusFirstInput);
+ $: pinCode.length > 5 && ($pin = pinCode.join(''));
+</script>
+
+<div class="fixed inset-0 bg-gray-600 bg-opacity-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">Please enter your PIN</h1>
+ <div class="text-center">
+ <Pincode bind:code={pinCode} bind:this={pinInput}>
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ </Pincode>
+ </div>
+
+ <div class="w-full flex">
+ <button
+ type="submit"
+ class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto"
+ >
+ <div class="my-auto">Cancel</div>
+ </button>
+ </div>
+ </div>
+</div>
diff --git a/src/components/Qr.svelte b/src/components/Qr.svelte
index 0ae028b9..3870abe0 100644
--- a/src/components/Qr.svelte
+++ b/src/components/Qr.svelte
@@ -57,8 +57,8 @@
{#if mask}
<div class="absolute bg-white w-[300px] h-[300px] t-0 l-0 z-10" out:fade />
{/if}
-<div bind:this={qr} on:click={toggle} class:hidden={full} class="relative overflow-visible" />
-<div bind:this={fullQr} on:click={toggle} class:hidden={!full} />
+<div bind:this={qr} on:click={toggle} on:keydown={toggle} class:hidden={full} class="relative overflow-visible" />
+<div bind:this={fullQr} on:click={toggle} on:keydown={toggle} class:hidden={!full} />
<style>
div {
diff --git a/src/components/RegisterOrLogin.svelte b/src/components/RegisterOrLogin.svelte
index 0b954387..bf2d2084 100644
--- a/src/components/RegisterOrLogin.svelte
+++ b/src/components/RegisterOrLogin.svelte
@@ -1,18 +1,28 @@
<script>
+ import { tick } from 'svelte';
+ import { Pincode, PincodeInput } from 'svelte-pincode';
import { fly } from 'svelte/transition';
import { enhance } from '$app/forms';
import { Icon } from '$comp';
import { focus } from '$lib/utils';
- import { redirect } from '$lib/store';
+ import { loginRedirect } from '$lib/store';
import { t } from '$lib/translations';
+ let token, tokenInput;
+ let code = [];
+
+ $: need2fa = form?.message === '2fa';
+ $: if (need2fa) tokenInput && tick().then(tokenInput.focusFirstInput);
+
export let form;
export let pageID;
- let username,
- password,
- email,
- twofa = '';
+ let username, password, email, submit;
+
+ $: update(form);
+ let update = (form) => form && ({ username, password } = form);
+
+ $: token && token?.length === 6 && tick().then(() => submit.click());
let revealPassword = false;
@@ -31,101 +41,122 @@
<div class="flex justify-center items-center">
<div class="shadow-xl rounded-3xl p-10 pb-12 space-y-5 w-full mx-5 md:mx-0 md:w-[400px]">
- <h1 class="text-2xl font-bold text-center">{$t('login.' + pageID)}</h1>
+ {#if need2fa}
+ <h1 class="text-2xl font-bold text-center">Enter 2FA Code</h1>
- {#if form?.error}
- <div class="text-red-600 text-center" in:fly>
- {form.error}
- </div>
- {/if}
+ {#if form?.token}
+ <div class="text-red-600 text-center" in:fly>Invalid token, try again</div>
+ {/if}
+
+ <form class="space-y-5" {action} method="POST" use:enhance>
+ <input type="hidden" name="username" value={username} />
+ <input type="hidden" name="password" value={password} />
+ <input type="hidden" name="token" value={token} />
+
+ <div>
+ <Pincode bind:code bind:value={token} bind:this={tokenInput}>
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ </Pincode>
+ </div>
+ <button
+ bind:this={submit}
+ type="submit"
+ class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80"
+ >{$t('login.' + pageID)}
+ </button>
+ </form>
+ {:else}
+ <h1 class="text-2xl font-bold text-center">{$t('login.' + pageID)}</h1>
- <form class="space-y-5" {action} method="POST" use:enhance>
- {#if $redirect}
- <input type="hidden" name="redirect" value={$redirect} />
+ {#if form?.error}
+ <div class="text-red-600 text-center" in:fly>
+ {form.error}
+ </div>
{/if}
- <div>
- <label for="username" class="font-semibold">{$t('login.username')}</label>
- <input
- name="username"
- type="text"
- required
- class="bg-primary"
- bind:value={username}
- use:focus
- autocapitalize="none"
- />
- </div>
+ <form class="space-y-5" {action} method="POST" use:enhance>
+ {#if $loginRedirect}
+ <input type="hidden" name="loginRedirect" value={$loginRedirect} />
+ {/if}
- <div class="relative">
- <label for="password" class="block font-semibold">{$t('login.password')}</label>
- {#if revealPassword}
+ <div>
+ <label for="username" class="font-semibold">{$t('login.username')}</label>
<input
- name="password"
+ name="username"
type="text"
required
class="bg-primary"
- bind:value={password}
+ bind:value={username}
+ use:focus
autocapitalize="none"
/>
- {:else}
- <input
- name="password"
- type="password"
- required
- class="bg-primary"
- bind:value={password}
- autocapitalize="none"
- />
- {/if}
- <button
- type="button"
- on:click={() => (revealPassword = !revealPassword)}
- class="absolute right-5 top-10"
- >
- <Icon icon={revealPassword ? 'eye' : 'eye-off'} />
- </button>
- </div>
+ </div>
- {#if pageID === 'signIn'}
- <div>
- <label for="token" class="font-semibold"
- >{$t('login.token')}
- <span class="text-secondary">({$t('login.optional')})</span></label
+ <div class="relative">
+ <label for="password" class="block font-semibold">{$t('login.password')}</label>
+ {#if revealPassword}
+ <input
+ name="password"
+ type="text"
+ required
+ class="bg-primary"
+ bind:value={password}
+ autocapitalize="none"
+ />
+ {:else}
+ <input
+ name="password"
+ type="password"
+ required
+ class="bg-primary"
+ bind:value={password}
+ autocapitalize="none"
+ />
+ {/if}
+ <button
+ type="button"
+ on:click={() => (revealPassword = !revealPassword)}
+ class="absolute right-5 top-10"
>
- <input name="token" type="text" class="bg-primary" bind:value={twofa} />
+ <Icon icon={revealPassword ? 'eye' : 'eye-off'} />
+ </button>
</div>
- {/if}
- {#if pageID === 'register'}
- <p class="text-secondary text-sm">
- {$t('login.passwordRecommendation')}
- </p>
- {:else}
- <!-- TODO -->
- <!-- <div class="flex justify-end items-center"> -->
- <!-- <a href="/forgot" class="underline underline-offset-4 text-black text-sm" -->
- <!-- >{$t('login.forgotUserOrPassword')}</a -->
- <!-- > -->
- <!-- </div> -->
- {/if}
+ {#if pageID === 'register'}
+ <p class="text-secondary text-sm">
+ {$t('login.passwordRecommendation')}
+ </p>
+ {:else}
+ <!-- TODO -->
+ <!-- <div class="flex justify-end items-center"> -->
+ <!-- <a href="/forgot" class="underline underline-offset-4 text-black text-sm" -->
+ <!-- >{$t('login.forgotUserOrPassword')}</a -->
+ <!-- > -->
+ <!-- </div> -->
+ {/if}
- <button
- type="submit"
- class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80"
- >{$t('login.' + pageID)}
- </button>
- </form>
+ <button
+ type="submit"
+ class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80"
+ >{$t('login.' + pageID)}
+ </button>
+ </form>
- <p class="text-secondary text-center font-medium">
- {pageID === 'register' ? $t('login.haveAccount') : $t('login.noAccount')}
- <a
- href={pageID === 'register' ? '/login' : '/register'}
- class="block md:inline text-black underline underline-offset-4 hover:opacity-80"
- >
- {pageID === 'register' ? $t('login.signIn') : $t('login.register')}
- </a>
- </p>
+ <p class="text-secondary text-center font-medium">
+ {pageID === 'register' ? $t('login.haveAccount') : $t('login.noAccount')}
+ <a
+ href={pageID === 'register' ? '/login' : '/register'}
+ class="block md:inline text-black underline underline-offset-4 hover:opacity-80"
+ >
+ {pageID === 'register' ? $t('login.signIn') : $t('login.register')}
+ </a>
+ </p>
+ {/if}
</div>
</div>
</div>
diff --git a/src/components/Tip.svelte b/src/components/Tip.svelte
index 25dbe3b9..6af1e00a 100644
--- a/src/components/Tip.svelte
+++ b/src/components/Tip.svelte
@@ -188,8 +188,4 @@
background: black;
cursor: pointer;
}
-
- #slider.slider-track::-moz-range-progress {
- background: black;
- }
</style>
diff --git a/src/components/index.js b/src/components/index.js
index 08ec217a..c3a5000c 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -1,3 +1,7 @@
+export { default as Pin } from './Pin.svelte';
+export { default as Card } from './Card.svelte';
+export { default as Lock } from './Lock.svelte';
+export { default as Calendar } from './Calendar.svelte';
export { default as Avatar } from './Avatar.svelte';
export { default as Left } from './Left.svelte';
export { default as Numpad } from './Numpad.svelte';
diff --git a/src/hooks.server.js b/src/hooks.server.js
index 58882771..a5b1eef3 100644
--- a/src/hooks.server.js
+++ b/src/hooks.server.js
@@ -2,9 +2,8 @@ import { auth, get } from '$lib/utils';
export async function handle({ event, resolve }) {
let token = event.cookies.get('token');
- if (token) {
- event.request.headers.set('authorization', `Bearer ${token}`);
+ if (token) {
try {
event.locals.user = await get('/me', auth(event.cookies));
} catch (e) {
@@ -13,7 +12,6 @@ export async function handle({ event, resolve }) {
}
const response = await resolve(event);
- response.headers.set('x-custom-header', 'potato');
return response;
}
diff --git a/src/lib/socket.js b/src/lib/socket.js
index 02d73a48..b385db9d 100644
--- a/src/lib/socket.js
+++ b/src/lib/socket.js
@@ -1,9 +1,10 @@
import { get } from 'svelte/store';
-import { invoices, newPayment, rate } from '$lib/store';
+import { invoices, newPayment, rate, user } from '$lib/store';
import { success } from '$lib/utils';
import { env } from '$env/dynamic/public';
const socketUrl = env.PUBLIC_SOCKET;
+const btc = env.PUBLIC_BTC;
let interval, socket, token;
@@ -18,12 +19,14 @@ export const messages = (data) => ({
rate.set(data);
},
- payment() {
+ async payment() {
let { amount, invoice } = data;
- console.log('INVOICE', invoice);
+ if (get(user).account_id !== data.account_id) return;
+
if (invoice) {
invoices.set({ ...get(invoices), [invoice.uuid]: invoice });
}
+
newPayment.set(true);
if (amount > 0) {
success(`Received ${amount} sats!`);
diff --git a/src/lib/store.js b/src/lib/store.js
index c64ad39f..3174a86c 100644
--- a/src/lib/store.js
+++ b/src/lib/store.js
@@ -1,20 +1,24 @@
import { get, writable } from 'svelte/store';
import { browser } from '$app/environment';
-// creates a Svelte store synced to sessionStorage with the given key and default value
-const persistSession = (key, defaultValue) => {
+const persistSession = (key, defaultValue, expiry) => {
+ if (expiry && browser && new Date() - expiry > sessionStorage.getItem(key + 'time')) {
+ sessionStorage.removeItem(key);
+ }
+
let s = writable(
browser && sessionStorage.getItem(key) && sessionStorage.getItem(key) !== 'undefined'
? JSON.parse(sessionStorage.getItem(key))
: defaultValue
);
+ expiry && browser && sessionStorage.setItem(key + 'time', Date.now());
+
s.subscribe((v) => browser && sessionStorage.setItem(key, JSON.stringify(v)));
return s;
};
-// creates a Svelte store synced to localStorage with the given key and default value
const persistLocal = (key, defaultValue) => {
let s = writable(
browser && localStorage.getItem(key) && localStorage.getItem(key) !== 'undefined'
@@ -27,8 +31,9 @@ const persistLocal = (key, defaultValue) => {
return s;
};
-export const redirect = writable();
+export const loginRedirect = writable();
export const rate = writable();
+export const user = writable();
export const selectedRate = writable();
export const token = persistSession('token');
export const invoices = writable({});
@@ -37,3 +42,4 @@ export const colorTheme = writable('from-primary to-gradient');
export const tempProfileFiles = writable();
export const avatarUpload = writable();
export const bannerUpload = writable();
+export const pin = persistSession('pin', undefined, 5 * 60 * 1000);
diff --git a/src/lib/ticket.js b/src/lib/ticket.js
new file mode 100644
index 00000000..708c10f4
--- /dev/null
+++ b/src/lib/ticket.js
@@ -0,0 +1,31 @@
+import { auth, get, post } from '$lib/utils';
+import { env } from '$env/dynamic/private';
+import tickets from '$lib/tickets';
+
+export const createTicket = async (cookies, username) => {
+ let { ticket } = await get('/ticket');
+
+ let { address } = await post(
+ '/invoice',
+ { invoice: { network: 'liquid' }, user: { username } },
+ auth(cookies)
+ );
+
+ let { asset } = await post(
+ '/assets',
+ {
+ address,
+ name: `Launch Party Ticket ${ticket}`,
+ ticker: 'LPT',
+ precision: 0,
+ asset_amount: '1',
+ token_amount: 0,
+ domain: 'coinos.io',
+ filename: tickets[ticket]
+ },
+ { authorization: `Bearer ${env.LAUNCH}` }
+ );
+
+ await post('/ticket', { asset }, { authorization: `Bearer ${env.LAUNCH}` });
+ return asset;
+};
diff --git a/src/lib/tickets.js b/src/lib/tickets.js
new file mode 100644
index 00000000..b0ca0ac0
--- /dev/null
+++ b/src/lib/tickets.js
@@ -0,0 +1,52 @@
+export default [
+ 'QmPbFkiXNuaQyfwD4W9GcXP7qWaNd6wFd9KCnJir5mes1J',
+ 'QmPVjvbaMxSJRpDxMLYTdUWQbknej1vYLckBQ2pAgxZgJN',
+ 'QmVasycSFzCYhyXrcvTE7mN3d6W78RFnrhzg5cRF6Dp6Zq',
+ 'QmTmnhjpTBjZdeHwXgT9Dftc3uWMW73qsZpZ5ECrTyiKKZ',
+ 'QmWDBiyVPyFkzUrEdvy7DSJe1rBQpene1m6quKAtyjBP6a',
+ 'QmcxxzQZwgG7SE9QbHNvoTiCi6Dhs9rrz9roqvxm2JQA88',
+ 'Qmd9ThEpXLLHimEzRUWEFrAGLSw5PAHzn2SmwfCnjNLwWt',
+ 'Qmc7pWw1yxc2ARBWitDMYiyPkWK61NsXXU8oYAQwNAvcx1',
+ 'QmX9Hgw1rMFUgBBuq8QfZZDzLjN5xCED19fwFdDeDDqwmr',
+ 'QmconCAyNyaagqKvvfKCbG7Eu8c6vCtkv2ivvxMoFN9YwA',
+ 'QmWbFmvViy5gx4JdD5sw5YJETet4ZQcXtYgijYnrsnSZ3a',
+ 'QmP6QiiJwoqkduZMqdd5e5htHZ4aYHCsvEu7WEEUknKYa5',
+ 'QmefHk8hvBUHsuKugntok5u75Q8i886cnPwsj4LZZB6uWg',
+ 'QmdrSDK5ByYHxc88hfDu8hQgK5XYFpXSfPorUXdyTbE437',
+ 'QmZ6ZycjRzYpsjnLm1Lfgk1fXXDdhcYRkyfmqYJZVGwADy',
+ 'QmSkBxS5yLPjB2VPs89oQRZANsoyodK6s8s52E8GKXr2zt',
+ 'QmSn8TCPwq3UZ1Z4eyko76CvQo2H2Hyef1NGboDQN22m5i',
+ 'QmP6CTH5zrKMzZwtjy7VaK9NXiq4Gw53kmuRjUbJjeovoe',
+ 'QmdwT4yvz9Xf8g3t6irKRoh3Wa7MwRQrcUJckQxyQe1Bc5',
+ 'QmcwuHH3bD5JExqjDvKqRQ3V5xEVGqs94qPNVDzaCqJa5a',
+ 'QmRfVduHwn8sXwSoyfgZpxSi8L7EjP4LMp6fmijo9VApDp',
+ 'Qmc4TGj1Ahbg9HJ9WDXDm2bXsNkT4zRDp8Wdw9wqqdEwuH',
+ 'QmY8uQiHMWA8ztf2wAurNFW7eiTHQEUiLTQ2vGceMwVqz3',
+ 'QmXiXbaD12F4mkbUSQcKhWshkPU4oi29cUawZGnPk86VVU',
+ 'QmZaiAGjvBZBw4suz33s9JQNzY2Kshean2mQQPCa6gKMQe',
+ 'Qme8bDSdzX7tanPYf672xXW8vPdkLkeSLqLzHJqidRXsYJ',
+ 'QmRJirjuUKgh2AMyBF4jNquaXHFPBtDJf3gKFC614hX7qu',
+ 'QmdRoSKSgabcrpekNBPB6T47RBtJnCETYE8ry2a3P37MUe',
+ 'QmXoqArruHSghrUGW35vVQHPknBG5Cj939iWWHHXU3B616',
+ 'QmYCxtDuoHLMAFhNpifUsLU9ptAeFKwyxSiSEJhDQLMZtN',
+ 'QmQZQBw8wgzUsrmVDEejdePK23AEJbk9aUracvxLPyHz2x',
+ 'QmSsbu7U3i62AQKuYqfisRo5E6nqdMZfHRKWYJgouaXDxD',
+ 'QmaP4adVQuWyqtN4QCXeYXj4nr19Je4EqsaTG1zM9ywqFS',
+ 'QmPPsvgfXsiN3wNqdAwHAhQVpw6SZWNpry9Na6R2Xb7zXY',
+ 'QmR7z6wQmcfZUvRsVK83GRRboyvwZEDQujLwumMY33nAit',
+ 'QmQXgkgorDLPUyQpFbW8aLUW6jioQjUKamtXw5UuNCttDg',
+ 'QmcwYBeXt1BPfdSrtSrXFpWctRZ7RDHvmKTttC5hPPTSzG',
+ 'QmTUexZnw3xPy3YVdUsqJALwTvy2qFax5b3QpJVMvTCJQb',
+ 'QmNLkYXFUvctpfeeDTZxruZE2f834EUnYMM6JJUeTcUB4J',
+ 'QmT7544UaTPgKFYMCpPE4unXphNGX71BLZBPKLkxAijSmj',
+ 'QmTpVR4dqX1QV4fae5pZ1ACtRcANHwEWCLwpMXFmwE95Sm',
+ 'Qmdfyps5fW8d8wstXMfpvpei2DTmaADBWgmxLT7e7uan4P',
+ 'QmUSK8byipYoG3H97QpmPrHw9vYrvHygyhkiM81XmaKyxS',
+ 'QmQUGRvp96LTw5i8Ly9f5LoMvUpeQHCLGNqC88KV74wEHK',
+ 'Qma693TFLh9Z1MeBFzhPHi6Yk4tmTcZY2jogwJ8cPkU2B4',
+ 'QmNu8FyMpmkFdbCXGRVG8mKJtMvvCoLP8LNzFsxBQwxQeS',
+ 'QmWZHE87PKujL1MGLuFgP3D12o5GQxYbvbeTRgr12f1XMu',
+ 'QmcZJn8gPGd3poS9yuiNJh6iP4cn9uy5fnypky71r4sSSw',
+ 'QmTwrAF4E9ucpdnrUZFj6EfrQamGv7Qyn7ceSx1DUQYceC',
+ 'QmexssxU5iTXypgNqrmrQwMDJnTNxyZUxor5wDLnwb5TZp'
+];
diff --git a/src/lib/utils.js b/src/lib/utils.js
index 54e965d9..7eeab648 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -9,6 +9,17 @@ export function scroll(section) {
const base = browser ? '' : env.PUBLIC_COINOS_URL;
+export const g = (url, fetch, headers) =>
+ fetch(base + url, { headers })
+ .then((r) => r.text())
+ .then((body) => {
+ try {
+ return JSON.parse(body);
+ } catch (e) {
+ throw new Error(body);
+ }
+ });
+
export const get = (url, headers = {}) =>
fetch(base + url, { headers })
.then((r) => r.text())
@@ -31,6 +42,8 @@ export const post = (url, body, headers) => {
throw new Error(body);
}
+
+ if (body.error) throw body.error;
if (body instanceof Error) throw body;
if (body.name === 'Error') throw new Error(body.message);
@@ -97,7 +110,17 @@ export const info = (m) => {
export const login = async (user, cookies) => {
let maxAge = 30 * 24 * 60 * 60;
- let { token } = await post('/login', user);
+ let res = await fetch(base + '/login', {
+ method: 'POST',
+ body: JSON.stringify(user),
+ headers: { 'content-type': 'application/json', accept: 'application/json' }
+ });
+ if (res.status === 401) {
+ let text = await res.text();
+ if (text.startsWith('2fa')) throw new Error('2fa');
+ }
+
+ let { token } = await res.json();
if (!token) throw new Error('Login failed');
let expires = new Date();
diff --git a/src/locales/en.json b/src/locales/en.json
index a6c94b58..b5aef47a 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -32,7 +32,7 @@
"bio": "Strategic & Project Management. Jack of all Trades. Can always find time to meet for coffee."
},
"cole": {
- "name": "Cole Goodwin",
+ "name": "Cole",
"position": "Bitcoin Professional",
"bio": "As a Certified Bitcoin Professional, Cole is a driven individual with a passion for freedom. He came across bitcoin in 2016 and studied it in depth. Now he wants to introduce the lightning network to as many users as possible."
},
@@ -152,7 +152,7 @@
"settings": "Settings",
"support": "Support"
},
- "newSiteHeader": "Welcome to the <a href=\"/news\" class=\"underline underline-offset-4 text-black hover:opacity-80\" >new and improved coinos</a>, focused on merchant adoption! Looking for the old coinos? Visit <a href=\"https://classic.coinos.io\" class=\"underline underline-offset-4 text-black hover:opacity-80\">classic.coinos.io</a>.",
+ "newSiteHeader": "Join us at our <a href=\"/launch\" class=\"underline underline-offset-4 text-black hover:opacity-80\">launch party</a> in Vancouver on October 26! Looking for the old coinos? Visit <a href=\"https://classic.coinos.io\" class=\"underline underline-offset-4 text-black hover:opacity-80\">classic.coinos.io</a>",
"news": {
"header": "News",
"subheader": "Latest Updates",
diff --git a/src/locales/es.json b/src/locales/es.json
index 5fad6dd8..068d93a5 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -32,7 +32,7 @@
"bio": "Gestión Estratégica y de Proyectos. Ella es una todóloga. Siempre encuentra tiempo para reunirse a tomar un café."
},
"cole": {
- "name": "Cole Goodwin",
+ "name": "Cole",
"position": "Profesional en Bitcoin",
"bio": "Como profesional certificado de Bitcoin, Cole es una persona motivada y apasionada por la libertad. Encontró bitcoin en el año 2016 bitcoin y se dedicó a estudiarlo en profundidad. Ahora quiere presentar Lightning Network a tantos usuarios como sea posible."
},
diff --git a/src/routes/+layout.server.js b/src/routes/+layout.server.js
index e110b467..be81e19f 100644
--- a/src/routes/+layout.server.js
+++ b/src/routes/+layout.server.js
@@ -1,12 +1,12 @@
import { error, redirect } from '@sveltejs/kit';
-import cookie from 'cookie';
import { protectedRoutes } from '$lib/utils';
import { get, post, auth } from '$lib/utils';
import { env } from '$env/dynamic/public';
export const { PUBLIC_BTC: btc } = env;
-export async function load({ cookies, request, url, params }) {
+export async function load({ cookies, locals, request, url, params }) {
+ url.pathname;
let token = cookies.get('token');
let user;
let rates;
@@ -17,11 +17,7 @@ export async function load({ cookies, request, url, params }) {
}
if (token && token !== 'undefined') {
- try {
- user = await get('/me', auth(cookies));
- } catch (e) {
- throw redirect(307, '/logout');
- }
+ user = locals.user;
if (protectedRoutes.find((p) => url.pathname.match(p))) {
if (user.username !== params.username) throw error(404, 'user not found');
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 42d249f4..5cc75b30 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -3,7 +3,7 @@
import '../app.css';
import { onMount } from 'svelte';
import { connect } from '$lib/socket';
- import { selectedRate, rate } from '$lib/store';
+ import { selectedRate, rate, user } from '$lib/store';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
@@ -14,12 +14,17 @@
const localeLocalStorageKey = 'sveltekit-i18n-locale';
export let data;
- let { user, rate: r, token, rates } = data;
+
+ let r, u, token, rates;
+
$: update(data);
- let update = () => ({ user, rate: r, token, rates } = data);
- $rate = r;
+ let update = () => {
+ ({ user: u, rate: r, token, rates } = data);
+ $rate = r;
+ $user = u;
+ };
- $: $selectedRate = user && $rate * (rates[user.currency] / rates.USD);
+ $: $selectedRate = u && r * (rates[u.currency] / rates.USD);
onMount(() => {
let localStorageLocale = localStorage.getItem(localeLocalStorageKey);
diff --git a/src/routes/[username]/+page.server.js b/src/routes/[username]/+page.server.js
index d6f2978c..0840161a 100644
--- a/src/routes/[username]/+page.server.js
+++ b/src/routes/[username]/+page.server.js
@@ -1,4 +1,3 @@
-import cookie from 'cookie';
import { get, post } from '$lib/utils';
export async function load({ params }) {
diff --git a/src/routes/[username]/dashboard/+page.server.js b/src/routes/[username]/dashboard/+page.server.js
new file mode 100644
index 00000000..b6206324
--- /dev/null
+++ b/src/routes/[username]/dashboard/+page.server.js
@@ -0,0 +1 @@
+export const load = ({ parent }) => parent();
diff --git a/src/routes/[username]/settings/+page.server.js b/src/routes/[username]/settings/+page.server.js
index 7053a939..ae7e0a78 100644
--- a/src/routes/[username]/settings/+page.server.js
+++ b/src/routes/[username]/settings/+page.server.js
@@ -3,9 +3,11 @@ import { auth, get, post } from '$lib/utils';
export const actions = {
default: async ({ cookies, request }) => {
+ let form = Object.fromEntries(await request.formData());
+
let user = {
...(await get('/me', auth(cookies))),
- ...Object.fromEntries(await request.formData())
+ ...form
};
try {
diff --git a/src/routes/[username]/settings/+page.svelte b/src/routes/[username]/settings/+page.svelte
index c1ac20d0..7bd4ddcc 100644
--- a/src/routes/[username]/settings/+page.svelte
+++ b/src/routes/[username]/settings/+page.svelte
@@ -1,21 +1,28 @@
<script>
+ import { fly } from 'svelte/transition';
import { enhance } from '$app/forms';
import Account from './_account.svelte';
import Pos from './_pos.svelte';
import Security from './_security.svelte';
- import { Icon } from '$comp';
+ import { Icon, Spinner, Pin } from '$comp';
import { t } from '$lib/translations';
import { failure, success } from '$lib/utils';
- import { avatarUpload, bannerUpload } from '$lib/store';
+ import { avatarUpload, bannerUpload, pin } from '$lib/store';
import { upload } from '$lib/upload';
export let data;
+ export let form;
let { user, rates, token } = data;
$: update(data);
let update = () => ({ user, rates, token } = data);
+ $: if (form?.message?.startsWith('Pin')) {
+ failure('Wrong pin, try again');
+ $pin = '';
+ }
+
let tab = 'account';
let tabs = [
@@ -26,23 +33,37 @@
$: ({ comp } = tabs.find((t) => t.name === tab));
- let save = async () => {
+ let loading, submit;
+ let save = async (msg) => {
+ loading = true;
+ if (typeof msg !== 'string') msg = 'Settings saved';
+
try {
if ($avatarUpload) {
await upload($avatarUpload.file, $avatarUpload.type, $avatarUpload.progress, token);
}
+
if ($bannerUpload) {
await upload($bannerUpload.file, $bannerUpload.type, $bannerUpload.progress, token);
}
- success('Settings saved');
+
+ success(msg);
} catch (e) {
console.log(e);
failure('Something went wrong');
}
+
+ loading = false;
};
</script>
+{#if user.haspin && $pin?.length !== 6}
+ <Pin />
+{/if}
+
<form method="POST" class="mb-[154px]" use:enhance on:submit={save}>
+ <input type="hidden" name="pin" value={$pin} />
+
<div class="mt-24 mb-20 px-3 md:px-0 w-full md:w-[400px] mx-auto space-y-8">
<h1 class="text-center text-3xl md:text-4xl font-semibold mb-10">
{$t('user.settings.header')}
@@ -58,16 +79,22 @@
{/each}
</div>
- <svelte:component this={comp} {user} {rates} />
+ <svelte:component this={comp} {user} {rates} {submit} />
</div>
<div
class="z-10 fixed bottom-0 bg-white shadow border-t w-full flex justify-center items-center py-3"
>
<button
+ bind:this={submit}
type="submit"
- class="bg-black text-white rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80"
+ class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80"
+ class:bg-black={loading}
>
- <div class="my-auto">Save Settings</div>
+ {#if loading}
+ <Spinner />
+ {:else}
+ <div class="my-auto">Save Settings</div>
+ {/if}
</button>
</div>
</form>
diff --git a/src/routes/[username]/settings/_account.svelte b/src/routes/[username]/settings/_account.svelte
index bb229b93..f139b3f4 100644
--- a/src/routes/[username]/settings/_account.svelte
+++ b/src/routes/[username]/settings/_account.svelte
@@ -17,6 +17,7 @@
$: banner = files.banner.src;
$: $tempProfileFiles = { profile: files.profile.src, banner: files.banner.src };
+ let togglePassword = () => (revealPassword = !revealPassword);
let colorThemes = [
{ theme: 1, color1: 'from-[#F5F7FA]', color2: 'to-[#C3CFE2]' },
{ theme: 2, color1: 'from-[#FDFCFB]', color2: 'to-[#E2D1C3]' },
@@ -36,9 +37,14 @@
percent = Math.round((event.loaded / event.total) * 100);
};
+ let tooLarge = {};
+
let files = { profile: {}, banner: {} };
let handleFile = async ({ target }, type) => {
+ tooLarge[type] = false;
let file = target.files[0];
+ console.log('TYPE', file.size);
+ if (file.size > 5000000) return (tooLarge[type] = true);
var reader = new FileReader();
reader.onload = async (e) => {
files[type].src = e.target.result;
@@ -83,7 +89,8 @@
{/if}
<button
type="button"
- on:click={() => (revealPassword = !revealPassword)}
+ on:click={togglePassword}
+ on:keydown={togglePassword}
class="absolute right-5 top-11"
>
<Icon icon={revealPassword ? 'eye' : 'eye-off'} />
@@ -118,11 +125,13 @@
<div>
<span class="font-bold">{$t('user.settings.profileImage')}</span>
+
<div class="flex">
{#if profile}
<div
class="relative rounded-full overflow-hidden text-center w-20 h-20 my-auto hover:opacity-80 cursor-pointer"
on:click={() => select('profile')}
+ on:keydown={() => select('profile')}
>
<img
src={profile}
@@ -134,9 +143,10 @@
<div
class="relative rounded-full overflow-hidden text-center w-20 h-20 my-auto hover:opacity-80 cursor-pointer"
on:click={() => select('profile')}
+ on:keydown={() => select('profile')}
>
<img
- src={`/api/public/${user.username}-profile.png`}
+ src={`/api/public/${user.username}-profile.webp`}
class="absolute w-full h-full object-cover object-center visible overflow-hidden"
alt={user.username}
/>
@@ -145,6 +155,7 @@
<div
class="rounded-full border-4 border-white p-4 bg-gradient-to-r {$colorTheme} w-24 my-auto hover:opacity-80 cursor-pointer"
on:click={() => select('profile')}
+ on:keydown={() => select('profile')}
>
<Icon icon="logo-symbol-white" style="mx-auto" />
</div>
@@ -154,7 +165,8 @@
<button
type="button"
class="border rounded-2xl font-bold w-24 text-center px-0 py-2 hover:opacity-80"
- on:click={() => select('profile')}>Select</button
+ on:click={() => select('profile')}
+ on:keydown={() => select('profile')}>Select</button
>
<input
type="file"
@@ -167,6 +179,10 @@
{/if}
</div>
</div>
+
+ {#if tooLarge['profile']}
+ <div class="text-red-600">Max file size 5MB</div>
+ {/if}
</div>
<div>
@@ -180,19 +196,22 @@
src={banner}
class="w-full object-cover object-center visible overflow-hidden h-48 mb-4 hover:opacity-80"
on:click={() => select('banner')}
+ on:keydown={() => select('banner')}
alt="Banner"
/>
{:else if user.banner}
<img
- src={`/api/public/${user.username}-banner.png`}
+ src={`/api/public/${user.username}-banner.webp`}
class="w-full object-cover object-center visible overflow-hidden h-48 mb-4 hover:opacity-80"
on:click={() => select('banner')}
+ on:keydown={() => select('banner')}
alt="Banner"
/>
{:else}
<div
class="bg-gradient-to-r {$colorTheme} w-full h-48 mb-4 cursor-pointer hover:opacity-80"
on:click={() => select('banner')}
+ on:keydown={() => select('banner')}
alt="Banner"
/>
{/if}
@@ -201,7 +220,8 @@
<button
type="button"
class="border rounded-2xl font-bold w-24 text-center px-0 py-2 hover:opacity-80"
- on:click={() => select('banner')}>Select</button
+ on:click={() => select('banner')}
+ on:keydown={() => select('banner')}>Select</button
>
<input
type="file"
@@ -212,6 +232,10 @@
{#if files.banner.filename}
<div class="mt-2 text-sm">{files.banner.filename}</div>
{/if}
+
+ {#if tooLarge['banner']}
+ <div class="text-red-600">Max file size 5MB</div>
+ {/if}
</div>
<!-- TODO
@@ -226,6 +250,7 @@
? 'border-black'
: ''}"
on:click={() => handleThemeClick(colors)}
+ on:keydown={() => handleThemeClick(colors)}
/>
{/each}
</div> -->
diff --git a/src/routes/[username]/settings/_security.svelte b/src/routes/[username]/settings/_security.svelte
index 2a490aa4..98b9b503 100644
--- a/src/routes/[username]/settings/_security.svelte
+++ b/src/routes/[username]/settings/_security.svelte
@@ -4,26 +4,80 @@
import { Icon, Qr } from '$comp';
import { Pincode, PincodeInput } from 'svelte-pincode';
import { post, success, failure } from '$lib/utils';
+ import { pin as current } from '$lib/store';
- export let user;
+ export let user, submit;
let settingPin, setting2fa, disabling2fa;
- let code = [];
- let pin = '';
let token = '';
- let tokenInput, pinInput;
+ let pinCode = [];
+ let verifyCode = [];
+ let tokenInput, pinInput, verifyInput;
+ let verifying = false;
+ let old;
+ let startVerifying = () => {
+ verifying = true;
+ };
+
+ $: pin = pinCode.join('');
+
+ let locked;
+ $: verifyCode.join('').length > 5 && checkPin();
+ let checkPin = async () => {
+ old = $current;
+ if (locked) return;
+ locked = true;
+
+ let verify = verifyCode.join('');
+
+ pinCode = ['', '', '', '', '', ''];
+ verifyCode = ['', '', '', '', '', ''];
+ settingPin = false;
+
+ try {
+ if (pin.length > 5 && pin === verify) {
+ user.haspin = true;
+ $current = pin;
+ submit.click();
+ } else {
+ failure('Pin mismatch, try again');
+ settingPin = true;
+ pinInput.focusFirstInput();
+ }
+ } catch (e) {
+ failure('Problem setting PIN');
+ }
+
+ verifying = false;
+
+ await new Promise((r) => setTimeout(r, 500));
+ locked = false;
+ };
+
+ $: if (verifying) verifyInput && tick().then(verifyInput.focusFirstInput);
$: if (settingPin) pinInput && tick().then(pinInput.focusFirstInput);
- $: if (setting2fa || disabling2fa) tokenInput && tick().then(tokenInput.focusFirstInput);
+ $: if (setting2fa || disabling2fa) tokenInput && tick().then(tokenInput?.focusFirstInput);
let reset = () => {
token = '';
- code = [];
return true;
};
- let togglePin = () => (settingPin = !settingPin);
+ let togglePin = async () => {
+ if (user.haspin) {
+ try {
+ pin = null;
+ submit.click();
+ } catch (e) {
+ failure('Failed to disable pin');
+ }
+ } else {
+ settingPin = true;
+ }
+ };
+
let toggleEnabling = () => reset() && (setting2fa = !setting2fa);
let toggleDisabling = () => reset() && (disabling2fa = !disabling2fa);
@@ -58,31 +112,44 @@
$: otpUri = `otpauth://totp/coinos:${user.username}?secret=${user.otpsecret}&period=30&digits=6&algorithm=SHA1&issuer=coinos`;
</script>
-<!-- TODO
<div>
- <span class="font-bold mb-1">{$t('user.settings.securityPIN')}</span>
+ <input type="hidden" name="newpin" value={pin} />
+
+ <span class="font-bold mb-1">{verifying ? 'Verify' : 'Security'} PIN</span>
<p class="text-secondary mb-1">
{$t('user.settings.securityPINDescription')}
</p>
{#if settingPin}
<div class="flex my-4">
<div class="mx-auto">
- <Pincode bind:code bind:value={user.pin} bind:this={pinInput}>
- <PincodeInput />
- <PincodeInput />
- <PincodeInput />
- <PincodeInput />
- <PincodeInput />
- <PincodeInput />
- </Pincode>
+ <div class:hidden={verifying}>
+ <Pincode on:complete={startVerifying} bind:code={pinCode} bind:this={pinInput}>
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ </Pincode>
+ </div>
+ <div class:hidden={!verifying}>
+ <Pincode bind:code={verifyCode} bind:this={verifyInput}>
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ </Pincode>
+ </div>
</div>
</div>
{:else}
<button type="button" class="primary" on:click={togglePin}
- ><Icon icon="lock" style="mr-1" /> {$t('user.settings.setPIN')}</button
+ ><Icon icon="lock" style="mr-1" /> {user.haspin ? 'Disable Pin' : 'Enable Pin'}</button
>
{/if}
-</div> -->
+</div>
<!-- TODO
<div>
@@ -116,7 +183,7 @@
<div class="text-center my-4">
{$t('user.settings.oneTimeCode')}<br />
- <Pincode bind:code bind:value={token} bind:this={tokenInput}>
+ <Pincode bind:value={token} bind:this={tokenInput}>
<PincodeInput />
<PincodeInput />
<PincodeInput />
@@ -128,7 +195,7 @@
{:else if disabling2fa}
<div class="text-center my-4">
{$t('user.settings.oneTimeCode')}<br />
- <Pincode bind:code bind:value={token} bind:this={tokenInput}>
+ <Pincode bind:value={token} bind:this={tokenInput}>
<PincodeInput />
<PincodeInput />
<PincodeInput />
diff --git a/src/routes/invoice/[id]/+page.svelte b/src/routes/invoice/[id]/+page.svelte
index 04a31813..84b03593 100644
--- a/src/routes/invoice/[id]/+page.svelte
+++ b/src/routes/invoice/[id]/+page.svelte
@@ -44,7 +44,9 @@
$: tip = Math.round((amount / 100) * tipPercent);
$invoices[id] = { amount, id, rate, received, text, tip, username };
- $: browser && $invoices[id]?.received >= $invoices[id]?.amount && goto(`/invoice/${id}/paid`);
+ $: browser &&
+ $invoices[id]?.received >= $invoices[id]?.amount &&
+ goto(($invoices[id].memo === 'launch' && '/launch/purchase') || `/invoice/${id}/paid`);
$: amountFiat = parseFloat(((amount * rate) / sats).toFixed(2));
diff --git a/src/routes/invoice/[id]/paid/+page.svelte b/src/routes/invoice/[id]/paid/+page.svelte
index 95c721d0..d619b7ff 100644
--- a/src/routes/invoice/[id]/paid/+page.svelte
+++ b/src/routes/invoice/[id]/paid/+page.svelte
@@ -22,7 +22,7 @@
};
</script>
-<div class="container px-4 text-center">
+<div class="container px-4 text-center mx-auto">
<div class="flex w-full py-20 max-w-[200px] mx-auto" in:scale={{ start: 0.5 }}>
<Icon icon="check" style="mx-auto" />
</div>
diff --git a/src/routes/launch/+page.server.js b/src/routes/launch/+page.server.js
new file mode 100644
index 00000000..2b44dfae
--- /dev/null
+++ b/src/routes/launch/+page.server.js
@@ -0,0 +1,6 @@
+import { auth, get } from '$lib/utils';
+
+export const load = async ({ parent }) => {
+ let tickets = await get('/tickets');
+ return { ...(await parent()), tickets };
+};
diff --git a/src/routes/launch/+page.svelte b/src/routes/launch/+page.svelte
index 04f53086..6faf492d 100644
--- a/src/routes/launch/+page.svelte
+++ b/src/routes/launch/+page.svelte
@@ -1,48 +1,99 @@
<script>
- import { redirect } from '$lib/store';
+ import { LandingHeader } from '$comp';
+ import { loginRedirect } from '$lib/store';
import { beforeNavigate } from '$app/navigation';
import { info } from '$lib/utils';
+ import { Icon } from '$comp';
+ import { t } from '$lib/translations';
+ import { goto } from '$app/navigation';
+ import { scroll } from '$lib/utils';
export let data;
- let { user } = data;
- $: link = user ? '/launch/purchase' : '/register';
- $redirect = '/launch/purchase';
+ let { user, tickets } = data;
+ $loginRedirect = '/launch/purchase';
+
+ let hasTicket = !!user?.accounts.find((a) => tickets.includes(a.asset));
+ $: link = hasTicket ? '/launch/ticket' : user ? $loginRedirect : '/register';
+ $: text = (hasTicket ? 'View' : 'Purchase') + ' Ticket';
beforeNavigate(({ to }) => {
- console.log(link);
- if (link === '/register') info('Please sign in so we can get you a ticket');
+ if (to?.routeId === 'register') info('Please sign in so we can get you a ticket');
});
+
+ let showMobileMenu = false;
+ const mobileMenuButtonClick = (section) => {
+ showMobileMenu = false;
+ scroll(section);
+ };
</script>
-<div class="flex mt-20 px-8">
- <img src="/images/ticket.webp" class="mb-8 md:max-w-xl mx-auto border border-gray-400" />
-</div>
-<div class="mx-auto flex items-center justify-center px-8">
- <div class="flex flex-col w-full bg-white rounded shadow-lg sm:w-3/4 md:w-1/2 lg:w-3/5">
+<header class="w-full lg:w-5/6 py-5 mx-auto fixed md:sticky z-10 top-0 bg-white/90">
+ <nav class="block xl:flex flex-wrap justify-between items-center">
+ <div class="flex justify-start md:justify-center items-center md:space-x-10">
+ <a href="/" class="hidden md:block">
+ <Icon icon="logo" style="mb-5 xl:mb-0" />
+ </a>
+ <Icon icon="logo" style="block md:hidden ml-5 w-48" />
+ </div>
+ </nav>
+</header>
+
+<div class="mx-auto flex items-center justify-center px-8 mt-20">
+ <div class="flex flex-col w-full bg-white rounded shadow-lg sm:w-3/4 lg:w-3/5">
<div class="flex flex-col w-full md:flex-row">
<div
class="flex flex-row justify-around p-4 font-bold leading-none text-gray-800 uppercase bg-gray-400 rounded md:flex-col md:items-center md:justify-center md:w-1/4"
>
<div class="md:text-3xl">Oct</div>
- <div class="md:text-6xl">5</div>
+ <div class="md:text-6xl">26</div>
<div class="md:text-xl">6 pm</div>
</div>
- <div class="p-4 font-normal text-gray-800 md:w-3/4">
+ <div class="p-4 font-normal text-gray-800 md:w-3/4 space-y-5">
<h1 class="mb-4 text-4xl font-bold leading-none tracking-tight text-gray-800">
- Coinos Launch Party!
+ Launch Party!
</h1>
+
<p class="leading-normal mb-2">
- Come join us at our launch party celebrating 10 years in business! We'll be introducing
- our newly designed payments interface and afterwards we'll network and enjoy tasty food
- and drinks from <a href="https://loftondenman.com/" class="underline">The Loft</a>!
+ Come join us at our launch party celebrating our new design and 10 years serving the
+ Vancouver Bitcoin community!
+ </p>
+
+ <div class="flex">
+ <div class="space-y-5">
+ <div class="ml-8">
+ <p><b>When</b></p>
+ <p>6pm October 26</p>
+ </div>
+ <div class="ml-8">
+ <p><b>Where</b></p>
+ <p>The Loft Lounge</p>
+ <p>1184 Denman Street, #203</p>
+ <p>Vancouver, BC</p>
+ </div>
+ </div>
+
+ <img src="/icons/party.svg" alt="Partygoers" class="w-[120px] mx-auto" />
+ </div>
+
+ <p>
+ We'll introduce our new interface and then network and enjoy tasty food and drinks from <a
+ href="https://loftondenman.com/"
+ class="underline">The Loft</a
+ >!
+ </p>
+
+ <p>
+ The Loft will be accepting Lightning payments at the event and going forward into the
+ future as well! We're charging $6 for for an NFT ticket that gets you entry, appies, and the
+ chance to win some swag and sats.
</p>
<div class="flex w-full my-8">
- <a href={link}>
+ <a href={link} class="mx-auto">
<button
- class="bg-white text-black border rounded-full px-8 py-4 font-bold hover:opacity-80 mx-auto text-2xl"
+ class="bg-white text-black border rounded-full px-8 py-4 font-bold hover:opacity-80 text-2xl"
>
- Purchase Ticket
+ {text}
</button>
</a>
</div>
diff --git a/src/routes/launch/purchase/+page.server.js b/src/routes/launch/purchase/+page.server.js
index f6314f7f..2ecce695 100644
--- a/src/routes/launch/purchase/+page.server.js
+++ b/src/routes/launch/purchase/+page.server.js
@@ -1,8 +1,45 @@
-export const load = ({ parent }) => parent();
+import { auth, get, post } from '$lib/utils';
+import tickets from '$lib/tickets';
+import { redirect } from '@sveltejs/kit';
+import { env } from '$env/dynamic/private';
+import { invalidateAll } from '$app/navigation';
+import { browser } from '$app/environment';
+import { createTicket } from '$lib/ticket';
+
+export const load = async ({ parent }) => {
+ let tickets = await get('/tickets');
+ let { user } = await parent();
+ let hasTicket = !!user?.accounts.find((a) => tickets.includes(a.asset));
+ if (hasTicket) throw redirect(307, '/launch/ticket');
+ return { tickets };
+};
export const actions = {
- default: async ({ cookies, request }) => {
- let form = Object.fromEntries(await request.formData());
- console.log(form);
+ internal: async ({ cookies, request }) => {
+ let form = await request.formData();
+ let asset = await createTicket(cookies, form.get('username'));
+ await post('/send', { amount: 22000, username: 'launch' }, auth(cookies));
+
+ throw redirect(307, `/launch/thanks/${asset}`);
+ },
+
+ lightning: async ({ cookies, request }) => {
+ let form = await request.formData();
+
+ let rates = await get('/rates');
+
+ let invoice = {
+ amount: 22000,
+ memo: 'launch',
+ network: 'lightning',
+ prompt: false,
+ rate: rates[form.get('currency')]
+ };
+
+ let user = { username: form.get('username') };
+
+ let { uuid } = await post('/invoice', { invoice, user }, auth(cookies));
+
+ throw redirect(307, `/invoice/${uuid}`);
}
};
diff --git a/src/routes/launch/purchase/+page.svelte b/src/routes/launch/purchase/+page.svelte
index df3048f3..3273752a 100644
--- a/src/routes/launch/purchase/+page.svelte
+++ b/src/routes/launch/purchase/+page.svelte
@@ -1,4 +1,5 @@
<script>
+ import { onMount } from 'svelte';
import { enhance } from '$app/forms';
import { selectedRate } from '$lib/store';
import { post } from '$lib/utils';
@@ -6,77 +7,61 @@
export let data;
- let amount = 35000;
+ let amount = 22000;
let {
+ currency,
account: { balance },
username
} = data.user;
- let invoice;
- let get = async (network) => {
- let { uuid } = await post('/invoice', {
- amount,
- rate: $selectedRate,
- network,
- username
- });
-
- goto(`/invoice/${uuid}`);
- };
-
- let lightning = () => get('lightning');
- let bitcoin = () => get('bitcoin');
-
- // if (balance < amount) throw redirect(307, `/${username}/fund/${amount - balance}`);
+ let { tickets } = data;
+ let ticket = tickets.length;
</script>
-<div class="mx-auto h-screen flex items-center justify-center px-8">
- <div class="p-4 font-normal text-gray-800 mx-auto mb-8">
- {#if balance > amount}
- <h1 class="mb-4 text-4xl font-bold leading-none tracking-tight text-gray-800 text-center">
- Buy Your Launch Party Ticket?
- </h1>
+<div class="mx-auto h-screen flex items-center justify-center px-4">
+ <div class="font-normal text-gray-800 mx-auto mb-8 space-y-8">
+ <h1 class="mb-4 text-4xl font-bold leading-none tracking-tight text-gray-800 text-center">
+ Purchase Ticket
+ </h1>
- <div class="text-center text-4xl mb-12">{amount} sats</div>
+ <img
+ src={`/tickets/${(ticket + 1).toString().padStart(2, '0')}.png`}
+ class="w-full max-w-[800px]"
+ alt="Ticket"
+ />
- <div class="w-full flex">
- <form method="POST" use:enhance>
+ {#if balance >= amount}
+ <form action="?/internal" method="POST" use:enhance>
+ <input type="hidden" name="username" value={username} />
+
+ <div class="w-full flex">
<button
class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 mx-auto text-2xl"
>
Count me in
</button>
- </form>
- </div>
+ </div>
+ </form>
{:else}
- <h1 class="mb-4 text-4xl font-bold leading-none tracking-tight text-gray-800 text-center">
- Ticket Price
- </h1>
- <div class="text-center text-4xl mb-12">{amount} sats</div>
-
- <h1 class="mb-4 text-4xl font-bold leading-none tracking-tight text-gray-800 text-center">
- Your Balance
- </h1>
-
- <div class="text-center text-4xl mb-12">{balance} sats</div>
-
- <h1 class="mb-4 text-4xl font-bold leading-none tracking-tight text-gray-800 text-center">
- Funding Options
+ <h1 class="mb-4 text-xl font-bold leading-none tracking-tight text-gray-800 text-center">
+ Payment Options
</h1>
- <button
- class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 mx-auto text-2xl"
- on:click={lightning}
- >
- Lightning
- </button>
+ <div class="flex justify-center gap-4">
+ <form action="?/lightning" method="POST">
+ <input type="hidden" name="currency" value={currency} />
+ <input type="hidden" name="username" value={username} />
+ <button class="border rounded-full px-8 py-4 font-bold hover:opacity-80 mx-auto text-2xl">
+ Lightning
+ </button>
+ </form>
- <button
- class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 mx-auto text-2xl"
- on:click={bitcoin}
- >
- Bitcoin
- </button>
+ <a href={`purchase/card`}>
+ <button class="border rounded-full px-8 py-4 font-bold hover:opacity-80 mx-auto text-2xl">
+ Credit Card
+ </button>
+ </a>
+ </div>
{/if}
</div>
</div>
diff --git a/src/routes/launch/purchase/card/+page.server.js b/src/routes/launch/purchase/card/+page.server.js
new file mode 100644
index 00000000..1f564f78
--- /dev/null
+++ b/src/routes/launch/purchase/card/+page.server.js
@@ -0,0 +1,44 @@
+import { env } from '$env/dynamic/private';
+import { error, redirect } from '@sveltejs/kit';
+import { post, auth } from '$lib/utils';
+import { createTicket } from '$lib/ticket';
+
+let stripe = 'https://api.stripe.com/v1';
+
+export const actions = {
+ default: async ({ cookies, request }) => {
+ let form = await request.formData();
+
+ let headers = new Headers();
+ headers.set('Authorization', 'Basic ' + new Buffer(env.STRIPE + ':').toString('base64'));
+
+ let body = new URLSearchParams({
+ 'card[number]': form.get('number').replace(/\s+/g, ''),
+ 'card[exp_month]': form.get('expiry').slice(0, 2),
+ 'card[exp_year]': form.get('expiry').slice(-2),
+ 'card[cvc]': form.get('cvc')
+ });
+
+ let method = 'POST';
+
+ let { id: source } = await fetch(`${stripe}/tokens`, { body, headers, method }).then((r) =>
+ r.json()
+ );
+
+ body = new URLSearchParams({
+ amount: 600,
+ currency: 'cad',
+ source,
+ description: 'launch ticket'
+ });
+
+ let { status } = await fetch(`${stripe}/charges`, { body, headers, method }).then((r) =>
+ r.json()
+ );
+
+ if (status !== 'succeeded') throw error(500, { message: 'Card payment failed' });
+ let asset = await createTicket(cookies, form.get('username'));
+
+ throw redirect(307, `/launch/thanks/${asset}`);
+ }
+};
diff --git a/src/routes/launch/purchase/card/+page.svelte b/src/routes/launch/purchase/card/+page.svelte
new file mode 100644
index 00000000..05d09025
--- /dev/null
+++ b/src/routes/launch/purchase/card/+page.svelte
@@ -0,0 +1,77 @@
+<script>
+ import { enhance } from '$app/forms';
+ import { Lock, Card, Calendar, Spinner } from '$comp';
+
+ export let data;
+
+ let { username } = data.user;
+ let loading;
+</script>
+
+<div class="container max-w-lg px-4 mx-auto space-y-5">
+ <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">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"
+ />
+ <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"
+ />
+ <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;"
+ />
+ <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>
diff --git a/src/routes/launch/thanks/[asset]/+page.server.js b/src/routes/launch/thanks/[asset]/+page.server.js
new file mode 100644
index 00000000..e9e01376
--- /dev/null
+++ b/src/routes/launch/thanks/[asset]/+page.server.js
@@ -0,0 +1 @@
+// export const load = ({ parent }) => parent();
diff --git a/src/routes/launch/thanks/[asset]/+page.svelte b/src/routes/launch/thanks/[asset]/+page.svelte
new file mode 100644
index 00000000..43f10057
--- /dev/null
+++ b/src/routes/launch/thanks/[asset]/+page.svelte
@@ -0,0 +1,29 @@
+<script>
+ import { page } from '$app/stores';
+
+ export let data;
+
+ let { user } = data;
+
+ let { asset } = $page.params;
+</script>
+
+<div class="container mx-auto max-w-lg px-4 mt-20">
+ <div class="font-normal text-gray-800 mx-auto mb-8 space-y-8">
+ <h1 class="mb-4 text-4xl font-bold leading-none tracking-tight text-gray-800 text-center">
+ Thanks!
+ </h1>
+
+ <p>We look forward to seeing you!</p>
+ </div>
+
+ <div class="w-full flex">
+ <a href="/launch/ticket" class="mx-auto">
+ <button
+ class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 text-2xl"
+ >
+ Done
+ </button>
+ </a>
+ </div>
+</div>
diff --git a/src/routes/launch/ticket/+page.server.js b/src/routes/launch/ticket/+page.server.js
new file mode 100644
index 00000000..0362eb7d
--- /dev/null
+++ b/src/routes/launch/ticket/+page.server.js
@@ -0,0 +1,9 @@
+import { get } from '$lib/utils';
+
+export const load = async ({ cookies, parent }) => {
+ let { user } = await parent();
+ let tickets = await get('/tickets');
+ let ticket = tickets.findIndex((t) => user.accounts.find((a) => a.asset === t));
+ let asset = tickets[ticket];
+ return { asset, ticket };
+};
diff --git a/src/routes/launch/ticket/+page.svelte b/src/routes/launch/ticket/+page.svelte
new file mode 100644
index 00000000..3879abcd
--- /dev/null
+++ b/src/routes/launch/ticket/+page.svelte
@@ -0,0 +1,35 @@
+<script>
+ export let data;
+ let { asset, ticket } = data;
+</script>
+
+<div class="mx-auto h-screen flex items-center justify-center px-4">
+ <div class="font-normal text-gray-800 mx-auto mb-8 space-y-8">
+ <h1 class="mb-4 text-4xl font-bold leading-none tracking-tight text-gray-800 text-center">
+ Your Ticket
+ </h1>
+
+ <p>
+ We've minted your ticket as an <a
+ href={`https://blockstream.info/liquid/asset/${asset}`}
+ class="underline">NFT on the Liquid Blockchain</a
+ > as a proof of purchase and commemorative token.
+ </p>
+
+ <img
+ src={`/tickets/${(ticket + 1).toString().padStart(2, '0')}.png`}
+ class="w-full max-w-[800px]"
+ alt="Ticket"
+ />
+
+ <div class="w-full flex">
+ <a href="/" class="mx-auto">
+ <button
+ class="bg-black text-white border rounded-full px-8 py-4 font-bold hover:opacity-80 text-2xl"
+ >
+ Done
+ </button>
+ </a>
+ </div>
+ </div>
+</div>
diff --git a/src/routes/login/+page.server.js b/src/routes/login/+page.server.js
index 94981ff7..b46af49e 100644
--- a/src/routes/login/+page.server.js
+++ b/src/routes/login/+page.server.js
@@ -4,15 +4,15 @@ import { login } from '$lib/utils';
export const actions = {
default: async ({ cookies, request }) => {
let form = Object.fromEntries(await request.formData());
- let { username, password, redirect: r } = form;
- let user = { username, password };
+ let { username, password, token, loginRedirect } = form;
+ let user = { username, password, token };
try {
- await login(user, cookies);
+ let r = await login(user, cookies);
} catch (e) {
- return invalid(400, { error: 'Login failed' });
+ return invalid(400, { error: 'Login failed', message: e.message, ...form });
}
- throw redirect(303, r || `/${user.username}/dashboard`);
+ throw redirect(303, loginRedirect || `/${user.username}/dashboard`);
}
};
diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte
index cf7cb517..75998cd0 100644
--- a/src/routes/login/+page.svelte
+++ b/src/routes/login/+page.svelte
@@ -1,5 +1,6 @@
<script>
import { RegisterOrLogin } from '$comp';
+
export let form;
</script>
diff --git a/src/routes/register/+page.server.js b/src/routes/register/+page.server.js
index bfe1a9b7..c5751d4c 100644
--- a/src/routes/register/+page.server.js
+++ b/src/routes/register/+page.server.js
@@ -4,7 +4,7 @@ import { post, login } from '$lib/utils';
export const actions = {
default: async ({ cookies, request }) => {
let form = Object.fromEntries(await request.formData());
- let { username, password, redirect: r } = form;
+ let { username, password, loginRedirect } = form;
let user = { username, password };
let error;
@@ -23,6 +23,6 @@ export const actions = {
}
if (error) return invalid(400, { error });
- throw redirect(307, r || `/${user.username}/dashboard`);
+ throw redirect(307, loginRedirect || `/${user.username}/dashboard`);
}
};
diff --git a/src/routes/register/+page.svelte b/src/routes/register/+page.svelte
index aa0e66e0..fd9e4417 100644
--- a/src/routes/register/+page.svelte
+++ b/src/routes/register/+page.svelte
@@ -5,9 +5,7 @@
afterNavigate(async () => {
try {
- console.log('whoa');
await invalidateAll();
- console.log('no');
} catch (e) {
console.log(e);
}
diff --git a/src/routes/scan/+page.svelte b/src/routes/scan/+page.svelte
index 1c14cb6d..dc6e291d 100644
--- a/src/routes/scan/+page.svelte
+++ b/src/routes/scan/+page.svelte
@@ -7,7 +7,7 @@
let scanner, vid;
onMount(() => {
- scanner = new QrScanner(vid, (text) => goto(`/send/${text}`));
+ scanner = new QrScanner(vid, (text) => goto(`/send/${text}`), { highlightScanRegion: true, highlightCodeOutline: true });
scanner.start();
});
diff --git a/src/routes/send/+layout.svelte b/src/routes/send/+layout.svelte
new file mode 100644
index 00000000..c9122466
--- /dev/null
+++ b/src/routes/send/+layout.svelte
@@ -0,0 +1,18 @@
+<script>
+ import { Pin } from '$comp';
+ import { pin } from '$lib/store';
+ import { onMount } from 'svelte';
+
+ export let data;
+
+ let loaded;
+ let { user } = data;
+
+ onMount(() => setTimeout(() => (loaded = true), 50));
+</script>
+
+{#if loaded && user.haspin && $pin?.length !== 6}
+ <Pin />
+{/if}
+
+<slot />
diff --git a/src/routes/send/[...text]/+page.server.js b/src/routes/send/[...text]/+page.server.js
index 45bb597d..b909ab4d 100644
--- a/src/routes/send/[...text]/+page.server.js
+++ b/src/routes/send/[...text]/+page.server.js
@@ -55,6 +55,6 @@ export const actions = {
const form = await request.formData();
await parse(form.get('text'));
- return invalid(403, { error: 'Failed to recognize input' });
+ return invalid(403, { error: 'Does not compute, try again' });
}
};
diff --git a/src/routes/send/[...text]/+page.svelte b/src/routes/send/[...text]/+page.svelte
index 5d6d123e..fe949617 100644
--- a/src/routes/send/[...text]/+page.svelte
+++ b/src/routes/send/[...text]/+page.svelte
@@ -92,8 +92,4 @@
line-height: 20px;
@apply block border rounded-2xl p-3 py-4 w-full;
}
-
- .expandable-expandable::-webkit-resizer {
- background: transparent;
- }
</style>
diff --git a/src/routes/send/[uuid=uuid]/+page.server.js b/src/routes/send/[uuid=uuid]/+page.server.js
index 661e9e7b..c9c77f70 100644
--- a/src/routes/send/[uuid=uuid]/+page.server.js
+++ b/src/routes/send/[uuid=uuid]/+page.server.js
@@ -10,14 +10,19 @@ export async function load({ params, parent }) {
export const actions = {
default: async ({ cookies, params, request }) => {
- let body = Object.fromEntries(await request.formData());
- let { confirmed } = body;
+ try {
+ let body = Object.fromEntries(await request.formData());
+ let { confirmed } = body;
- if (!confirmed) {
- return invalid(403, { amount, confirm: true });
+ if (!confirmed) {
+ return invalid(403, { amount, confirm: true });
+ }
+
+ await post('/send', body, auth(cookies));
+ } catch (e) {
+ return invalid(400, { message: e.message });
}
- await post('/send', body, auth(cookies));
throw redirect(307, '/sent');
}
};
diff --git a/src/routes/send/[uuid=uuid]/+page.svelte b/src/routes/send/[uuid=uuid]/+page.svelte
index aa798bc1..73712f71 100644
--- a/src/routes/send/[uuid=uuid]/+page.svelte
+++ b/src/routes/send/[uuid=uuid]/+page.svelte
@@ -1,6 +1,6 @@
<script>
import { goto } from '$app/navigation';
- import { selectedRate } from '$lib/store';
+ import { pin, selectedRate } from '$lib/store';
import { enhance } from '$app/forms';
import { Icon, Numpad, Spinner } from '$comp';
import { page } from '$app/stores';
@@ -31,12 +31,24 @@
let loading;
let submit = () => (loading = true);
+
+ $: update(form);
+ let update = () => {
+ if (form?.message.includes('pin')) $pin = undefined;
+ loading = false;
+ };
</script>
<button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" on:click={handleBack}>
<Icon icon="arrow-left" style="w-10" />
</button>
+{#if form?.message}
+ <div class="text-red-600 text-center">
+ {form.message}
+ </div>
+{/if}
+
<div class="container px-4 mt-20 max-w-xl mx-auto">
{#if amount}
<div class="text-center mb-8">
@@ -61,6 +73,7 @@
<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">
{#if amount}
diff --git a/src/routes/send/bitcoin/[address]/+page.server.js b/src/routes/send/bitcoin/[address]/+page.server.js
index 9b29a6da..642593ab 100644
--- a/src/routes/send/bitcoin/[address]/+page.server.js
+++ b/src/routes/send/bitcoin/[address]/+page.server.js
@@ -1,11 +1,16 @@
-import { redirect } from '@sveltejs/kit';
+import { invalid, redirect } from '@sveltejs/kit';
import { auth, post } from '$lib/utils';
export const actions = {
default: async ({ cookies, request }) => {
- let { address, amount } = Object.fromEntries(await request.formData());
- let { tx } = await post('/bitcoin/fee', { address, amount }, auth(cookies));
- await post('/bitcoin/send', { address, tx }, auth(cookies));
+ try {
+ let { address, amount, pin } = Object.fromEntries(await request.formData());
+ let { tx } = await post('/bitcoin/fee', { address, amount }, auth(cookies));
+ await post('/bitcoin/send', { address, pin, tx }, auth(cookies));
+ } catch (e) {
+ return invalid(400, { message: e.message });
+ }
+
throw redirect(307, '/sent');
}
};
diff --git a/src/routes/send/bitcoin/[address]/+page.svelte b/src/routes/send/bitcoin/[address]/+page.svelte
index 8c9e72ff..c9023180 100644
--- a/src/routes/send/bitcoin/[address]/+page.svelte
+++ b/src/routes/send/bitcoin/[address]/+page.svelte
@@ -3,19 +3,33 @@
import { Icon, Numpad, Spinner } from '$comp';
import { page } from '$app/stores';
import { back } from '$lib/utils';
+ import { pin } from '$lib/store';
+
export let data;
+ export let form;
let { address } = $page.params;
let { currency } = data.user;
let amount, loading;
let submit = () => (loading = true);
+ $: update(form);
+ let update = () => {
+ if (form?.message.includes('pin')) $pin = undefined;
+ loading = false;
+ };
</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>
+{#if form?.message}
+ <div class="text-red-600 text-center">
+ {form.message}
+ </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">Send to</h1>
@@ -26,6 +40,8 @@
<form method="POST" use:enhance on:submit={submit}>
<input name="address" value={address} type="hidden" />
<input name="amount" value={amount} type="hidden" />
+ <input name="pin" value={$pin} type="hidden" />
+
<div class="flex w-full">
<button
type="submit"
diff --git a/src/routes/send/lightning/[payreq]/+page.server.js b/src/routes/send/lightning/[payreq]/+page.server.js
index f17d6a60..49ebac1c 100644
--- a/src/routes/send/lightning/[payreq]/+page.server.js
+++ b/src/routes/send/lightning/[payreq]/+page.server.js
@@ -7,14 +7,20 @@ export async function load({ params }) {
export const actions = {
default: async ({ cookies, request }) => {
- let body = Object.fromEntries(await request.formData());
- let { amount, confirmed } = body;
+ try {
+ let body = Object.fromEntries(await request.formData());
+ let { amount, confirmed } = body;
- if (!confirmed) {
- return invalid(403, { amount, confirm: true });
+ if (!confirmed) {
+ return invalid(403, { amount, confirm: true });
+ }
+
+ await post('/lightning/send', body, auth(cookies));
+ } catch (e) {
+ if (e.message.includes("unusable")) e.message = "Failed to route payment, try sending a lower amount"
+ return invalid(400, { message: e.message });
}
- await post('/lightning/send', body, auth(cookies));
throw redirect(307, '/sent');
}
};
diff --git a/src/routes/send/lightning/[payreq]/+page.svelte b/src/routes/send/lightning/[payreq]/+page.svelte
index 973550f6..bda10038 100644
--- a/src/routes/send/lightning/[payreq]/+page.svelte
+++ b/src/routes/send/lightning/[payreq]/+page.svelte
@@ -3,6 +3,8 @@
import { Icon, Numpad, Spinner } from '$comp';
import { page } from '$app/stores';
import { back, s } from '$lib/utils';
+ import { pin } from '$lib/store';
+
export let data;
export let form;
@@ -14,12 +16,23 @@
let loading;
let submit = () => (loading = true);
+ $: update(form);
+ let update = () => {
+ if (form?.message.includes('pin')) $pin = undefined;
+ loading = false;
+ };
</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>
+{#if form?.message}
+ <div class="text-red-600 text-center">
+ {form.message}
+ </div>
+{/if}
+
<div class="container px-4 mt-20 max-w-xl mx-auto">
{#if amount}
<div class="text-center mb-8">
@@ -38,6 +51,7 @@
<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" />
<div class="flex w-full">
<button
diff --git a/src/routes/send/user/[username]/[...amount]/+page.server.js b/src/routes/send/user/[username]/[...amount]/+page.server.js
index 12bae186..a659eef3 100644
--- a/src/routes/send/user/[username]/[...amount]/+page.server.js
+++ b/src/routes/send/user/[username]/[...amount]/+page.server.js
@@ -11,14 +11,19 @@ export async function load({ params, parent }) {
export const actions = {
default: async ({ cookies, request }) => {
- let body = Object.fromEntries(await request.formData());
- let { amount, confirmed } = body;
+ try {
+ let body = Object.fromEntries(await request.formData());
+ let { amount, confirmed } = body;
- if (!confirmed) {
- return invalid(403, { amount, confirm: true });
+ if (!confirmed) {
+ return invalid(403, { amount, confirm: true });
+ }
+
+ await post('/send', body, auth(cookies));
+ } catch (e) {
+ return invalid(400, { message: e.message });
}
- await post('/send', body, auth(cookies));
throw redirect(307, '/sent');
}
};
diff --git a/src/routes/send/user/[username]/[...amount]/+page.svelte b/src/routes/send/user/[username]/[...amount]/+page.svelte
index e50acd5b..6f4cb313 100644
--- a/src/routes/send/user/[username]/[...amount]/+page.svelte
+++ b/src/routes/send/user/[username]/[...amount]/+page.svelte
@@ -1,10 +1,11 @@
<script>
import { goto } from '$app/navigation';
- import { selectedRate } from '$lib/store';
+ import { pin, selectedRate } from '$lib/store';
import { enhance } from '$app/forms';
import { Avatar, Icon, Numpad, Spinner } from '$comp';
import { page } from '$app/stores';
import { back, f, s, sats } from '$lib/utils';
+
export let data;
export let form;
@@ -31,12 +32,24 @@
let loading;
let submit = () => (loading = true);
+
+ $: update(form);
+ let update = () => {
+ if (form?.message.includes('pin')) $pin = undefined;
+ loading = false;
+ };
</script>
<button class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" on:click={handleBack}>
<Icon icon="arrow-left" style="w-10" />
</button>
+{#if form?.message}
+ <div class="text-red-600 text-center">
+ {form.message}
+ </div>
+{/if}
+
<div class="container px-4 mt-20 max-w-xl mx-auto">
{#if amount}
<div class="text-center mb-8">
@@ -63,6 +76,7 @@
<input name="amount" value={amount} 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">
{#if amount}
diff --git a/src/routes/user/+server.js b/src/routes/user/+server.js
index c9490984..b9c37e6e 100644
--- a/src/routes/user/+server.js
+++ b/src/routes/user/+server.js
@@ -1,5 +1,4 @@
import { auth, post } from '$lib/utils';
-import cookie from 'cookie';
export async function PUT({ cookies, request }) {
let user = await request.json();