diff options
| -rw-r--r-- | package.json | 3 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 119 | ||||
| -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 | ||||
| -rw-r--r-- | static/icons/paste.svg | 10 | ||||
| -rw-r--r-- | static/icons/scan.svg | 3 |
16 files changed, 559 insertions, 131 deletions
diff --git a/package.json b/package.json index a1fdfdde..9c8dadf9 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "type": "module", "dependencies": { - "@lottiefiles/svelte-lottie-player": "^0.2.0" + "@lottiefiles/svelte-lottie-player": "^0.2.0", + "qr-scanner": "^1.4.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 172fa101..249d6f05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,7 @@ specifiers: prettier: ^2.7.1 prettier-plugin-svelte: ^2.7.0 qr-code-styling: 1.6.0-rc.1 + qr-scanner: ^1.4.1 svelte: ^3.50.1 svelte-outclick: ^3.1.0 svelte-pincode: ^2.1.1 @@ -22,9 +23,10 @@ specifiers: dependencies: '@lottiefiles/svelte-lottie-player': 0.2.0 + qr-scanner: 1.4.1 devDependencies: - '@sveltejs/adapter-node': 1.0.0-next.92 + '@sveltejs/adapter-node': 1.0.0-next.96 '@sveltejs/kit': [email protected][email protected] '@zerodevx/svelte-toast': 0.7.2 autoprefixer: [email protected] @@ -84,6 +86,58 @@ packages: resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} dev: true + /@rollup/plugin-commonjs/[email protected]: + resolution: {integrity: sha512-//NdP6iIwPbMTcazYsiBMbJW7gfmpHom33u1beiIoHDEM0Q9clvtQB1T0efvMqHeKsGohiHo97BCPCkBXdscwg==} + engines: {node: '>= 12.0.0'} + peerDependencies: + rollup: ^2.68.0 + dependencies: + '@rollup/pluginutils': [email protected] + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 7.2.3 + is-reference: 1.2.1 + magic-string: 0.25.9 + resolve: 1.22.1 + rollup: 2.78.1 + dev: true + + /@rollup/plugin-json/[email protected]: + resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': [email protected] + rollup: 2.78.1 + dev: true + + /@rollup/plugin-node-resolve/[email protected]: + resolution: {integrity: sha512-5G2niJroNCz/1zqwXtk0t9+twOSDlG00k1Wfd7bkbbXmwg8H8dvgHdIWAun53Ps/rckfvOC7scDBjuGFg5OaWw==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^2.78.0 + dependencies: + '@rollup/pluginutils': [email protected] + '@types/resolve': 1.17.1 + deepmerge: 4.2.2 + is-builtin-module: 3.2.0 + is-module: 1.0.0 + resolve: 1.22.1 + rollup: 2.78.1 + dev: true + + /@rollup/pluginutils/[email protected]: + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.78.1 + dev: true + /@rollup/pluginutils/4.2.1: resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} engines: {node: '>= 8.0.0'} @@ -92,10 +146,13 @@ packages: picomatch: 2.3.1 dev: true - /@sveltejs/adapter-node/1.0.0-next.92: - resolution: {integrity: sha512-HhSZzXN9BlkkoEPMYfHk6BbkYnO/zoLqBLM+oafDZszABvhNXmAMgO7GFFcWxBnZkO7gicLDHYzsOtxxambLfA==} + /@sveltejs/adapter-node/1.0.0-next.96: + resolution: {integrity: sha512-tIHaRolUYy2PiHl4RUWaOsYxEjK5lN9501qzCzFbYr/uoLnZcnPGSXNJICwX0AX9AUkV6cvkZey6bLbUQcwH0Q==} dependencies: - esbuild: 0.15.7 + '@rollup/plugin-commonjs': [email protected] + '@rollup/plugin-json': [email protected] + '@rollup/plugin-node-resolve': [email protected] + rollup: 2.78.1 dev: true /@sveltejs/kit/[email protected][email protected]: @@ -163,14 +220,32 @@ packages: resolution: {integrity: sha512-HheveklTjp3hxpYQhoHfyA6B4bQaUeSV5MQf2usIv/58UF2jY/YqhCAWj9bDBjufbuZc5pSz4BXvdX3WVT+viA==} dev: true + /@types/estree/0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: true + + /@types/estree/1.0.0: + resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} + dev: true + /@types/node/18.7.18: resolution: {integrity: sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==} dev: true + /@types/offscreencanvas/2019.7.0: + resolution: {integrity: sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==} + dev: false + /@types/pug/2.0.6: resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} dev: true + /@types/resolve/1.17.1: + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + dependencies: + '@types/node': 18.7.18 + dev: true + /@types/sass/1.43.1: resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==} dependencies: @@ -266,6 +341,11 @@ packages: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} dev: true + /builtin-modules/3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + /camelcase-css/2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -294,6 +374,10 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /commondir/1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: true + /concat-map/0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} dev: true @@ -584,6 +668,10 @@ packages: engines: {node: '>=6'} dev: true + /estree-walker/1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: true + /estree-walker/2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true @@ -709,6 +797,13 @@ packages: binary-extensions: 2.2.0 dev: true + /is-builtin-module/3.2.0: + resolution: {integrity: sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + /is-core-module/2.10.0: resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} dependencies: @@ -727,11 +822,21 @@ packages: is-extglob: 2.1.1 dev: true + /is-module/1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + dev: true + /is-number/7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} dev: true + /is-reference/1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + dependencies: + '@types/estree': 1.0.0 + dev: true + /kleur/4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -978,6 +1083,12 @@ packages: qrcode-generator: 1.4.4 dev: true + /qr-scanner/1.4.1: + resolution: {integrity: sha512-xiR90NONHTfTwaFgW/ihlqjGMIZg6ExHDOvGQRba1TvV+WVw7GoDArIOt21e+RO+9WiO4AJJq+mwc5f4BnGH3w==} + dependencies: + '@types/offscreencanvas': 2019.7.0 + dev: false + /qrcode-generator/1.4.4: resolution: {integrity: sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==} dev: true 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> diff --git a/static/icons/paste.svg b/static/icons/paste.svg new file mode 100644 index 00000000..eb1412d4 --- /dev/null +++ b/static/icons/paste.svg @@ -0,0 +1,10 @@ +<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;} +</style> +<polyline class="st0" points="23,12 23,18 29,18 23,12 15,12 15,30 29,30 29,18 "/> +<path class="st0" d="M20,6h-8V4c0-1.1,0.9-2,2-2h4c1.1,0,2,0.9,2,2V6z"/> +<path class="st0" d="M15,28h-5c-2.2,0-4-1.8-4-4V8c0-2.2,1.8-4,4-4h2"/> +<path class="st0" d="M20,4h2c2.2,0,4,1.8,4,4v7"/> +</svg> diff --git a/static/icons/scan.svg b/static/icons/scan.svg new file mode 100644 index 00000000..c58313bd --- /dev/null +++ b/static/icons/scan.svg @@ -0,0 +1,3 @@ +<svg width="15px" height="15px" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 1C1.67157 1 1 1.67157 1 2.5V5H0V2.5C0 1.11929 1.11929 0 2.5 0H5V1H2.5ZM12.5 1H10V0H12.5C13.8807 0 15 1.11929 15 2.5V5H14V2.5C14 1.67157 13.3284 1 12.5 1ZM13 8H2V7H13V8ZM0 12.5V10H1V12.5C1 13.3284 1.67157 14 2.5 14H5V15H2.5C1.11929 15 0 13.8807 0 12.5ZM14 12.5V10H15V12.5C15 13.8807 13.8807 15 12.5 15H10V14H12.5C13.3284 14 14 13.3284 14 12.5Z" fill="black"/> +</svg> |
