diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/AppHeader.svelte | 8 | ||||
| -rw-r--r-- | src/components/RegisterOrLogin.svelte | 4 | ||||
| -rw-r--r-- | src/lib/socket.js | 4 | ||||
| -rw-r--r-- | src/lib/utils.js | 5 | ||||
| -rw-r--r-- | src/routes/[...path].svelte | 16 | ||||
| -rw-r--r-- | src/routes/[username]/index.js | 3 | ||||
| -rw-r--r-- | src/routes/[username]/index.svelte | 27 | ||||
| -rw-r--r-- | src/routes/[username]/receive.svelte (renamed from src/routes/receive.svelte) | 12 | ||||
| -rw-r--r-- | src/routes/__layout.svelte | 23 | ||||
| -rw-r--r-- | src/routes/invoice/[amount].js | 12 | ||||
| -rw-r--r-- | src/routes/invoice/[id].js | 13 | ||||
| -rw-r--r-- | src/routes/invoice/[id].svelte | 306 | ||||
| -rw-r--r-- | src/routes/invoice/index.js | 11 | ||||
| -rw-r--r-- | src/routes/logout/done.svelte | 13 | ||||
| -rw-r--r-- | src/routes/logout/index.js | 26 | ||||
| -rw-r--r-- | src/routes/logout/index.svelte | 10 |
16 files changed, 456 insertions, 37 deletions
diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte index 6376e27f..99eea6d5 100644 --- a/src/components/AppHeader.svelte +++ b/src/components/AppHeader.svelte @@ -8,15 +8,19 @@ <header class="bg-gradient-to-r from-primary to-gradient h-[20vh] w-full relative"> <nav class="flex justify-end items-center space-x-4 p-5"> - <button class="bg-white p-2 rounded-full w-12 h-12" on:click={() => goto('/receive')} + <a href={`/${$user.username}/receive`}> + <button class="bg-white p-2 rounded-full w-12 h-12" ><Icon icon="numpad" style="mx-auto" /></button > + </a> <button class="bg-white p-2 rounded-full w-12 h-12" ><Icon icon="clock" style="mx-auto" /></button > - <button class="bg-white p-2 rounded-full w-12 h-12" + <a href={`/${$user.username}`}> + <button class="bg-white p-2 rounded-full w-12 h-12" ><Icon icon="profile" style="mx-auto" /></button > + </a> <button class="bg-white p-2 rounded-full w-12 h-12" ><Icon icon="menu" style="mx-auto" /></button > diff --git a/src/components/RegisterOrLogin.svelte b/src/components/RegisterOrLogin.svelte index b42b8d7f..56b96499 100644 --- a/src/components/RegisterOrLogin.svelte +++ b/src/components/RegisterOrLogin.svelte @@ -1,7 +1,7 @@ <script> import { Icon } from '$comp'; import { post } from '$lib/utils'; - import { token } from '$lib/store'; + import { user, token } from '$lib/store'; import { auth } from '$lib/socket'; import { goto } from '$app/navigation'; @@ -22,7 +22,7 @@ if (!r.token) r = await post('/login', { username, password }); $token = r.token; auth(); - goto('/receive'); + user.subscribe(({ username }) => goto(`/${username}/receive`)); }; </script> diff --git a/src/lib/socket.js b/src/lib/socket.js index 9d0c472e..14555bd3 100644 --- a/src/lib/socket.js +++ b/src/lib/socket.js @@ -46,6 +46,10 @@ export function connect() { interval = setInterval(() => send('heartbeat'), 5000); } +export function close() { + if (socket) socket.close(); +} + function onWebsocketMessage(msg) { let { type, data } = JSON.parse(msg.data); messages(data)[type] && messages(data)[type](); diff --git a/src/lib/utils.js b/src/lib/utils.js index c17b0a98..e9d1b73e 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -6,6 +6,11 @@ export function scroll(section) { const base = browser ? '' : import.meta.env.VITE_COINOS_URL; +export const get = (url) => + fetch(base + url, { + headers: { accept: 'application/json'} + }).then((r) => r.json()); + export const post = (url, body) => fetch(base + url, { method: 'POST', diff --git a/src/routes/[...path].svelte b/src/routes/[...path].svelte deleted file mode 100644 index 503316bb..00000000 --- a/src/routes/[...path].svelte +++ /dev/null @@ -1,16 +0,0 @@ -<script> - import { Icon } from '$comp'; -</script> - -<div class="flex justify-center items-center mt-20"> - <div class="space-y-5 text-center"> - <a href="/"> - <Icon icon="logo" /> - </a> - - <Icon icon="question" style="mx-auto w-full" /> - - <h1 class="font-bold text-5xl">404</h1> - <p>Page not found.</p> - </div> -</div> diff --git a/src/routes/[username]/index.js b/src/routes/[username]/index.js new file mode 100644 index 00000000..39a216c2 --- /dev/null +++ b/src/routes/[username]/index.js @@ -0,0 +1,3 @@ +export async function GET({ params }) { + return { body: params } +} diff --git a/src/routes/[username]/index.svelte b/src/routes/[username]/index.svelte new file mode 100644 index 00000000..fe1f7754 --- /dev/null +++ b/src/routes/[username]/index.svelte @@ -0,0 +1,27 @@ +<script> + import { AppHeader, Icon } from '$comp'; + import { user } from '$lib/store'; + export let username; + + console.log($user, username); +</script> + +{#if $user?.username === username} + <AppHeader /> + <div class="flex justify-center items-center mt-20 mb-3 px-3"> + <div class="space-y-3 w-full md:w-[300px] mx-auto"> + <div class="text-center"> + <b>Signed in as {username}</b> + </div> + + <div> + <a href="/logout" style="cursor: pointer"> + <button + class="bg-primary rounded-xl w-full h-[48px] flex justify-center items-center font-semibold text-sm" + ><Icon icon="logout" style="mr-2" /> Logout</button + > + </a> + </div> + </div> + </div> +{/if} diff --git a/src/routes/receive.svelte b/src/routes/[username]/receive.svelte index 3ccd45e8..d588eaed 100644 --- a/src/routes/receive.svelte +++ b/src/routes/[username]/receive.svelte @@ -4,6 +4,8 @@ import { AppHeader, Icon, LoadingSplash } from '$comp'; import { goto } from '$app/navigation'; import { invoiceAmount, invoiceAmountFiat, rate, user } from '$lib/store'; + import { post } from '$lib/utils'; + import { page } from '$app/stores'; onMount(async () => { if (browser) { @@ -21,6 +23,8 @@ let amountSats = 0; let message = ''; + $: amount = useFiat ? Math.round(amountFiat / ($rate / 100000000)) : amountSats; + $: $invoiceAmount = parseInt(amountSats === 0 ? amountFiat / ($rate / 100000000) : amountSats); $: $invoiceAmountFiat = parseInt( amountFiat === 0 ? amountSats * ($rate / 100000000) : amountFiat @@ -88,6 +92,12 @@ } } }; + + let submit = async () => { + let { id } = await post('/invoice', { amount, rate: $rate, username: $page.params.username }); + console.log(id) + goto(`/invoice/${id}`); + }; </script> {#if $user} @@ -172,7 +182,7 @@ 0 ? 'opacity-50' : 'opacity-100'}" - on:click={() => goto(`/invoice/${$invoiceAmount}`)} + on:click={submit} disabled={$invoiceAmount === 0}><Icon icon="qr" style="mr-2" /> Show QR</button > <button diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte index f91772e8..1e4e2560 100644 --- a/src/routes/__layout.svelte +++ b/src/routes/__layout.svelte @@ -3,13 +3,28 @@ import '../app.css'; import { onMount } from 'svelte'; import { connect } from '$lib/socket'; - import { token } from '$lib/store'; - import { session } from '$app/stores'; + import { user, token } from '$lib/store'; + import { page, session } from '$app/stores'; import { browser } from '$app/env'; + import { goto } from '$app/navigation'; browser && ($token ||= $session.token); - onMount(connect); + let protectedRoutes = [/receive/]; + let ready = false; + + onMount(() => { + if (protectedRoutes.find((p) => $page.url.pathname.match(p))) { + if (!$token) goto('/login'); + user.subscribe((u) => (ready = true)); + } else { + ready = true; + } + + connect(); + }); </script> +{#if ready} <SvelteToast /> -<slot /> + <slot /> +{/if} diff --git a/src/routes/invoice/[amount].js b/src/routes/invoice/[amount].js deleted file mode 100644 index 9e9678a6..00000000 --- a/src/routes/invoice/[amount].js +++ /dev/null @@ -1,12 +0,0 @@ -import { post } from '$lib/utils'; - -const username = 'bob', - network = 'lightning'; - -export async function GET({ params }) { - let { amount } = params; - let { text } = await post('/lightning/invoice', { amount }); - await post('/invoice', { invoice: { text, network }, user: { username } }); - - return { body: { amount, text } }; -} diff --git a/src/routes/invoice/[id].js b/src/routes/invoice/[id].js new file mode 100644 index 00000000..7d18ba86 --- /dev/null +++ b/src/routes/invoice/[id].js @@ -0,0 +1,13 @@ +import { get } from '$lib/utils'; + +const username = 'bob', + network = 'lightning'; + +export async function GET({ params }) { + let { id } = params; + let huh = await get(`/invoice?uuid=${id}`); + console.log(huh) + let { amount, rate, text } = await get(`/invoice?uuid=${id}`); + + return { body: { amount, rate, text } }; +} diff --git a/src/routes/invoice/[id].svelte b/src/routes/invoice/[id].svelte new file mode 100644 index 00000000..a9f3cefb --- /dev/null +++ b/src/routes/invoice/[id].svelte @@ -0,0 +1,306 @@ +<script> + import { browser } from '$app/env'; + import { user } from '$lib/store'; + import { Icon } from '$comp'; + import { goto } from '$app/navigation'; + import { onMount } from 'svelte'; + + export let amount, rate, text; + + if (!$user && typeof window !== 'undefined') { + window.location = '/login'; + } + + $: amountFiat = parseFloat(((amount * rate) / 100000000).toFixed(2)); + + let tipPercent = 0; + + $: tipAmount = customTipAmount + ? customTipAmount < 0 + ? (customTipAmount = 0) + : customTipAmount + : (amountFiat / 100) * tipPercent; + + $: tipAmountFormatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(tipAmount); + + $: tipAmountSats = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format( + tipAmount / (rate / 100000000) + ); + + $: totalAmountSats = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format( + amount + parseFloat(tipAmountSats.replace(/,/g, '')) + ); + + $: totalAmountFormatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(amountFiat + tipAmount); + + const amountFormatted = new Intl.NumberFormat('en-US', { + maximumFractionDigits: 0 + }).format(amount); + + $: invoiceAmountFiatFormatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(amountFiat); + + let customTipAmount; + + const tipAmounts = ['None', '10%', '15%', '20%']; + + const handleTipButtonClick = (amount) => { + customTipAmount = ''; + if (amount === 'None') { + tipPercent = 0; + } else { + tipPercent = amount.slice(0, 2); + } + }; + + const handleCustomTipAmount = (e) => { + tipPercent = 0; + customTipAmount = e.target.value; + }; + + onMount(async () => { + if (browser) { + let { default: QRCodeStyling } = await import('qr-code-styling'); + const qrCode = new QRCodeStyling({ + width: window.screen.width < 640 ? 250 : 300, + type: 'svg', + data: text, + image: '/images/invoice.svg', + backgroundOptions: { + color: 'rgba(0, 0, 0, 0)' + }, + dotsOptions: { + type: 'rounded' + }, + imageOptions: { + hideBackgroundDots: false + } + }); + + qrCode.append(document.getElementById('qr')); + } + }); + + let showMobileTip = false; + + const handleBackButton = () => { + if (showMobileTip) { + tipPercent = 0; + customInput.value = ''; + customTipAmount = 0; + tipAmount = 0; + showMobileTip = false; + } else { + goto(`/${$user.username}/receive`); + } + }; + + let customInput; +</script> + +{#if $user} + <div class:full-shadow={showMobileTip}> + <button class="ml-5 md:ml-20 mt-5 md:mt-10" on:click={handleBackButton}> + <Icon icon="arrow-left" style="w-10" /> + </button> + + <div class="flex justify-center items-center mb-20 md:mt-[20px] px-3"> + <div + class="block lg:flex justify-center items-center space-y-10 lg:space-y-0 space-x-0 lg:space-x-24" + > + <!-- tipping section --> + <div + class="absolute {showMobileTip + ? 'bottom-0' + : '-bottom-full'} bg-white p-6 md:p-0 rounded-t-3xl md:rounded-none transition-all md:transition-none ease-in-out duration-500 left-0 md:static text-center space-y-5" + > + <h1 class="hidden md:block text-4xl font-semibold">Add a tip?</h1> + + <div> + <span class="font-semibold mr-1">{tipAmountFormatted}</span><span + class="text-secondary font-semibold text-sm">{`(${tipAmountSats} SATS)`}</span + > + <span class="block text-secondary text-sm">{tipPercent}%</span> + </div> + + <input + id="slider" + type="range" + min="0" + max="100" + value={tipPercent} + class="px-2" + on:input={(e) => { + customTipAmount = ''; + tipPercent = e.target.value; + }} + /> + + <div> + {#each tipAmounts as amount} + <button + class="bg-primary w-16 md:w-20 py-3 m-1 rounded-2xl font-semibold {tipPercent === + amount.slice(0, 2) || + ((tipPercent === 0 || tipPercent === '0') && amount === 'None') + ? '!bg-black text-white' + : ''}" + on:click={() => handleTipButtonClick(amount)}>{amount}</button + > + {/each} + + <div class="relative"> + <span + class="absolute top-[20px] left-5 font-semibold {!customTipAmount + ? 'opacity-50' + : 'opacity-100'}">$</span + > + <input + bind:this={customInput} + type="number" + min="0" + on:input={(e) => handleCustomTipAmount(e)} + class="bg-primary rounded-2xl py-3 px-10 font-semibold mt-2 mr-1 w-full md:w-auto" + placeholder="Other" + /> + <button + class="w-full md:w-auto mt-2 md:mt-0 bg-black text-white font-semibold py-3 px-7 rounded-2xl {showMobileTip + ? !customTipAmount && !tipAmount + ? 'opacity-50' + : 'opacity-100' + : !customTipAmount + ? 'opacity-50' + : 'opacity-100'}" + disabled={showMobileTip ? !customTipAmount && !tipAmount : !customTipAmount} + >Done</button + > + </div> + </div> + </div> + + <div + class="hidden md:block h-[1px] lg:h-[600px] w-full lg:w-[1px] bg-lightgrey rounded-xl" + id="section-divider" + /> + + <!-- invoice section --> + <div class="text-center space-y-5"> + <button + class="block md:hidden bg-black {showMobileTip + ? 'text-white/50' + : 'text-white'} rounded-xl px-6 py-3 block text-sm font-semibold mx-auto flex justify-center items-center" + on:click={() => (showMobileTip = true)} + disabled={showMobileTip} + > + <svg + width="24" + height="24" + class="mr-1" + viewBox="0 0 24 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M4.31802 6.31802C2.56066 8.07538 2.56066 10.9246 4.31802 12.682L12.0001 20.364L19.682 12.682C21.4393 10.9246 21.4393 8.07538 19.682 6.31802C17.9246 4.56066 15.0754 4.56066 13.318 6.31802L12.0001 7.63609L10.682 6.31802C8.92462 4.56066 6.07538 4.56066 4.31802 6.31802Z" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> Add a tip + </button> + + <span class="text-secondary block" + >Scan to pay <span class="text-black font-semibold">{$user.username}</span></span + > + + <div + id="qr" + class="border {showMobileTip + ? 'border-gray-400' + : 'border-lightgrey'} rounded-3xl block md:flex justify-center items-center" + /> + + <div class="px-5 space-y-3"> + <div class="flex justify-between"> + <span class="font-semibold text-sm">Invoice</span> + <span class="font-semibold text-sm" + >{invoiceAmountFiatFormatted} + <span class="text-secondary font-normal">{`(${amountFormatted} SATS)`}</span></span + > + </div> + + <div class="flex justify-between"> + <span class="font-semibold text-sm">Tip</span> + <span class="font-semibold text-sm" + >{tipAmountFormatted} + <span class="text-secondary font-normal">{`(${tipAmountSats} SATS)`}</span></span + > + </div> + + <div class="flex justify-between"> + <span class="font-bold">Total</span> + <span class="font-bold" + >{totalAmountFormatted} + <span class="text-secondary font-normal">{`(${totalAmountSats} SATS)`}</span></span + > + </div> + </div> + </div> + </div> + </div> + </div> +{/if} + +<style> + #slider { + -webkit-appearance: none; + width: 100%; + height: 6px; + border-radius: 5px; + background: #eaecef; + outline: none; + } + + #slider:active { + background-color: black; + } + + #slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 25px; + height: 25px; + border-radius: 50%; + background: black; + cursor: pointer; + } + + #slider::-moz-range-thumb { + width: 25px; + height: 25px; + border-radius: 50%; + background: black; + cursor: pointer; + } + + input[type='number']::-webkit-inner-spin-button, + input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + } + + input[type='number'] { + -moz-appearance: textfield; + } + + .full-shadow { + box-shadow: inset 0 0 0 1000px rgba(0, 0, 0, 0.25); + } +</style> diff --git a/src/routes/invoice/index.js b/src/routes/invoice/index.js new file mode 100644 index 00000000..d89f7130 --- /dev/null +++ b/src/routes/invoice/index.js @@ -0,0 +1,11 @@ +import { post } from '$lib/utils'; + +let network = 'lightning'; + +export async function POST({ request }) { + let { amount, rate, username } = await request.json(); + let { text } = await post('/lightning/invoice', ); + let { uuid: id } = await post('/invoice', { invoice: { amount, currency: 'USD', network, rate, text }, user: { username } }); + + return { body: { id } }; +} diff --git a/src/routes/logout/done.svelte b/src/routes/logout/done.svelte new file mode 100644 index 00000000..894c7b99 --- /dev/null +++ b/src/routes/logout/done.svelte @@ -0,0 +1,13 @@ +<script> + import { onMount } from "svelte"; + import { user, token } from "$lib/store"; + import { goto } from "$app/navigation"; + import { close } from "$lib/socket"; + + onMount(() => { + $token = null; + $user = null; + close(); + goto('/'); + }); +</script> diff --git a/src/routes/logout/index.js b/src/routes/logout/index.js new file mode 100644 index 00000000..80e65f8b --- /dev/null +++ b/src/routes/logout/index.js @@ -0,0 +1,26 @@ +import cookie from "cookie"; + +const opts = { + httpOnly: true, + sameSite: "lax", + path: "/", +}; + +export async function GET({ request: { headers } }) { + return { + status: 302, + headers: { + location: "/logout/done", + "set-cookie": [ + cookie.serialize("token", "", { + ...opts, + expires: new Date(0), + }), + cookie.serialize("refresh_token", "", { + ...opts, + expires: new Date(0), + }), + ], + }, + }; +} diff --git a/src/routes/logout/index.svelte b/src/routes/logout/index.svelte new file mode 100644 index 00000000..2c6a7085 --- /dev/null +++ b/src/routes/logout/index.svelte @@ -0,0 +1,10 @@ +<script> + import { onMount } from 'svelte'; + import { browser } from '$app/env'; + import { user, token } from '$lib/store'; + + onMount(() => { + $user = null; + $token = null; + }); +</script> |
