diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/LandingHeader.svelte | 8 | ||||
| -rw-r--r-- | src/components/RegisterOrLogin.svelte | 19 | ||||
| -rw-r--r-- | src/components/Spinner.svelte | 13 | ||||
| -rw-r--r-- | src/components/index.js | 1 | ||||
| -rw-r--r-- | src/lib/parse.js | 271 | ||||
| -rw-r--r-- | src/lib/utils.js | 5 | ||||
| -rw-r--r-- | src/locales/en.json | 6 | ||||
| -rw-r--r-- | src/locales/es.json | 3 | ||||
| -rw-r--r-- | src/locales/fr.json | 3 | ||||
| -rw-r--r-- | src/routes/[username]/dashboard/+page.svelte | 140 | ||||
| -rw-r--r-- | src/routes/scan/+page.svelte | 24 | ||||
| -rw-r--r-- | src/routes/send/+page.svelte | 62 |
12 files changed, 429 insertions, 126 deletions
diff --git a/src/components/LandingHeader.svelte b/src/components/LandingHeader.svelte index c979e2c8..adeac98e 100644 --- a/src/components/LandingHeader.svelte +++ b/src/components/LandingHeader.svelte @@ -67,11 +67,11 @@ </button> <div - class="block md:hidden absolute top-0 {showMobileMenu + class="container w-full px-10 md:hidden absolute top-0 {showMobileMenu ? 'right-0' : 'right-[-100%]'} transition-all ease-in-out duration-300 h-[100vh] w-full bg-white" > - <div class="space-y-8 mt-24 ml-10 font-bold text-xl"> + <div class="space-y-8 mt-24 font-bold text-xl"> <button on:click={() => mobileMenuButtonClick(howItWorks)} class="block" >{$t('howItWorks.header')}</button > @@ -82,12 +82,12 @@ > {#if !$user} <button - class="border rounded-full px-6 py-2 font-bold block" + class="border rounded-full px-6 py-2 font-bold block w-full" on:click={() => goto('/register')} >{$t('nav.startInSeconds')} </button> <button - class="bg-black text-white border rounded-full px-6 py-2 font-bold block" + class="bg-black text-white border rounded-full px-6 py-2 font-bold block w-full" on:click={() => goto('/login')} > {$t('nav.signIn')} diff --git a/src/components/RegisterOrLogin.svelte b/src/components/RegisterOrLogin.svelte index 88fe62e6..fa805a1d 100644 --- a/src/components/RegisterOrLogin.svelte +++ b/src/components/RegisterOrLogin.svelte @@ -1,5 +1,5 @@ <script> - import { browser } from "$app/environment"; + import { browser } from '$app/environment'; import { page } from '$app/stores'; import { tick } from 'svelte'; import { Icon } from '$comp'; @@ -23,12 +23,12 @@ signIn: '/login' }[pageID]; - // TODO: move to server side + // TODO: move to server side $: if (browser && $user) goto(`/${$user.username}/receive`); - let focus = (el) => setTimeout(() => el.focus()); + let focus = (el) => setTimeout(() => el.focus()); - console.log("REDIRECT STORE", $redirect) + console.log('REDIRECT STORE', $redirect); </script> <div class="pt-10"> @@ -56,13 +56,21 @@ class="bg-primary" bind:value={username} use:focus + autocapitalize="none" /> </div> <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} /> + <input + name="password" + type="text" + required + class="bg-primary" + bind:value={password} + autocapitalize="none" + /> {:else} <input name="password" @@ -70,6 +78,7 @@ required class="bg-primary" bind:value={password} + autocapitalize="none" /> {/if} <button diff --git a/src/components/Spinner.svelte b/src/components/Spinner.svelte new file mode 100644 index 00000000..eccf98ce --- /dev/null +++ b/src/components/Spinner.svelte @@ -0,0 +1,13 @@ +<svg + class="mx-auto h-6 w-6 animate-spin text-white" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" +> + <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /> + <path + class="opacity-75" + fill="currentColor" + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" + /> +</svg> diff --git a/src/components/index.js b/src/components/index.js index 7d01e338..ba0f6516 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,3 +1,4 @@ +export { default as Spinner } from './Spinner.svelte'; export { default as Check } from './Check.svelte'; export { default as Heart } from './Heart.svelte'; export { default as Quote } from './Quote.svelte'; diff --git a/src/lib/parse.js b/src/lib/parse.js new file mode 100644 index 00000000..2a827956 --- /dev/null +++ b/src/lib/parse.js @@ -0,0 +1,271 @@ +export default (text) => { + if (!text) return; + + if (text.includes('@') && text.includes('.')) { + let [name, domain] = text.split('@'); + try { + clearTimeout(debounce); + await new Promise((r) => (debounce = setTimeout(r, 1500))); + ({ data: text } = await Vue.axios.get( + `/encode?domain=${domain}&name=${name}` + )); + } catch (e) {} + } + + try { + if (text.toLowerCase().slice(0, 10) === 'lightning:') + text = text.slice(10); + let payreq = text.toLowerCase(); + payment.payreq = payreq; + payment.payobj = bolt11.decode(payment.payreq); + let { + network: { bech32 }, + } = payment.payobj; + + if (!networks.includes('lightning')) + return commit('error', 'Lightning not supported in this account'); + + if (bech32 !== process.env.VUE_APP_NETWORK_BECH32) { + dispatch('clearPayment'); + commit( + 'error', + `Wrong network prefix '${bech32}' instead of '${process.env.VUE_APP_NETWORK_BECH32}'` + ); + throw new Error('Wrong network'); + } + + if (user.account.ticker !== 'BTC') + await dispatch( + 'shiftAccount', + user.accounts.find((a) => a.asset === BTC).id + ); + + let { tags, satoshis, millisatoshis } = payment.payobj; + let description = tags.find((t) => t.tagName === 'description'); + if (description) payment.memo = description.data; + payment.amount = millisatoshis + ? Math.round(millisatoshis / 1000) + : satoshis; + payment.fiatAmount = ((payment.amount * getters.rate) / SATS).toFixed( + 2 + ); + payment.network = 'lightning'; + + await Vue.axios.post('/lightning/query', { payreq }); + if (!getters.payment.recipient) + go({ name: 'send', params: { keep: true } }); + + return; + } catch (e) { + if (e.response) commit('error', e.response.data); + } + // + // let url; + // try { + // if (nodes.includes('bitcoin')) { + // url = bip21.decode(text); + // if (!networks.includes('bitcoin')) + // return commit('error', 'Bitcoin not supported in this account'); + // payment.network = 'bitcoin'; + // url.options.asset = BTC; + // } + // } catch (e) { + // if (nodes.includes('liquid')) { + // try { + // url = bip21.decode(text, 'liquidnetwork'); + // if (!networks.includes('liquid')) + // return commit('error', 'Liquid not supported in this account'); + // payment.network = 'liquid'; + // } catch (e) { + // /**/ + // } + // } + // } + // + // if (url) { + // let { amount, asset, assetid, message } = url.options; + // if (assetid) asset = assetid; + // if (!asset) asset = BTC; + // let account = user.accounts.find((a) => a.asset === asset); + // if (account) { + // if (account.asset !== user.account.asset) + // await dispatch('shiftAccount', account.id); + // } else return commit('error', 'Unrecognized asset'); + // + // payment.address = url.address; + // + // if (message) payment.memo = message; + // + // if (amount) { + // payment.amount = parseInt((amount * SATS).toFixed(0)); + // payment.fiatAmount = ((payment.amount * getters.rate) / SATS).toFixed( + // 2 + // ); + // } + // + // await dispatch('isInternal'); + // await dispatch('estimateFee'); + // go({ name: 'send', params: { keep: true } }); + // return; + // } + // + // if (nodes.includes('bitcoin') && validate(text)) { + // if (!networks.includes('bitcoin')) + // return commit('error', 'Bitcoin not supported in this account'); + // payment.address = text; + // payment.network = 'bitcoin'; + // await dispatch('isInternal'); + // go({ name: 'send', params: { keep: true } }); + // return; + // } + // + // // Liquid + // if (nodes.includes('liquid') && isLiquid(text)) { + // if (!networks.includes('liquid')) + // return commit('error', 'Liquid not supported in this account'); + // payment.address = text; + // payment.network = 'liquid'; + // await dispatch('isInternal'); + // go({ name: 'send', params: { keep: true } }); + // return; + // } + // + // if (text.startsWith('6P')) { + // commit('text', text); + // return go('/decrypt'); + // } + // + // try { + // let ecpair = ECPair.fromWIF(text, { + // ...this._vm.$network, + // confidentialPrefix: 1, + // assetHash: '', + // }); + // commit('ecpair', ecpair); + // go('/sweep'); + // } catch (e) { + // /* */ + // } + // + // if (text.toLowerCase().startsWith('lnurl:')) { + // try { + // const { data: lnurl } = await Vue.axios.get(`/url?code=${text}`); + // await dispatch('handleScan', lnurl); + // return; + // } catch (e) { + // commit('error', e.response ? e.response.data : e.message); + // } + // } + // + // if (text.toLowerCase().includes('lnurl')) { + // commit('loading', true); + // let txt = text.toLowerCase().match(/lnurl[a-z0-9]+/); + // + // try { + // const { data: params } = await Vue.axios.get(`/decode?text=${txt}`); + // if (params.status === 'ERROR') { + // let { reason } = params; + // try { + // ({ reason } = JSON.parse(reason.replace(/.*{/, '{'))); + // } catch { + // /**/ + // } + // + // commit('loading', false); + // return commit('error', reason); + // } + // + // let { seed } = getters; + // + // switch (params.tag) { + // case 'channelRequest': + // await dispatch('openChannel', params); + // break; + // case 'login': + // if (!seed) ({ seed } = await dispatch('passwordPrompt')); + // try { + // const key = linkingKey(params.domain, seed); + // const signedMessage = secp256k1.ecdsaSign( + // hexToUint8Array(params.k1), + // key.privateKey + // ); + // const signedMessageDER = secp256k1.signatureExport( + // signedMessage.signature + // ); + // const linkingKeyPub = secp256k1.publicKeyCreate( + // key.privateKey, + // true + // ); + // const sig = bytesToHexString(signedMessageDER); + // + // const response = await Vue.axios.post('/login', { + // params, + // sig, + // key: bytesToHexString(linkingKeyPub), + // }); + // + // if (response.data.status === 'OK') { + // commit('snack', 'Login success'); + // } else { + // commit('error', response.status); + // } + // + // go('/home'); + // } catch (e) { + // commit('error', e.response ? e.response.data : e.message); + // } + // break; + // + // case 'withdrawRequest': + // commit('lnurl', params); + // go('/withdraw'); + // break; + // case 'payRequest': + // commit('lnurl', params); + // go('/pay'); + // break; + // } + // } catch (e) { + // commit('error', e.response ? e.response.data : e.message); + // } + // + // commit('loading', false); + // } + // + // if (isUuid(text)) { + // try { + // const { data: payment } = await Vue.axios.post('/checkRedeemCode', { + // redeemcode: text, + // }); + // + // if (payment) { + // window.location.href = `${window.location.protocol}//${window.location.host}/redeem/${text}`; + // return; + // } + // } catch (e) { + // commit('error', e.response ? e.response.data : e.message); + // } + // + // try { + // const { data: invoice } = await Vue.axios.get( + // `/invoice?uuid=${text}` + // ); + // await dispatch('handleScan', invoice.text); + // return; + // } catch (e) { + // commit('error', e.response ? e.response.data : e.message); + // } + // } + // + // if ( + // text.startsWith( + // `${window.location.protocol}//${window.location.host}/redeem` + // ) + // ) { + // window.location.href = text; + // } + // + // commit('stopScanning', true); + // + // return 'no'; +}; diff --git a/src/lib/utils.js b/src/lib/utils.js index df621c4b..61e2ddac 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,6 +1,7 @@ import cookie from 'cookie'; import { browser } from '$app/environment'; import { toast } from '@zerodevx/svelte-toast'; +import { goto } from '$app/navigation'; export function scroll(section) { section.scrollIntoView({ behavior: 'smooth' }); @@ -144,3 +145,7 @@ export const toggleFullscreen = (el) => : el.requestFullscreen().catch(console.log); export const sats = 100000000; + +export const goBack = () => { + browser && history.go(-1); +}; diff --git a/src/locales/en.json b/src/locales/en.json index 7c07f0e9..0a99b947 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -162,7 +162,9 @@ "remove": "Remove", "scanToPayBTC": "Scan to pay with bitcoin", "dashboard": { - "ACCOUNT_BALANCE": "ACCOUNT BALANCE", + "go": "Go", + "dashboard": "Dashboard", + "accountBalance": "Account Balance", "bitcoinPrice": "bitcoin Price:", "conversionFeeWarning": "You will be charged a 1% withdrawal fee on the amount.", "dataFromBinance": "Data from <span class=\"text-black\">Binance</span>.", @@ -170,7 +172,7 @@ "optionalNote": "Note (Optional)", "send": "Send", "receive": "Receive a Payment", - "withdraw": "Withdraw" + "withdraw": "Withdraw Funds" }, "receive": { "lessThan1BTCWarning": "Please enter an amount less than 1 bitcoin.", diff --git a/src/locales/es.json b/src/locales/es.json index f378c555..41452f4a 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -159,7 +159,8 @@ "remove": "Eliminar", "scanToPayBTC": "Escanea para pagar en bitcoin", "dashboard": { - "ACCOUNT_BALANCE": "ACCOUNT BALANCE", + "dashboard": "Tablero", + "accountBalance": "Saldo", "bitcoinPrice": "Precio de bitcoin:", "conversionFeeWarning": "Cobraremos una tarifa de retiro del 1% sobre el monto", "dataFromBinance": "Datos de <span class=\"text-black\">Binance</span>.", diff --git a/src/locales/fr.json b/src/locales/fr.json index 221679ab..263bc74c 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -131,7 +131,8 @@ "remove": "Enlève", "scanToPayBTC": "Scanner pour payer avec bitcoin", "dashboard": { - "ACCOUNT_BALANCE": "SOLDE", + "dashboard": "Tableau de bord", + "accountBalance": "Solde", "bitcoinPrice": "Prix de bitcoin:", "conversionFeeWarning": "Il y a un prix de 1% pour convertir entre bitcoin, Liquid et Lightning.", "dataFromBinance": "Données de <span class=\"text-black\">Binance</span>.", diff --git a/src/routes/[username]/dashboard/+page.svelte b/src/routes/[username]/dashboard/+page.svelte index a580f349..55fe69ee 100644 --- a/src/routes/[username]/dashboard/+page.svelte +++ b/src/routes/[username]/dashboard/+page.svelte @@ -13,48 +13,29 @@ $: animateRate($rate); let animateRate = (newRate) => { - if (!accountBalanceFiat) return; + if (!accountBalanceFiat) return; let t = 0; - let newBalance = (user.account.balance * newRate / sats); - let oldBalance = parseFloat(accountBalanceFiat); - let diff = oldBalance - newBalance; + let newBalance = (user.account.balance * newRate) / sats; + let oldBalance = parseFloat(accountBalanceFiat); + let diff = oldBalance - newBalance; clearInterval(interval); interval = setInterval(() => { let delta = diff * ease(t / 100); - accountBalanceFiat = (oldBalance - delta).toFixed(2) + accountBalanceFiat = (oldBalance - delta).toFixed(2); if (t > 80) clearInterval(interval); t++; }, 10); }; $: user = data.user; - $: accountBalanceFiat = accountBalanceFiat || user && $rate && (user.account.balance * $rate / sats).toFixed(2); + $: accountBalanceFiat = + accountBalanceFiat || (user && $rate && ((user.account.balance * $rate) / sats).toFixed(2)); let payreq = '', payreqField, - withdrawing, loading; - let toggle = async () => { - payreq = ''; - withdrawing = !withdrawing; - await tick(); - if (withdrawing) payreqField.focus(); - }; - - let withdraw = async () => { - loading = true; - try { - await post('/withdraw', { payreq }); - withdrawing = false; - loading = false; - } catch (e) { - failure(e.message); - loading = false; - } - }; - $: btcPrice = f($selectedRate, user.currency); $: accountBalanceBtc = user.account.balance / sats; @@ -75,96 +56,29 @@ <div class="px-3 md:px-0 flex justify-center items-center mt-10 lg:mt-0 mb-20"> <div> <h1 class="text-3xl md:text-4xl font-semibold mb-8"> - {withdrawing ? 'Withdraw' : 'Dashboard'} + {$t('user.dashboard.dashboard')} </h1> - {#if !withdrawing} - <!-- <span class="text-secondary mx-auto text-lg font-bold" --> - <!-- >{$t('user.dashboard.bitcoinPrice')} --> - <!-- <span class="text-black" --> - <!-- >{$selectedRate ? btcPrice : $t('user.dashboard.fetchingRate')}</span --> - <!-- > --> - <!-- </span> --> - <!-- --> - <h3 class="text-secondary font-bold border-b pb-1 mt-10 mb-6 w-full md:w-[500px]"> - {$t('user.dashboard.ACCOUNT_BALANCE')} - </h3> - - <span class="text-3xl font-bold block mb-1">{accountBalanceSats} SAT</span> + <h3 class="text-secondary font-bold border-b pb-1 mt-10 mb-6 w-full md:w-[500px] uppercase"> + {$t('user.dashboard.accountBalance')} + </h3> - <span class="text-secondary text-xl block" - >{$selectedRate ? f(accountBalanceFiat, user.currency) : $t('user.dashboard.fetchingRate')}</span - > - - <div class="md:flex md:space-x-5"> - <button - class="rounded-full border py-2 px-5 font-bold mt-4 block md:inline hover:opacity-80" - on:click={() => goto(`/${user.username}/receive`)}>{$t('user.dashboard.receive')}</button - > - <button - class="rounded-full border py-2 px-5 font-bold mt-5 md:mt-4 hover:opacity-80" - on:click={toggle}>{$t('user.dashboard.withdraw')}</button - > - </div> - {:else} - <div class="space-y-3 w-[250px] md:w-[500px] md:pr-20"> - <button on:click={() => (withdrawing = false)}> - <Icon icon="arrow-left" style="w-8 hover:opacity-80" /> - </button> - - <div> - <!-- found missing translation --> - <label for="invoice" class="font-bold mb-1 block">Lightning Invoice</label> - <input - bind:this={payreqField} - type="text" - name="payreq" - required - bind:value={payreq} - class="block rounded-2xl p-3 w-full bg-primary" - /> - </div> + <span class="text-3xl font-bold block mb-1">{accountBalanceSats} SAT</span> - <!-- TODO --> - <!-- - <div> - <label for="note" class="font-bold mb-1 block">{$t('user.dashboard.optionalNote')}</label> - <textarea name="note" rows={2} class="block border rounded-2xl p-3 w-full bg-primary" /> - </div> ---> + <span class="text-secondary text-xl block" + >{$selectedRate + ? f(accountBalanceFiat, user.currency) + : $t('user.dashboard.fetchingRate')}</span + > - <button - disabled={!payreq} - class="{!payreq - ? 'opacity-50' - : 'opacity-100 hover:opacity-80'} rounded-2xl border py-3 font-bold w-full mt-2 bg-black text-white" - on:click={withdraw} - > - {#if loading} - <svg - class="mx-auto h-6 w-6 animate-spin text-white" - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - > - <circle - class="opacity-25" - cx="12" - cy="12" - r="10" - stroke="currentColor" - stroke-width="4" - /> - <path - class="opacity-75" - fill="currentColor" - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" - /> - </svg> - {:else} - {$t('user.dashboard.send')} - {/if} - </button> - </div> - {/if} + <div class="grid sm:grid-cols-2 gap-2 mt-8"> + <button + class="rounded-full border py-2 px-5 font-bold hover:opacity-80 text-xl w-full mb-2" + on:click={() => goto(`/${user.username}/receive`)}>{$t('user.dashboard.receive')}</button + > + <button + class="rounded-full border py-2 px-5 font-bold hover:opacity-80 text-xl w-full mb-2" + on:click={() => goto('/send')}>{$t('user.dashboard.withdraw')}</button + > + </div> </div> </div> diff --git a/src/routes/scan/+page.svelte b/src/routes/scan/+page.svelte new file mode 100644 index 00000000..5c8c09f6 --- /dev/null +++ b/src/routes/scan/+page.svelte @@ -0,0 +1,24 @@ +<script> + import { Icon } from "$comp"; + import QrScanner from 'qr-scanner'; + import { onMount } from 'svelte'; + import { goBack } from "$lib/utils"; + + let scanner, vid; + onMount(() => { + scanner = new QrScanner(vid, console.log); + scanner.start(); + }); + +</script> + + <button + class="ml-5 md:ml-20 mt-5 md:mt-10 hover:opacity-80" + on:click={goBack} + > + <Icon icon="arrow-left" style="w-10" /> + </button> + + <div class="flex w-full"> +<video bind:this={vid} class="w-full max-w-[600px] mx-auto" /> +</div> diff --git a/src/routes/send/+page.svelte b/src/routes/send/+page.svelte new file mode 100644 index 00000000..777f9766 --- /dev/null +++ b/src/routes/send/+page.svelte @@ -0,0 +1,62 @@ +<script> + import { t } from '$lib/translations'; + import { AppHeader, Icon, Spinner } from '$comp'; + + let payreq = '', + loading, + payreqField; + + let withdraw = async () => { + loading = true; + try { + await post('/withdraw', { payreq }); + loading = false; + } catch (e) { + failure(e.message); + loading = false; + } + }; +</script> + +<AppHeader avatarPosition="left-[calc(50vw-64px)] lg:left-[calc(15vw-64px)]" /> + +<div class="container px-4 mt-20 max-w-xl mx-auto"> + <div> + <!-- found missing translation --> + <label for="invoice" class="font-bold mb-1 block" + >Destination <small class="text-secondary">Address, Invoice, User, Email or Phone</small + ></label + > + + <div class="relative"> + <input + bind:this={payreqField} + type="text" + name="payreq" + required + bind:value={payreq} + class="block rounded-2xl p-3 w-full bg-primary" + /> + + <a href="/scan"> + <div class="flex absolute h-[48px] right-0 top-0 px-2 mt-[1px]"> + <Icon icon="scan" style="mr-2 w-6 my-auto" /> + </div> + </a> + </div> + </div> + + <button + disabled={!payreq} + class="{!payreq + ? 'opacity-50' + : 'opacity-100 hover:opacity-80'} rounded-2xl border py-3 font-bold w-full mt-2 bg-black text-white" + on:click={withdraw} + > + {#if loading} + <Spinner /> + {:else} + {$t('user.dashboard.go')} + {/if} + </button> +</div> |
