diff options
Diffstat (limited to 'src')
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="•••" + /> + <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(); |
