aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/AppHeader.svelte8
-rw-r--r--src/components/RegisterOrLogin.svelte4
-rw-r--r--src/lib/socket.js4
-rw-r--r--src/lib/utils.js5
-rw-r--r--src/routes/[...path].svelte16
-rw-r--r--src/routes/[username]/index.js3
-rw-r--r--src/routes/[username]/index.svelte27
-rw-r--r--src/routes/[username]/receive.svelte (renamed from src/routes/receive.svelte)12
-rw-r--r--src/routes/__layout.svelte23
-rw-r--r--src/routes/invoice/[amount].js12
-rw-r--r--src/routes/invoice/[id].js13
-rw-r--r--src/routes/invoice/[id].svelte306
-rw-r--r--src/routes/invoice/index.js11
-rw-r--r--src/routes/logout/done.svelte13
-rw-r--r--src/routes/logout/index.js26
-rw-r--r--src/routes/logout/index.svelte10
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>