diff options
| author | Adam Soltys <[email protected]> | 2023-04-20 08:42:02 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2023-04-20 08:42:02 -0700 |
| commit | aa187ee0a4c5d5132fae5255800efaa0c0d425ce (patch) | |
| tree | 50287dcf34b182b68d3493424d40024b688c47ff /src | |
| parent | d7af57e0e48dfce401e486a572664ac11112a164 (diff) | |
| parent | 4b82d5755b52979c11046b6328a95b7f52212f2c (diff) | |
| download | coinos-ui-aa187ee0a4c5d5132fae5255800efaa0c0d425ce.tar.gz coinos-ui-aa187ee0a4c5d5132fae5255800efaa0c0d425ce.zip | |
Merge branch 'master' into static-qr
Diffstat (limited to 'src')
69 files changed, 1425 insertions, 959 deletions
diff --git a/src/app.html b/src/app.html index 4a03a220..c0b7b4c1 100644 --- a/src/app.html +++ b/src/app.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8" /> <link rel="icon" href="%sveltekit.assets%/favicon.svg" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="description" content="An easy bitcoin web wallet" /> <meta name="keywords" content="open source bitcoin lightning web wallet" /> <link rel="manifest" href="/manifest.webmanifest" /> diff --git a/src/components/About.svelte b/src/components/About.svelte index 0a0ff874..4292b9fd 100644 --- a/src/components/About.svelte +++ b/src/components/About.svelte @@ -12,22 +12,7 @@ { type: 'github', url: 'https://github.com/asoltys', color: 'bg-gray-800' } ] }, - { name: 'cole', socials: [] }, - { - name: 'john', - socials: [] - }, - { - name: 'shauna', - socials: [ - { type: 'twitter', url: 'https://twitter.com/ShaunaCollister', color: 'bg-[#1D9BF0]' }, - { - type: 'linkedin', - url: 'https://www.linkedin.com/in/shauna-collister-32905214', - color: 'bg-[#0a66c2]' - } - ] - } + { name: 'cole', socials: [] } ]; </script> diff --git a/src/components/Balance.svelte b/src/components/Balance.svelte index cc923b7d..fddaae3e 100644 --- a/src/components/Balance.svelte +++ b/src/components/Balance.svelte @@ -1,21 +1,34 @@ <script> + import { Pin } from '$comp'; import { f, s, sat, sats } from '$lib/utils'; - import { animatedRate } from '$lib/store'; + import { animatedRate, pin } from '$lib/store'; import { t } from '$lib/translations'; + let show; export let user; </script> +{#if show && user.haspin && $pin?.length !== 6} + <Pin /> +{/if} + <div class="space-y-2"> - <div class="text-5xl font-bold tabular-nums"> - {#if isNaN($animatedRate)} - <div class="text-gray-200">—</div> - {:else} - {f((user.balance * $animatedRate) / sats, user.currency)} - {/if} - </div> + {#if user.haspin && !$pin} + <button + class="text-sm md:text-lg rounded-full border py-2 px-4 hover:opacity-80 min-w-[72px] bg-black text-white" + on:click={() => (show = true)}>Show Balance</button + > + {:else} + <div class="text-5xl font-bold tabular-nums"> + {#if isNaN($animatedRate)} + <div class="text-gray-200">—</div> + {:else} + {f((user.balance * $animatedRate) / sats, user.currency)} + {/if} + </div> - <div class="text-secondary text-2xl"> - ⚡️{s(user.balance)} - </div> + <div class="text-secondary text-2xl"> + ⚡️{s(user.balance)} + </div> + {/if} </div> diff --git a/src/components/Lnurlp.svelte b/src/components/Lnurlp.svelte index ab0c50ce..9918a3f4 100644 --- a/src/components/Lnurlp.svelte +++ b/src/components/Lnurlp.svelte @@ -6,7 +6,9 @@ export let data, form; let { currency } = data.user; - let { minSendable, maxSendable, callback, metadata } = data; + let { minSendable, maxSendable, callback, metadata, rates } = data; + + $: rate = rates[currency]; let amount = Math.round(minSendable / 1000), loading; @@ -33,7 +35,7 @@ </div> {/if} - <Numpad bind:amount {currency} /> + <Numpad bind:amount bind:rate {currency} /> <form action="?/pay" method="POST" use:enhance on:submit={submit}> <input name="amount" value={amount} type="hidden" /> diff --git a/src/components/Lnurlw.svelte b/src/components/Lnurlw.svelte index f2855b53..f780d755 100644 --- a/src/components/Lnurlw.svelte +++ b/src/components/Lnurlw.svelte @@ -7,7 +7,8 @@ export let data, form; let { currency, username } = data.user; - let { defaultDescription, minWithdrawable, maxWithdrawable, k1, callback } = data; + let { defaultDescription, minWithdrawable, maxWithdrawable, k1, callback, rates } = data; + $: rate = rates[currency]; let amount = Math.round(maxWithdrawable / 1000), loading; @@ -28,7 +29,7 @@ </div> {/if} - <Numpad bind:amount {currency} /> + <Numpad bind:amount {currency} bind:rate /> <form action="?/withdraw" method="POST" use:enhance on:submit={submit}> <input name="amount" value={amount} type="hidden" /> diff --git a/src/components/LocationsMap.svelte b/src/components/LocationsMap.svelte index 8a1b6afd..20eea6c0 100644 --- a/src/components/LocationsMap.svelte +++ b/src/components/LocationsMap.svelte @@ -38,43 +38,12 @@ locations = locations.filter( (location) => - location['osm_json'].tags && location['osm_json'].tags['payment:coinos'] === 'yes' + location['osm_json'].tags && + location['osm_json'].tags['payment:coinos'] === 'yes' && + location['osm_json'].lat && + location['osm_json'].lon ); - /* const locations = [ - { - lat: 49.27034, - lng: -123.141373, - info: 'Rolf Vancouver <br> 1628 W 1st Ave #231, Vancouver <br> <a href="https://www.rolfbodywork.com" target="_blank" rel="noreferrer">www.rolfbodywork.com</a>' - }, - { lat: 49.26419, lng: -123.15702, info: 'Tacos Jorge <br> 2287 W Broadway, Vancouver' }, - { - lat: 49.28273, - lng: -123.120735, - info: 'Engage Strategies <br> <a href="https://www.linkedin.com/in/shauna-collister-32905214" target="_blank" rel="noreferrer">LinkedIn</a>' - }, - { - lat: 49.28134, - lng: -123.02373, - info: 'Lumota Collection <br> Clothing Design/Upcycling - Appointment Only <br> <a href="tel:778-726-3776">778-726-3776</a> <br> 369 Boundary Rd, Vancouver, BC V5K 2B1 <br> <a href="https://www.instagram.com/lumotacollection/" target="_blank" rel="noreferrer">IG: Lumotacollection</a> <br> <a href="https://www.facebook.com/LumotaCollection/posts" target="_blank" rel="noreferrer">FB: Lumotacollection</a>' - }, - { - lat: 49.22643, - lng: -122.93085, - info: 'Goodwin Contracting <br> Project Management/Consulting - Appointment Only <br> <a href="tel:403-812-1085">403-812-1085</a> <br> 8017 17th Ave, Burnaby, BC V3N 1M5' - }, - { - lat: 51.04813, - lng: -114.07999, - info: 'Goodwin Contracting <br> Project Management/Consulting - Appointment Only <br> <a href="tel:403-812-1085">403-812-1085</a> <br> 8017 17th Ave, Burnaby, BC V3N 1M5' - }, - { - lat: 49.2677, - lng: -123.10124, - info: 'General Strike Coffee <br> Artisan coffee shop <br> 1965 Main St, Vancouver, BC V5T 2K6 <br> <a href="https://www.instagram.com/generalstrikecoffee/" target="_blank" rel="noreferrer">IG: generalstrikecoffee</a>' - } - ];*/ - L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png', { maxZoom: 18, subdomains: ['a', 'b', 'c', 'd'] @@ -100,12 +69,9 @@ let markers = new MarkerClusterGroup(); locations.forEach((location) => { - if (location['deleted_at']) { - return; - } + if (location['deleted_at']) return; location = location['osm_json']; - let marker = L.marker([location.lat, location.lon]); marker.bindPopup( diff --git a/src/components/Numpad.svelte b/src/components/Numpad.svelte index 77a4f38b..5ea1418c 100644 --- a/src/components/Numpad.svelte +++ b/src/components/Numpad.svelte @@ -1,23 +1,24 @@ <script> import { Left, Icon } from '$comp'; - import { selectedRate } from '$lib/store'; import { f, s, post, warning, sat, sats } from '$lib/utils'; import { t } from '$lib/translations'; export let amount, currency, fiat = !amount, + rate, submit = undefined; export let amountFiat = 0; + let arrow = '<'; $: update(amount, amountFiat); let update = (a, f) => { if (fiat) { - amount = Math.round(f / ($selectedRate / sats)); + amount = Math.round(f / (rate / sats)); } else { - amountFiat = ((a * $selectedRate) / sats).toFixed(2); + amountFiat = ((a * rate) / sats).toFixed(2); } }; @@ -30,7 +31,7 @@ AUD: '$', NZD: '$', MXN: '$', - BRL: '$', + BRL: 'R$', HKD: '$', TWD: '$', JPY: '¥', @@ -60,7 +61,7 @@ amount = 0; } } - } else if (value !== '.' && value !== '<' && parseInt(amountFiat + value) > $selectedRate) { + } else if (value !== '.' && value !== '<' && parseInt(amountFiat + value) > rate) { warning($t('user.receive.lessThan1BTCWarning')); } else if (amountFiat !== 0 && amountFiat.match(/\.../)) { return; @@ -184,12 +185,10 @@ type="button" on:click={() => { if (fiat) { - amount = parseInt((amountFiat / ($selectedRate / sats)).toFixed(0)); + amount = parseInt((amountFiat / (rate / sats)).toFixed(0)); } else { amountFiat = - (amount * ($selectedRate / sats)).toFixed(2) > 0.0 - ? (amount * ($selectedRate / sats)).toFixed(2) - : 0; + (amount * (rate / sats)).toFixed(2) > 0.0 ? (amount * (rate / sats)).toFixed(2) : 0; } fiat = !fiat; }}><Icon icon="swap" style="inline hover:opacity-80" /></button diff --git a/src/components/Pin.svelte b/src/components/Pin.svelte index 1d7ed65b..0cff904f 100644 --- a/src/components/Pin.svelte +++ b/src/components/Pin.svelte @@ -1,31 +1,59 @@ <script> import { t } from '$lib/translations'; - import { focus } from '$lib/utils'; + import { post, success, fail, focus } from '$lib/utils'; import { Pinpad } from '$comp'; + import { pin } from '$lib/store'; + import { onMount } from 'svelte'; export let value = ''; export let title = $t('user.settings.verifyPIN'); - export let cancel = () => (pin = ''); + export let cancel = () => (p = ''); + export let persist = true; + export let notify = true; - let pin = ''; + let p = ''; $: update(value); - let update = (v) => (pin = v); + let update = (value) => (p = value); - $: pin.toString().length > 5 && (value = pin.toString()); + $: p.toString().length > 5 && (value = p.toString()); + + let loaded; + onMount(() => setTimeout(() => (loaded = true), 500)); + + $: loaded && checkPin(p); + let checkPin = async (p) => { + if (p?.length !== 6) return; + let result; + try { + result = await post('/pin', { pin: p }); + } catch (e) { + console.log('Pin check failed', e); + } + + if (result) { + if (notify) success('Pin confirmed'); + if (persist) $pin = p; + } else { + fail('Invalid pin'); + value = ''; + } + }; </script> -<div class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> - <div class="relative top-40 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white space-y-5"> - <h1 class="text-center text-2xl font-semibold">{title}</h1> - <Pinpad bind:v={pin} {cancel} /> - <div class="w-full flex"> - <button - class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto" - on:click|preventDefault={cancel} - > - <div class="my-auto">Cancel</div> - </button> +{#if loaded} + <div class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50"> + <div class="relative top-40 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white space-y-5"> + <h1 class="text-center text-2xl font-semibold">{title}</h1> + <Pinpad bind:v={p} {cancel} /> + <div class="w-full flex"> + <button + class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80 mx-auto" + on:click|preventDefault={cancel} + > + <div class="my-auto">Cancel</div> + </button> + </div> </div> </div> -</div> +{/if} diff --git a/src/components/Pinpad.svelte b/src/components/Pinpad.svelte index bf3778f7..f55dc40d 100644 --- a/src/components/Pinpad.svelte +++ b/src/components/Pinpad.svelte @@ -1,7 +1,6 @@ <script> import { Left, Icon } from '$comp'; - import { pin, selectedRate } from '$lib/store'; - import { f, s, focus, post, warning, sat, sats } from '$lib/utils'; + import { focus, post, warning } from '$lib/utils'; import { t } from '$lib/translations'; export let v; diff --git a/src/components/RegisterOrLogin.svelte b/src/components/RegisterOrLogin.svelte index 6c099178..7aedeb9b 100644 --- a/src/components/RegisterOrLogin.svelte +++ b/src/components/RegisterOrLogin.svelte @@ -22,11 +22,7 @@ $: need2fa = form?.message === '2fa'; let cancel = () => (need2fa = false); - $: if (form?.message === '2fa' && form.token === token) { - token = ''; - console.log('TOKEN', token); - fail('Invalid token, try again'); - } + $: if (form?.message === '2fa' && form.token === token) token = ''; let username, email, btn; @@ -71,7 +67,7 @@ </script> {#if need2fa} - <Pin bind:value={token} title="Enter 2FA Code" {cancel} /> + <Pin bind:value={token} title="Enter 2FA Code" {cancel} notify={false} /> {/if} <div class="pt-10"> diff --git a/src/lib/nostr.js b/src/lib/nostr.js index d72f48ef..1924643e 100644 --- a/src/lib/nostr.js +++ b/src/lib/nostr.js @@ -1,60 +1,39 @@ import { get } from 'svelte/store'; -import * as ecc from 'tiny-secp256k1'; -import { generateMnemonic, entropyToMnemonic, mnemonicToEntropy, mnemonicToSeedSync } from 'bip39'; import { Buffer } from 'buffer'; -import { bech32m } from 'bech32'; -import { bip32, fail, wait, post, stretch } from '$lib/utils'; -import { calculateId, signId } from 'nostr'; +import { fail, wait, post, stretch } from '$lib/utils'; import { decrypted, password as pw, passwordPrompt, pin, loginRedirect } from '$lib/store'; import { goto, invalidate } from '$app/navigation'; +import { getPublicKey, signEvent, getEventHash, nip04, nip06 } from 'nostr-tools'; +import { mnemonicToEntropy, entropyToMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english.js'; +import { bech32m } from '@scure/base'; -const { encode, decode, fromWords, toWords } = bech32m; +const { generateSeedWords, privateKeyFromSeedWords } = nip06; +const { encode, decode, toWords, fromWords } = bech32m; export let generate = async (user) => { - let password = get(pw); - let mnemonic, key, seed, entropy, cipher, salt, child; - if (!get(pin)?.length === 6) return; + let password = get(pw); - mnemonic = generateMnemonic(); - seed = mnemonicToSeedSync(mnemonic); - entropy = mnemonicToEntropy(mnemonic); - key = bip32.fromSeed(seed); - child = key.derivePath("m/44'/1237'/0'/0/0"); - salt = crypto.getRandomValues(new Uint8Array(16)); + let salt = crypto.getRandomValues(new Uint8Array(16)); + let mnemonic = generateSeedWords(); + let sk = privateKeyFromSeedWords(mnemonic); let bytes = new Uint8Array( await crypto.subtle.encrypt( { name: 'AES-GCM', iv: new Uint8Array(16) }, await stretch(password, salt), - Uint8Array.from(Buffer.from(entropy, 'hex')) + Uint8Array.from(Buffer.from(mnemonicToEntropy(mnemonic, wordlist), 'hex')) ) ); - user.pubkey = Buffer.from(ecc.xOnlyPointFromPoint(child.publicKey)).toString('hex'); + user.pubkey = getPublicKey(sk); user.cipher = encode('en', toWords(bytes), 180); user.salt = Buffer.from(salt).toString('hex'); }; export let encrypt = async ({ message, recipient, user }) => { - let sharedPoint = Buffer.from( - ecc.pointMultiply( - Uint8Array.from(Buffer.from('02' + recipient, 'hex')), - Buffer.from(await getPrivateKey(user), 'hex') - ) - ); - - let iv = crypto.getRandomValues(new Uint8Array(16)); - let cipher = await crypto.subtle.encrypt( - { name: 'AES-CBC', iv }, - await crypto.subtle.importKey('raw', sharedPoint.slice(1), 'AES-CBC', true, [ - 'encrypt', - 'decrypt' - ]), - new TextEncoder().encode(message) - ); - - return Buffer.from(cipher).toString('base64') + '?iv=' + Buffer.from(iv).toString('base64'); + return nip04.encrypt(await getPrivateKey(user), recipient, message); }; export let decrypt = async ({ event, user }) => { @@ -63,22 +42,8 @@ export let decrypt = async ({ event, user }) => { let { content, pubkey, id } = event; if (cache[id]) return cache[id]; if (pubkey === user.pubkey) pubkey = event.tags[0][1]; - pubkey = Uint8Array.from(Buffer.from('02' + pubkey, 'hex')); - - let sharedPoint = Buffer.from( - ecc.pointMultiply(pubkey, Buffer.from(await getPrivateKey(user), 'hex')) - ); - let [cipher, iv] = content.split('?iv='); - cipher = Uint8Array.from(Buffer.from(cipher, 'base64')); - iv = Uint8Array.from(Buffer.from(iv, 'base64')); - - let key = await crypto.subtle.importKey('raw', sharedPoint.slice(1), 'AES-CBC', true, [ - 'encrypt', - 'decrypt' - ]); - - let message = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, key, cipher); + let message = await nip04.decrypt(await getPrivateKey(user), pubkey, content); cache[id] = Buffer.from(message).toString('utf8'); decrypted.set(cache); @@ -111,27 +76,22 @@ let getPrivateKey = async (user) => { password = get(pw); let mnemonic, key, seed, entropy, child, privkey; - entropy = Buffer.from( + + entropy = new Uint8Array( await crypto.subtle.decrypt( { name: 'AES-GCM', iv: new Uint8Array(16) }, await stretch(password, Buffer.from(salt, 'hex')), Uint8Array.from(fromWords(decode(cipher, 180).words)) - ), - 'hex' - ).toString('hex'); - - mnemonic = entropyToMnemonic(entropy); - seed = mnemonicToSeedSync(mnemonic); - key = bip32.fromSeed(seed); - child = key.derivePath("m/44'/1237'/0'/0/0"); + ) + ); - return child.privateKey; + mnemonic = entropyToMnemonic(entropy, wordlist); + return privateKeyFromSeedWords(mnemonic); }; export let sign = async ({ event, user }) => { - let privkey = await getPrivateKey(user); - event.id = await calculateId(event); - event.sig = await signId(privkey, event.id); + event.id = getEventHash(event); + event.sig = signEvent(event, await getPrivateKey(user)); }; export let send = (event) => { diff --git a/src/lib/samples.js b/src/lib/samples.js index 93b9aa24..14d9e4e0 100644 --- a/src/lib/samples.js +++ b/src/lib/samples.js @@ -7,6 +7,13 @@ export default { "username": "adam", "password": "hunter2" }'`, + payments: `curl "https://coinos.io/api/payments" + -H "application/json" + -H "Authorization: Bearer $token" +}'`, + rates: `curl "https://coinos.io/api/rates" + -H "application/json" +}'`, invoice: `curl "https://coinos.io/api/invoice" -H "application/json" -H "Authorization: Bearer $token" @@ -19,5 +26,19 @@ export default { }, }'`, fetchInvoice: `curl "https://coinos.io/api/invoice/bc1qmhfk9stzffhd9umzmld92vff7zg3mdlh7rvvaj" - -H "application/json"'` + -H "application/json"'`, + invoiceResponse: `{ + "amount":3141 + "tip":0 + "type":"bitcoin" + "prompt":false + "rate":31836.9702667 + "hash":"bc1qmhfk9stzffhd9umzmld92vff7zg3mdlh7rvvaj" + "text":"bitcoin:bc1qmhfk9stzffhd9umzmld92vff7zg3mdlh7rvvaj?amount=0.00003141" + "currency":"CAD" + "uid":"a9770421-3f65-11ed-9f57-0242ac2a0004" + "received":0 + "created":1677537428134 +}`, + socketAuth: `echo '{"type":"login","data":{"username":"user","password":"password"}}' | websocat -n https://coinos.io/ws` }; diff --git a/src/lib/socket.js b/src/lib/socket.js index aef8a513..e8859fa6 100644 --- a/src/lib/socket.js +++ b/src/lib/socket.js @@ -7,7 +7,7 @@ import { browser } from '$app/environment'; let socket, token; -export const auth = () => token && send('login', token) && send('heartbeat'); +export const auth = () => token && send('login', token); export const send = (type, data) => { socket?.readyState === 1 && socket.send(JSON.stringify({ type, data })); diff --git a/src/lib/store.js b/src/lib/store.js index 6853386c..4e88c90c 100644 --- a/src/lib/store.js +++ b/src/lib/store.js @@ -32,7 +32,7 @@ const persistLocal = (key, defaultValue) => { return s; }; -export const animatedRate = writable(0); +export const animatedRate = persistLocal('rate', 0); export const avatar = writable(); export const banner = writable(); export const colorTheme = writable('from-primary to-gradient'); @@ -46,10 +46,9 @@ export const newPayment = persistLocal('newPayment'); export const password = persistLocal('password'); export const passwordPrompt = writable(); export const payments = persistLocal('transactions', []); -export const pin = persistSession('pin', undefined); +export const pin = writable(); export const rate = writable(); export const request = writable(); export const requestRedirect = writable(); -export const selectedRate = writable(); export const token = persistSession('token'); export const user = writable(); diff --git a/src/lib/translations/index.js b/src/lib/translations/index.js index dfd7f47f..8d1b9feb 100644 --- a/src/lib/translations/index.js +++ b/src/lib/translations/index.js @@ -28,14 +28,14 @@ const config = { loader: async () => (await import('../../locales/en.json')).default }, { - locale: 'fr', + locale: 'es', key: '', - loader: async () => (await import('../../locales/fr.json')).default + loader: async () => (await import('../../locales/es.json')).default }, { - locale: 'es', + locale: 'fr', key: '', - loader: async () => (await import('../../locales/es.json')).default + loader: async () => (await import('../../locales/fr.json')).default }, { locale: 'pt', @@ -51,6 +51,16 @@ const config = { locale: 'zh', key: '', loader: async () => (await import('../../locales/zh.json')).default + }, + { + locale: 'fa', + key: '', + loader: async () => (await import('../../locales/fa.json')).default + }, + { + locale: 'el', + key: '', + loader: async () => (await import('../../locales/el.json')).default } ] }; diff --git a/src/lib/translations/lang.json b/src/lib/translations/lang.json index c607ebd0..0acc1de6 100644 --- a/src/lib/translations/lang.json +++ b/src/lib/translations/lang.json @@ -4,5 +4,7 @@ "es": "Español", "de": "Deutsch", "pt": "Português", - "zh": "中国人" + "zh": "中国人", + "fa": "فارسی ", + "el": "Ελληνικά" } diff --git a/src/lib/utils.js b/src/lib/utils.js index 7b10c638..5eef79fc 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,5 +1,3 @@ -import * as ecc from 'tiny-secp256k1'; -import { BIP32Factory } from 'bip32'; import { browser } from '$app/environment'; import { toast } from '@zerodevx/svelte-toast'; import { goto } from '$app/navigation'; @@ -12,11 +10,11 @@ export function scroll(section) { setTimeout(() => section.scrollIntoView({ behavior: 'smooth' }), 500); } -const base = browser ? '' : PUBLIC_COINOS_URL; +let base = browser ? '' : PUBLIC_COINOS_URL; -export const punk = (k) => Math.floor((parseInt(k.slice(-2), 16) / 256) * 64) + 1 + '.webp'; +export let punk = (k) => Math.floor((parseInt(k.slice(-2), 16) / 256) * 64) + 1 + '.webp'; -export const g = (url, fetch, headers) => +export let g = (url, fetch, headers) => fetch(base + url, { headers }) .then((r) => r.text()) .then((body) => { @@ -27,7 +25,7 @@ export const g = (url, fetch, headers) => } }); -export const get = (url, headers = {}) => +export let get = (url, headers = {}) => fetch(base + url, { headers }) .then(async (r) => { if (r.ok) return r.text(); @@ -41,7 +39,7 @@ export const get = (url, headers = {}) => } }); -export const post = (url, body, headers) => { +export let post = (url, body, headers) => { headers = { ...headers, 'content-type': 'application/json', accept: 'application/json' }; return fetch(base + url, { method: 'POST', body: JSON.stringify(body), headers }) .then(async (r) => { @@ -71,7 +69,7 @@ export const post = (url, body, headers) => { }); }; -export const copy = (text) => { +export let copy = (text) => { navigator.clipboard.writeText(text); success('Copied!'); }; @@ -87,7 +85,7 @@ export function reverseFormat(val, locale) { export let protectedRoutes = [/customers/, /settings/, /payments/]; -export const success = (m) => { +export let success = (m) => { toast.pop(); toast.push(m, { theme: { @@ -96,7 +94,7 @@ export const success = (m) => { }); }; -export const warning = (m) => { +export let warning = (m) => { toast.pop(); toast.push(m, { theme: { @@ -104,7 +102,7 @@ export const warning = (m) => { } }); }; -export const fail = (m) => { +export let fail = (m) => { toast.pop(); toast.push(m, { theme: { @@ -112,7 +110,7 @@ export const fail = (m) => { } }); }; -export const info = (m) => { +export let info = (m) => { toast.pop(); toast.push(m, { theme: { @@ -121,7 +119,7 @@ export const info = (m) => { }); }; -export const login = async (user, cookies) => { +export let login = async (user, cookies) => { let maxAge = 30 * 24 * 60 * 60; let res = await fetch(base + '/login', { @@ -145,17 +143,17 @@ export const login = async (user, cookies) => { cookies.set('token', token, opts); }; -export const auth = (cookies) => ({ authorization: `Bearer ${cookies.get('token')}` }); +export let auth = (cookies) => ({ authorization: `Bearer ${cookies.get('token')}` }); -export const fiat = (amount, rate) => (amount * rate) / sats; -export const fd = async (req) => { +export let fiat = (amount, rate) => (amount * rate) / sats; +export let fd = async (req) => { let obj = Object.fromEntries(await req.formData()); for (let k in obj) (obj[k] === 'undefined' && (obj[k] = undefined)) || (obj[k] === 'false' && (obj[k] = false)); return obj; }; -export const f = (s, currency) => +export let f = (s, currency) => new Intl.NumberFormat('en-US', { style: 'currency', currency @@ -163,8 +161,8 @@ export const f = (s, currency) => .format(s) .replace('CA', ''); -export const s = (s) => new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(s); -export const sat = (s) => { +export let s = (s) => new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(s); +export let sat = (s) => { s = Math.abs(s); let p = Math.floor(Math.log(s) / Math.LN10 + 0.000000001); let d = Math.floor((p + 1) / 3); @@ -179,20 +177,20 @@ export const sat = (s) => { ); }; -export const sats = 100000000; +export let sats = 100000000; -export const back = () => browser && history.go(-1); -export const focus = (el) => browser && screen.width > 1280 && setTimeout(() => el.focus(), 1); +export let back = () => browser && (history.length ? history.go(-1) : goto('/')); +export let focus = (el) => browser && screen.width > 1280 && setTimeout(() => el.focus(), 1); -export const sleep = (n) => new Promise((r) => setTimeout(r, n)); -export const wait = async (f, n = 100, s = 300) => { +export let sleep = (n) => new Promise((r) => setTimeout(r, n)); +export let wait = async (f, n = 100, s = 300) => { let i = 0; while (!(await f()) && i < s) (await sleep(n)) && i++; if (i >= s) throw new Error('timeout'); return f(); }; -export const stretch = async (password, salt) => +export let stretch = async (password, salt) => crypto.subtle.deriveKey( { name: 'PBKDF2', @@ -212,8 +210,6 @@ export const stretch = async (password, salt) => ['encrypt', 'decrypt'] ); -export const bip32 = await BIP32Factory(ecc); - function get_bigrams(string) { var s = string.toLowerCase(); var v = s.split(''); @@ -240,10 +236,12 @@ export function ss(str1, str2) { return 0.0; } -export const types = { +export let types = { bitcoin: 'bitcoin', lightning: 'lightning', internal: 'internal', pot: 'pot', classic: 'classic' }; + +export let ease = (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t); diff --git a/src/locales/el.json b/src/locales/el.json new file mode 100644 index 00000000..28f7db2b --- /dev/null +++ b/src/locales/el.json @@ -0,0 +1,283 @@ +{ + "404": { + "404": "404", + "pageNotFound": "Page not found." + }, + "about": { + "header": "About", + "p1": "Coinos is an open source bitcoin payment app. Development started in Vancouver in September 2012 as a way to provide local merchants with a convenient way to accept bitcoin payments.", + "p2": "You can download the source code to run an instance and connect it to your own bitcoin and lightning nodes. We'll help you get started being a benevolent custodian and payments provider for your community.", + "p3": "Account transfers amongst users on our server are done off-chain so they're completely free, instant, and unlimited.", + "team": { + "header": "The Team", + "adam": { + "name": "Adam Soltys", + "position": "Chief Technical Officer", + "bio": "Adam is a web developer and bitcoin enthusiast who believes sound money is key to increasing global prosperity. He lives in North Vancouver with his wife Chelsea and two children." + }, + "cole": { + "name": "Cole", + "position": "Certified Bitcoin Professional", + "bio": "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." + } + }, + "locations": { + "header": "Locations", + "description": "Here are some businesses that are accepting bitcoin with Coinos. We hope that yours will be next!" + } + }, + "error": { + "header": "Error", + "message": "Please try again or contact support.", + "signIn": "Please sign in to continue." + }, + "faq": { + "header": "FAQ", + "cost": { + "question": "What does it cost?", + "answer": "It's free to register an account and receive payments. We charge a 0.1% conversion fee if you withdraw regular bitcoin to lightning or vice versa." + }, + "compatibility": { + "question": "Do I need any special device or software?", + "answer": "No. Coinos is web based, so as long as you have an internet connection and a web browser, it works anywhere. Retail merchants might use it on a phone or a tablet." + }, + "safety": { + "question": "When do my funds settle?", + "answer": "Lightning payments are settled instantly. You can withdraw your balance to another wallet or exchange immediately once funds are received." + } + }, + "footer": { + "company": "Company", + "followUs": "Follow Us", + "github": "GitHub", + "linktree": "Linktree", + "jobs": "Jobs", + "support": "Support", + "telegram": "Telegram", + "twitter": "Twitter", + "documentation": "Documentation" + }, + "howItWorks": { + "header": "How it works", + "step1": { + "header": "1. Create your account", + "description": "A username and password is all you need to get started." + }, + "step2": { + "header": "2. Ask for payments", + "description": "The same as you normally would, but with bitcoin." + }, + "step3": { + "header": "3. That's all!", + "description": "Spend your coin immediately or save it and watch it grow, the choice is yours." + } + }, + "invoice": { + "addTip": "Add a tip", + "addTipq": "Add a tip?", + "done": "Done", + "paymentSuccessful": "Payment received!", + "invoice": "Invoice", + "total": "Total", + "tip": "Tip" + }, + "jobs": { + "header": "Jobs", + "subheader": "Current Openings", + "pitch1": "Coinos is actively looking for developers to join our growing team (full-time or part-time, remote working OK).", + "pitch2": "Excited about working on open source bitcoin? Then take a look at our repositories on", + "github": "GitHub", + "pitch3": "If you think you can make an impact, we'd love to hear from you. Send us your resume to" + }, + "landing": { + "header_before_icon": "The", + "header_with_icon": "easiest", + "header_after_icon": "way to get started with bitcoin.", + "subheader": "A simple but powerful web wallet for everyone. No software or hardware required.", + "info1": { + "title": "Instant payments via Lightning", + "description": "We make it easy for merchants to receive retail payments. No waiting for bank settlements or worrying about chargebacks. Instant settlement, in seconds. Say goodbye to 3% credit card processing fees." + }, + "info2": { + "title": "Simple to use", + "description1": "Just enter an amount in your local currency, share the QR code and you’re done.", + "description2": "We do all the heavy lifting so you can focus on your business.", + "description3": "You always have the option of taking custody of your funds by withdrawing instantly at any time." + }, + "info3": { + "title": "Brand it your way!", + "description": "Customize your profile page look and feel, or keep it on default - it's totally up to you." + } + }, + "login": { + "optional": "Optional", + "token": "2FA Token", + "username": "Username", + "password": "Password", + "email": "Email", + "passwordRecommendation": "We recommend a long, random password generated with a password manager.", + "forgotUserOrPassword": "Forgot Username or Password?", + "haveAccount": "Already have an account?", + "noAccount": "Don't have an account?", + "register": "Register", + "signIn": "Sign In", + "failed": "Login failed" + }, + "nav": { + "profile": "Profile", + "startInSeconds": "Start in Seconds", + "account": "Account", + "signIn": "Sign In", + "signOut": "Sign Out", + "dashboard": "Dashboard", + "settings": "Settings", + "support": "Support" + }, + "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", + "update1": "" + }, + "user": { + "bitcoin": "Bitcoin", + "lightning": "Lightning", + "invoice": "Invoice", + "unfollow": "Unfollow", + "following": "Following", + "follow": "Follow", + "followers": "Followers", + "remove": "Remove", + "noNotes": "No notes yet", + "message": "Message", + "messages": "Messages", + "feed": "Feed", + "notes": "Notes", + "pay": "Pay", + "scanToPayBTC": "Scan to pay with bitcoin", + "this": "This", + "lightningAddress": "lightning address", + "likeAnEmail": "is like an email for bitcoin. You can publish it on your website or business cards and use it multiple times.", + "dashboard": { + "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>.", + "fetchingRate": "fetching rate...", + "optionalNote": "Note (Optional)", + "send": "Send", + "receive": "Receive", + "withdraw": "Send", + "deposit": "Deposit", + "voucher": "Voucher" + }, + "receive": { + "lessThan1BTCWarning": "Please enter an amount less than 1 bitcoin.", + "showQR": "Show QR" + }, + "send": { + "contacts": "Contacts", + "scan": "Scan", + "paste": "Paste", + "next": "Next", + "placeholder": "Paste a bitcoin address, lightning invoice or coinos username" + }, + "settings": { + "saveSettings": "Save Settings", + "bannerImage": "Banner Image", + "profileImage": "Profile Image", + "oneTimeCode": "Enter one-time code", + "accountId": "Account Secret", + "header": "Settings", + "ACCOUNT": "ACCOUNT", + "POINT_OF_SALE": "PREFERENCES", + "SECURITY": "SECURITY", + "locale": "Language", + "username": "Username", + "email": "Email", + "newPassword": "Change Password", + "about": "About", + "aboutPlaceholder": "A little something about you or your business address and hours", + "emailNotifications": "Email Notifications", + "emailNotificationsDescription": "Get notified via email when a change occurs on your account or when people send you payments.", + "smsNotifications": "SMS Notifications", + "smsNotificationsDescription": "Coinos will send you text notifications when this is enabled.", + "theme": "Color Theme", + "tipPrompt": "Prompt for tips", + "tipPromptDescription": "Ask people if they want to add a tip before paying their invoices", + "displayName": "Display name", + "themeDescription": "Select from the default color themes.", + "localCurrency": "Currency", + "btcUnit": "Display bitcoin in:", + "bitcoin": "bitcoin", + "minutes": "minutes", + "hour": "hour", + "hours": "hours", + "satoshis": "Satoshis", + "securityPIN": "Security PIN", + "securityPINDescription": "Set a security pin that will be used to unlock your settings and to send outgoing payments.", + "setPIN": "Set PIN", + "autoLock": "Auto-Lock Timer", + "autoLockDescription": "Your account will go into a secure mode, requiring you to re-enter your credentials after a specified time:", + "twofa": "2-Factor Authentication", + "twofaDescription": "Add an extra level of security to your device by enabling 2-factor authentication.", + "twofaSetup": "Set up 2FA", + "twofaDisable": "Disable 2FA", + "select": "Select", + "verifyPIN": "Verify PIN", + "enablePIN": "Enable PIN", + "disablePIN": "Disable PIN" + }, + "support": { + "header": "Support", + "email": "Email", + "accountName": "Account Name", + "message": "Message", + "send": "Send" + } + }, + "payments": { + "amount": "Amount", + "date": "Date", + "id": "Transaction ID", + "details": "Transaction Details", + "header": "Payments", + "preimage": "Proof of payment", + "type": "Type", + "time": "Time", + "sent": "Sent", + "received": "Received", + "empty": "No transactions yet.", + "send": "Send", + "next": "Next", + "sendersChoice": "Sender's choice", + "sendTo": "Send to", + "to": "to", + "pay": "Pay", + "requesting": "is requesting", + "sentInvoiceTo": "Sent invoice to", + "sentRequestTo": "Request sent!", + "pleasePay": "Please pay", + "day": "Day", + "week": "Week", + "month": "Month", + "all": "All", + "export": "Export", + "invoice": "Invoice", + "readyToPay": "is ready to pay", + "describeOrder": "Use this field to describe your order", + "requestAnInvoice": "Request Invoice", + "submit": "Submit", + "isRequesting": "is requesting", + "invoiceFrom": "Invoice from", + "awaiting": "Awaiting", + "tapAnywhere": "Tap anywhere to continue", + "from": "from", + "redeem": "Redeem", + "redeemed": "Redeemed", + "redeemedBy": "Redeemed by", + "sendMessage": "Write a message..." + } +} diff --git a/src/locales/en.json b/src/locales/en.json index 9a50d6b4..28f7db2b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -15,20 +15,10 @@ "position": "Chief Technical Officer", "bio": "Adam is a web developer and bitcoin enthusiast who believes sound money is key to increasing global prosperity. He lives in North Vancouver with his wife Chelsea and two children." }, - "shauna": { - "name": "Shauna", - "position": "Operations & Marketing", - "bio": "Strategic & Project Management. Jack of all Trades. Can always find time to meet for coffee." - }, "cole": { "name": "Cole", "position": "Certified Bitcoin Professional", "bio": "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." - }, - "john": { - "name": "John Barratt", - "position": "Full Stack Web Developer", - "bio": "When not coding you can find John out in the woods hiking all the great trails that BC has to offer." } }, "locations": { @@ -45,15 +35,15 @@ "header": "FAQ", "cost": { "question": "What does it cost?", - "answer": "It's free to register an account and receive payments over the lightning network. We charge a 0.1% fee to convert from lightning to bitcoin." + "answer": "It's free to register an account and receive payments. We charge a 0.1% conversion fee if you withdraw regular bitcoin to lightning or vice versa." }, "compatibility": { "question": "Do I need any special device or software?", - "answer": "No. Coinos is web-based, so as long as you have an internet connection, you can use it on any device. Retail merchants might use it on a phone or a tablet." + "answer": "No. Coinos is web based, so as long as you have an internet connection and a web browser, it works anywhere. Retail merchants might use it on a phone or a tablet." }, "safety": { "question": "When do my funds settle?", - "answer": "Lightning payments are settled instantly. You can withdraw your balance to another wallet or exchange right after payments are received." + "answer": "Lightning payments are settled instantly. You can withdraw your balance to another wallet or exchange immediately once funds are received." } }, "footer": { @@ -79,7 +69,7 @@ }, "step3": { "header": "3. That's all!", - "description": "Spend your coin at other merchants or save it for a rainy day, the choice is yours." + "description": "Spend your coin immediately or save it and watch it grow, the choice is yours." } }, "invoice": { @@ -102,11 +92,11 @@ "landing": { "header_before_icon": "The", "header_with_icon": "easiest", - "header_after_icon": "way to accept bitcoin in your store.", - "subheader": "Grow your customer base and revenue by accepting bitcoin with an easy-to-use web app. No software or hardware required.", + "header_after_icon": "way to get started with bitcoin.", + "subheader": "A simple but powerful web wallet for everyone. No software or hardware required.", "info1": { "title": "Instant payments via Lightning", - "description": "The moment your customer sends a payment, it’s there. No waiting for bank settlements or worrying about chargebacks. Instant settlement, in seconds. Say goodbye to 3% credit card processing fees." + "description": "We make it easy for merchants to receive retail payments. No waiting for bank settlements or worrying about chargebacks. Instant settlement, in seconds. Say goodbye to 3% credit card processing fees." }, "info2": { "title": "Simple to use", @@ -116,7 +106,7 @@ }, "info3": { "title": "Brand it your way!", - "description": "Customize your point of sale look and feel, or keep it on default - it's totally up to you." + "description": "Customize your profile page look and feel, or keep it on default - it's totally up to you." } }, "login": { @@ -195,6 +185,7 @@ "placeholder": "Paste a bitcoin address, lightning invoice or coinos username" }, "settings": { + "saveSettings": "Save Settings", "bannerImage": "Banner Image", "profileImage": "Profile Image", "oneTimeCode": "Enter one-time code", diff --git a/src/locales/es.json b/src/locales/es.json index 8e68a5f3..9f6c6e1b 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -15,31 +15,10 @@ "position": "Director de Tecnología", "bio": "Adam es un desarrollador web y un entusiasta de bitcoin que cree que el dinero sólido es clave para aumentar la prosperidad global. Vive en la ciudad de North Vancouver con su esposa Chelsea y sus dos hijos." }, - "secondl1ght": { - "name": "secondl1ght", - "position": "Desarrollador Web", - "bio": "Desarrollador web autodidacta, se sumergió de cabeza en con el bitcoin y dejó su trabajo fiduciario para concentrarse en el desarrollo de bitcoin a tiempo completo. Contribuye en proyectos de código abierto, es un defensor de la privacidad y un amante de la libertad." - }, - - "karnage": { - "name": "Karnage", - "position": "Diseñador de Producto", - "bio": "Mi misión es ayudar a los fundadores de startups a tener éxito. Creo productos para lograr estos objetivos. Diseño de producto con píxeles perfectos. Haz que te lo envíen." - }, - "shauna": { - "name": "Shauna", - "position": "Operaciones y Mercadeo", - "bio": "Gestión Estratégica y de Proyectos. Sé un poco de todo. Siempre habrá tiempo para compartir un buen café." - }, "cole": { "name": "Cole", "position": "Profesional de Bitcoin", "bio": "Como profesional certificado de Bitcoin, Cole es una persona motivada y apasionada por la libertad. Conoció el bitcoin en 2016 y lo estudió en profundidad. Ahora quiere presentarle la Red Lightning a tantos usuarios como sea posible." - }, - "john": { - "name": "John Barratt", - "position": "Desarrollador Web Full Stack", - "bio": "Cuando no está programando, podemos encontrar a John en el bosque recorriendo todos los grandes senderos que British Columbia tiene para ofrecer." } }, "locations": { @@ -165,6 +144,7 @@ "invoice": "Factura", "unfollow": "Dejar de seguir", "following": "Siguiendo", + "followers": "Seguidores", "follow": "Seguir", "remove": "Eliminar", "scanToPayBTC": "Escanear para pagar con bitcoin", @@ -198,6 +178,9 @@ "placeholder": "Asigne una dirección de bitcoin, una factura lightning o un usuario de coinos" }, "settings": { + "saveSettings": "Guardar ajustes", + "tipPrompt": "Solicitar propinas", + "tipPromptDescription": "Pregunta a las personas si quieren agregar una propina antes de pagar sus facturas", "bannerImage": "Imagen del Banner", "profileImage": "Imagen de perfil", "oneTimeCode": "Ingrese el código de un solo uso", @@ -243,7 +226,7 @@ "send": "Enviar" } }, - "transactions": { + "payments": { "header": "Transacciones", "AMOUNT": "CANTIDAD", "TYPE": "TIPO", @@ -253,7 +236,7 @@ "empty": "Aún no hay transacciones.", "send": "Enviado", "next": "Siguiente", - "sendersChoice": "Sender's choice", + "sendersChoice": "Elección del remitente", "sendTo": "Enviar a", "to": "para", "pay": "Pagar", diff --git a/src/locales/fa.json b/src/locales/fa.json new file mode 100644 index 00000000..28f7db2b --- /dev/null +++ b/src/locales/fa.json @@ -0,0 +1,283 @@ +{ + "404": { + "404": "404", + "pageNotFound": "Page not found." + }, + "about": { + "header": "About", + "p1": "Coinos is an open source bitcoin payment app. Development started in Vancouver in September 2012 as a way to provide local merchants with a convenient way to accept bitcoin payments.", + "p2": "You can download the source code to run an instance and connect it to your own bitcoin and lightning nodes. We'll help you get started being a benevolent custodian and payments provider for your community.", + "p3": "Account transfers amongst users on our server are done off-chain so they're completely free, instant, and unlimited.", + "team": { + "header": "The Team", + "adam": { + "name": "Adam Soltys", + "position": "Chief Technical Officer", + "bio": "Adam is a web developer and bitcoin enthusiast who believes sound money is key to increasing global prosperity. He lives in North Vancouver with his wife Chelsea and two children." + }, + "cole": { + "name": "Cole", + "position": "Certified Bitcoin Professional", + "bio": "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." + } + }, + "locations": { + "header": "Locations", + "description": "Here are some businesses that are accepting bitcoin with Coinos. We hope that yours will be next!" + } + }, + "error": { + "header": "Error", + "message": "Please try again or contact support.", + "signIn": "Please sign in to continue." + }, + "faq": { + "header": "FAQ", + "cost": { + "question": "What does it cost?", + "answer": "It's free to register an account and receive payments. We charge a 0.1% conversion fee if you withdraw regular bitcoin to lightning or vice versa." + }, + "compatibility": { + "question": "Do I need any special device or software?", + "answer": "No. Coinos is web based, so as long as you have an internet connection and a web browser, it works anywhere. Retail merchants might use it on a phone or a tablet." + }, + "safety": { + "question": "When do my funds settle?", + "answer": "Lightning payments are settled instantly. You can withdraw your balance to another wallet or exchange immediately once funds are received." + } + }, + "footer": { + "company": "Company", + "followUs": "Follow Us", + "github": "GitHub", + "linktree": "Linktree", + "jobs": "Jobs", + "support": "Support", + "telegram": "Telegram", + "twitter": "Twitter", + "documentation": "Documentation" + }, + "howItWorks": { + "header": "How it works", + "step1": { + "header": "1. Create your account", + "description": "A username and password is all you need to get started." + }, + "step2": { + "header": "2. Ask for payments", + "description": "The same as you normally would, but with bitcoin." + }, + "step3": { + "header": "3. That's all!", + "description": "Spend your coin immediately or save it and watch it grow, the choice is yours." + } + }, + "invoice": { + "addTip": "Add a tip", + "addTipq": "Add a tip?", + "done": "Done", + "paymentSuccessful": "Payment received!", + "invoice": "Invoice", + "total": "Total", + "tip": "Tip" + }, + "jobs": { + "header": "Jobs", + "subheader": "Current Openings", + "pitch1": "Coinos is actively looking for developers to join our growing team (full-time or part-time, remote working OK).", + "pitch2": "Excited about working on open source bitcoin? Then take a look at our repositories on", + "github": "GitHub", + "pitch3": "If you think you can make an impact, we'd love to hear from you. Send us your resume to" + }, + "landing": { + "header_before_icon": "The", + "header_with_icon": "easiest", + "header_after_icon": "way to get started with bitcoin.", + "subheader": "A simple but powerful web wallet for everyone. No software or hardware required.", + "info1": { + "title": "Instant payments via Lightning", + "description": "We make it easy for merchants to receive retail payments. No waiting for bank settlements or worrying about chargebacks. Instant settlement, in seconds. Say goodbye to 3% credit card processing fees." + }, + "info2": { + "title": "Simple to use", + "description1": "Just enter an amount in your local currency, share the QR code and you’re done.", + "description2": "We do all the heavy lifting so you can focus on your business.", + "description3": "You always have the option of taking custody of your funds by withdrawing instantly at any time." + }, + "info3": { + "title": "Brand it your way!", + "description": "Customize your profile page look and feel, or keep it on default - it's totally up to you." + } + }, + "login": { + "optional": "Optional", + "token": "2FA Token", + "username": "Username", + "password": "Password", + "email": "Email", + "passwordRecommendation": "We recommend a long, random password generated with a password manager.", + "forgotUserOrPassword": "Forgot Username or Password?", + "haveAccount": "Already have an account?", + "noAccount": "Don't have an account?", + "register": "Register", + "signIn": "Sign In", + "failed": "Login failed" + }, + "nav": { + "profile": "Profile", + "startInSeconds": "Start in Seconds", + "account": "Account", + "signIn": "Sign In", + "signOut": "Sign Out", + "dashboard": "Dashboard", + "settings": "Settings", + "support": "Support" + }, + "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", + "update1": "" + }, + "user": { + "bitcoin": "Bitcoin", + "lightning": "Lightning", + "invoice": "Invoice", + "unfollow": "Unfollow", + "following": "Following", + "follow": "Follow", + "followers": "Followers", + "remove": "Remove", + "noNotes": "No notes yet", + "message": "Message", + "messages": "Messages", + "feed": "Feed", + "notes": "Notes", + "pay": "Pay", + "scanToPayBTC": "Scan to pay with bitcoin", + "this": "This", + "lightningAddress": "lightning address", + "likeAnEmail": "is like an email for bitcoin. You can publish it on your website or business cards and use it multiple times.", + "dashboard": { + "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>.", + "fetchingRate": "fetching rate...", + "optionalNote": "Note (Optional)", + "send": "Send", + "receive": "Receive", + "withdraw": "Send", + "deposit": "Deposit", + "voucher": "Voucher" + }, + "receive": { + "lessThan1BTCWarning": "Please enter an amount less than 1 bitcoin.", + "showQR": "Show QR" + }, + "send": { + "contacts": "Contacts", + "scan": "Scan", + "paste": "Paste", + "next": "Next", + "placeholder": "Paste a bitcoin address, lightning invoice or coinos username" + }, + "settings": { + "saveSettings": "Save Settings", + "bannerImage": "Banner Image", + "profileImage": "Profile Image", + "oneTimeCode": "Enter one-time code", + "accountId": "Account Secret", + "header": "Settings", + "ACCOUNT": "ACCOUNT", + "POINT_OF_SALE": "PREFERENCES", + "SECURITY": "SECURITY", + "locale": "Language", + "username": "Username", + "email": "Email", + "newPassword": "Change Password", + "about": "About", + "aboutPlaceholder": "A little something about you or your business address and hours", + "emailNotifications": "Email Notifications", + "emailNotificationsDescription": "Get notified via email when a change occurs on your account or when people send you payments.", + "smsNotifications": "SMS Notifications", + "smsNotificationsDescription": "Coinos will send you text notifications when this is enabled.", + "theme": "Color Theme", + "tipPrompt": "Prompt for tips", + "tipPromptDescription": "Ask people if they want to add a tip before paying their invoices", + "displayName": "Display name", + "themeDescription": "Select from the default color themes.", + "localCurrency": "Currency", + "btcUnit": "Display bitcoin in:", + "bitcoin": "bitcoin", + "minutes": "minutes", + "hour": "hour", + "hours": "hours", + "satoshis": "Satoshis", + "securityPIN": "Security PIN", + "securityPINDescription": "Set a security pin that will be used to unlock your settings and to send outgoing payments.", + "setPIN": "Set PIN", + "autoLock": "Auto-Lock Timer", + "autoLockDescription": "Your account will go into a secure mode, requiring you to re-enter your credentials after a specified time:", + "twofa": "2-Factor Authentication", + "twofaDescription": "Add an extra level of security to your device by enabling 2-factor authentication.", + "twofaSetup": "Set up 2FA", + "twofaDisable": "Disable 2FA", + "select": "Select", + "verifyPIN": "Verify PIN", + "enablePIN": "Enable PIN", + "disablePIN": "Disable PIN" + }, + "support": { + "header": "Support", + "email": "Email", + "accountName": "Account Name", + "message": "Message", + "send": "Send" + } + }, + "payments": { + "amount": "Amount", + "date": "Date", + "id": "Transaction ID", + "details": "Transaction Details", + "header": "Payments", + "preimage": "Proof of payment", + "type": "Type", + "time": "Time", + "sent": "Sent", + "received": "Received", + "empty": "No transactions yet.", + "send": "Send", + "next": "Next", + "sendersChoice": "Sender's choice", + "sendTo": "Send to", + "to": "to", + "pay": "Pay", + "requesting": "is requesting", + "sentInvoiceTo": "Sent invoice to", + "sentRequestTo": "Request sent!", + "pleasePay": "Please pay", + "day": "Day", + "week": "Week", + "month": "Month", + "all": "All", + "export": "Export", + "invoice": "Invoice", + "readyToPay": "is ready to pay", + "describeOrder": "Use this field to describe your order", + "requestAnInvoice": "Request Invoice", + "submit": "Submit", + "isRequesting": "is requesting", + "invoiceFrom": "Invoice from", + "awaiting": "Awaiting", + "tapAnywhere": "Tap anywhere to continue", + "from": "from", + "redeem": "Redeem", + "redeemed": "Redeemed", + "redeemedBy": "Redeemed by", + "sendMessage": "Write a message..." + } +} diff --git a/src/locales/pt.json b/src/locales/pt.json index 117a5101..309511f9 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -1,277 +1,283 @@ { "404": { "404": "404", - "pageNotFound": "Page not found." + "pageNotFound": "Página não encontrada." }, "about": { - "header": "About", - "p1": "Coinos is an open source bitcoin payment app. Development started in Vancouver in September 2012 as a way to provide local merchants with a convenient way to accept bitcoin payments.", - "p2": "You can download the source code to run an instance and connect it to your own bitcoin and lightning nodes. We'll help you get started being a benevolent custodian and payments provider for your community.", - "p3": "Account transfers amongst users on our server are done off-chain so they're completely free, instant, and unlimited.", + "header": "Sobre", + "p1": "O Coinos é uma aplicação de pagamento de bitcoin de código aberto. O desenvolvimento começou em Vancouver em Setembro de 2012 como forma de fornecer aos comerciantes locais uma forma conveniente de aceitar pagamentos de bitcoin.", + "p2": "Você pode baixar o código fonte para executar uma instância e ligá-lo aos seus próprios nós de bitcoin e da Lightning Network. Vamos ajudá-lo a começar a ser um depositário benevolente e fornecedor de pagamentos para a sua comunidade..", + "p3": "As transferências de conta entre utilizadores no nosso servidor são feitas off-chain, por isso são completamente gratuitas, instantâneas e ilimitadas.", "team": { - "header": "The Team", + "header": "O time", "adam": { "name": "Adam Soltys", - "position": "Chief Technical Officer", - "bio": "Adam is a web developer and bitcoin enthusiast who believes sound money is key to increasing global prosperity. He lives in North Vancouver with his wife Chelsea and two children." - }, - "secondl1ght": { - "name": "secondl1ght", - "position": "Web Developer", - "bio": "A self-taught Web Developer, secondl1ght dove head first down the bitcoin rabbit hole and left his fiat job to focus on bitcoin development full-time. He contributes to open source projects, is a privacy advocate, and freedom lover." - }, - - "karnage": { - "name": "Karnage", - "position": "Product Designer", - "bio": "My mission is to help startup founders succeed. I create products to achieve this goal. Pixel-perfect product design. Get it shipped." - }, - "shauna": { - "name": "Shauna", - "position": "Operations & Marketing", - "bio": "Strategic & Project Management. Jack of all Trades. Can always find time to meet for coffee." + "position": "Responsável Técnico Principal", + "bio": "Adam é um desenvolvedor web e um entusiasta do bitcoin que acredita que o dinheiro sólido é a chave para aumentar a prosperidade global. Ele vive em North Vancouver com a sua esposa Chelsea e dois filhos." }, "cole": { "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." - }, - "john": { - "name": "John Barratt", - "position": "Full Stack Web Developer", - "bio": "When not coding you can find John out in the woods hiking all the great trails that BC has to offer." + "position": "Profissional Certificado de Bitcoin", + "bio": "Cole é um indivíduo motivado com uma paixão pela liberdade. Encontrou-se com o bitcoin em 2016 e estudou-o em profundidade. Agora quer introduzir a rede lightning ao maior número de utilizadores possível." } }, "locations": { - "header": "Locations", - "description": "Here are some businesses that are accepting bitcoin with Coinos. We hope that yours will be next!" + "header": "Locais", + "description": "Aqui estão algumas empresas que estão aceitando bitcoin com a Coinos. Esperamos que o seu seja o próximo!" } }, "error": { - "header": "Error", - "message": "Please try again or contact support.", - "signIn": "Please sign in to continue." + "header": "Erro", + "message": "Por favor tente novamente ou contate o suporte.", + "signIn": "Por favor, faça o login para continuar." }, "faq": { "header": "FAQ", "cost": { - "question": "What does it cost?", - "answer": "It's free to register an account and receive payments over the lightning network. We charge a 0.1% fee to convert from lightning to bitcoin." + "question": "Quanto custa?", + "answer": "É grátis para registrar uma conta e receber pagamentos. Cobramos uma taxa de conversão de 0,1% se você retirar o bitcoin da rede principal para a lightning ou vice-versa.." }, "compatibility": { - "question": "Do I need any special device or software?", - "answer": "No. Coinos is web-based, so as long as you have an internet connection, you can use it on any device. Retail merchants might use it on a phone or a tablet." + "question": "Eu preciso de algum dispositivo ou software especial?", + "answer": "Não. Coinos baseado na web, desde que você tenha uma conexão com a Internet e um navegador web, ele funciona em qualquer lugar. Os comerciantes de varejo podem usá-lo em um telefone, computador ou tablet." }, "safety": { - "question": "When do my funds settle?", - "answer": "Lightning payments are settled instantly. You can withdraw your balance to another wallet or exchange right after payments are received." + "question": "Como é a liquidez?", + "answer": "Os pagamentos pela lightning são liquidados instantaneamente. Você pode retirar seu saldo para outra carteira ou trocar imediatamente assim que os fundos forem recebidos." } }, "footer": { - "company": "Company", - "followUs": "Follow Us", + "company": "Empresa", + "followUs": "Siga-nos", "github": "GitHub", - "jobs": "Jobs", - "support": "Support", + "linktree": "Linktree", + "jobs": "Empregos", + "support": "Suporte", "telegram": "Telegram", "twitter": "Twitter", - "news": "News" + "documentation": "Documentação" }, "howItWorks": { - "header": "How it works", + "header": "Como funciona", "step1": { - "header": "1. Create your account", - "description": "A username and password is all you need to get started." + "header": "1. Crie sua conta", + "description": "Um nome de usuário e senha é tudo o que você precisa para começar." }, "step2": { - "header": "2. Ask for payments", - "description": "The same as you normally would, but with bitcoin." + "header": "2. Solicite pagamentos", + "description": "O mesmo que você normalmente faria, mas com bitcoin." }, "step3": { - "header": "3. That's all!", - "description": "Spend your coin at other merchants or save it for a rainy day, the choice is yours." + "header": "3. Pronto!", + "description": "Gaste sua moeda imediatamente ou guarde-a e veja-a crescer, a escolha é sua." } }, "invoice": { - "addTip": "Add a tip", - "addTipq": "Add a tip?", - "done": "Done", - "paymentSuccessful": "Payment received!", - "invoice": "Invoice", + "addTip": "Adicionar gorjeta", + "addTipq": "Adicionar gorjeta?", + "done": "Feito", + "paymentSuccessful": "Pagamento recebido!", + "invoice": "Fatura", "total": "Total", - "tip": "Tip" + "tip": "Gorjeta" }, "jobs": { - "header": "Jobs", - "subheader": "Current Openings", - "pitch1": "Coinos is actively looking for developers to join our growing team (full-time or part-time, remote working OK).", - "pitch2": "Excited about working on open source bitcoin? Then take a look at our repositories on", + "header": "Vagas de emprego", + "subheader": "Atualmente abertas", + "pitch1": "Coinos está procurando ativamente desenvolvedores para se juntar à nossa crescente equipe (tempo integral ou meio-período, trabalho remoto OK).", + "pitch2": "Entusiasmado com o trabalho em bitcoin open source? Então dê uma olhada em nossos repositórios em", "github": "GitHub", - "pitch3": "If you think you can make an impact, we'd love to hear from you. Send us your resume to" + "pitch3": "Se você acha que pode causar um impacto, gostaríamos muito de ouvir de você. Envie-nos seu currículo para" }, "landing": { - "header_before_icon": "The", - "header_with_icon": "easiest", - "header_after_icon": "way to accept bitcoin in your store.", - "subheader": "Grow your customer base and revenue by accepting bitcoin with an easy-to-use web app. No software or hardware required.", + "header_before_icon": "O", + "header_with_icon": "jeito mais fácil", + "header_after_icon": "de iniciar no Bitcoin.", + "subheader": "Uma carteira web simples, mas poderosa para todos. Não requer software nem hardware.", "info1": { - "title": "Instant payments via Lightning", - "description": "The moment your customer sends a payment, it’s there. No waiting for bank settlements or worrying about chargebacks. Instant settlement, in seconds. Say goodbye to 3% credit card processing fees." + "title": "Pagamentos instantâneos via Lightning", + "description": "Facilitamos para os comerciantes o recebimento de pagamentos de varejo. Nada de esperar por liquidações bancárias ou se preocupar com chargebacks. Liquidação instantânea, em segundos. Diga adeus a 3% de taxas de processamento de cartão de crédito.." }, "info2": { - "title": "Simple to use", - "description1": "Just enter an amount in your local currency, share the QR code and you’re done.", - "description2": "We do all the heavy lifting so you can focus on your business.", - "description3": "You always have the option of taking custody of your funds by withdrawing instantly at any time." + "title": "Simples de usar", + "description1": "Basta inserir a quantia em sua moeda local, compartilhar o QR code e pronto..", + "description2": "Nós fazemos todo o trabalho pesado para que você possa se concentrar no seu negócio.", + "description3": "Você sempre tem a opção de tomar para sí a custódia de seus fundos, retirando-se imediatamente a qualquer momento." }, "info3": { - "title": "Brand it your way!", - "description": "Customize your point of sale look and feel, or keep it on default - it's totally up to you." + "title": "Personalise do seu jeito!", + "description": "Personalize a aparência de sua página de perfil, ou mantenha o padrão - você escolhe." } }, "login": { - "optional": "Optional", + "optional": "Opcional", "token": "2FA Token", - "username": "Username", - "password": "Password", + "username": "Usuário", + "password": "Senha", "email": "Email", - "passwordRecommendation": "We recommend a long, random password generated with a password manager.", - "forgotUserOrPassword": "Forgot Username or Password?", - "haveAccount": "Already have an account?", - "noAccount": "Don't have an account?", - "register": "Register", - "signIn": "Sign In", - "failed": "Login failed" + "passwordRecommendation": "Recomendamos uma senha longa e aleatória gerada com um gerenciador de senhas.", + "forgotUserOrPassword": "Esqueceu o usuário ou senha?", + "haveAccount": "Já tem uma conta?", + "noAccount": "Não tem uma conta?", + "register": "Registre", + "signIn": "Entrar", + "failed": "Falha no login" }, "nav": { - "profile": "Profile", - "startInSeconds": "Start in Seconds", - "account": "Account", - "signIn": "Sign In", - "signOut": "Sign Out", - "dashboard": "Dashboard", - "settings": "Settings", - "support": "Support" + "profile": "Perfil", + "startInSeconds": "Inicie em segundos", + "account": "Conta", + "signIn": "Entrar", + "signOut": "Sair", + "dashboard": "Painel de controle", + "settings": "Ajustes", + "support": "Suporte" }, "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", + "header": "Notícias", + "subheader": "Últimos Updates", "update1": "" }, "user": { "bitcoin": "Bitcoin", "lightning": "Lightning", - "invoice": "Invoice", - "unfollow": "Unfollow", - "following": "Following", - "follow": "Follow", - "remove": "Remove", - "scanToPayBTC": "Scan to pay with bitcoin", - "this": "This", - "lightningAddress": "lightning address", - "likeAnEmail": "is like an email for bitcoin. You can publish it on your website or business cards and use it multiple times.", + "invoice": "Fatura", + "unfollow": "Deixar de seguir", + "following": "Seguindo", + "follow": "Seguir", + "followers": "Seguidores", + "remove": "Remover", + "noNotes": "Nenhuma nota ainda", + "message": "Mensagem", + "messages": "Mensagens", + "feed": "Feed", + "notes": "Notas", + "pay": "Pagar", + "scanToPayBTC": "Escaneie para pagar com bitcoin", + "this": "Este", + "lightningAddress": "endereço lightning", + "likeAnEmail": "é como um email para o bitcoin. Você pode publicá-lo em seu site ou cartão de visita e usá-lo várias vezes.", "dashboard": { - "go": "Go", - "dashboard": "Dashboard", - "accountBalance": "Account Balance", - "bitcoinPrice": "bitcoin Price:", - "conversionFeeWarning": "You will be charged a 1% withdrawal fee on the amount.", + "go": "Ir", + "dashboard": "Painel de controle", + "accountBalance": "Saldo na conta", + "bitcoinPrice": "Preço do bitcoin:", + "conversionFeeWarning": "Será cobrada uma taxa de retirada de 1% sobre o valor.", "dataFromBinance": "Data from <span class=\"text-black\">Binance</span>.", - "fetchingRate": "fetching rate...", - "optionalNote": "Note (Optional)", - "send": "Send", - "receive": "Receive", - "withdraw": "Send", + "fetchingRate": "buscando taxa...", + "optionalNote": "Nota (Opcional)", + "send": "Enviar", + "receive": "Receber", + "withdraw": "Enviar", "deposit": "Deposit", "voucher": "Voucher" }, "receive": { - "lessThan1BTCWarning": "Please enter an amount less than 1 bitcoin.", - "showQR": "Show QR" + "lessThan1BTCWarning": "Favor inserir uma quantidade inferior a 1 bitcoin.", + "showQR": "Mostrar QR code" }, "send": { - "contacts": "Contacts", - "scan": "Scan", - "paste": "Paste", - "next": "Next", - "placeholder": "Paste a bitcoin address, lightning invoice or coinos username" + "contacts": "Contatos", + "scan": "Escanear", + "paste": "Colar", + "next": "Continuar", + "placeholder": "Colar um endereço de bitcoin, fatura lightning ou nome de usuário na coinos" }, "settings": { - "bannerImage": "Banner Image", - "profileImage": "Profile Image", - "oneTimeCode": "Enter one-time code", - "accountId": "Account Secret", - "header": "Settings", - "ACCOUNT": "ACCOUNT", - "POINT_OF_SALE": "POINT OF SALE", - "SECURITY": "SECURITY", - "locale": "Language", - "username": "Username", + "saveSettings": "Salvar ajustes", + "bannerImage": "Imagem do banner", + "profileImage": "Imagem do perfil", + "oneTimeCode": "Entre com o código único", + "accountId": "ID da conta", + "header": "Ajustes", + "ACCOUNT": "CONTA", + "POINT_OF_SALE": "PREFERÊNCIAS", + "SECURITY": "SEGURANÇA", + "locale": "Idioma", + "username": "Usuário", "email": "Email", - "newPassword": "New Password", - "businessAddress": "Business Address", - "emailNotifications": "Email Notifications", - "emailNotificationsDescription": "Get notified via email when a change occurs on your account or when people send you payments.", - "smsNotifications": "SMS Notifications", - "smsNotificationsDescription": "Coinos will send you text notifications when this is enabled.", - "theme": "Color Theme", - "themeDescription": "Select from the default color themes.", - "localCurrency": "Local Currency", - "btcUnit": "Display bitcoin in:", + "newPassword": "Alterar senhar", + "about": "Sobre", + "aboutPlaceholder": "Um pouco sobre você ou seu endereço comercial e horários", + "emailNotifications": "Notificações por email", + "emailNotificationsDescription": "Ser notificado por e-mail quando ocorrer uma mudança em sua conta ou quando as pessoas lhe enviarem pagamentos.", + "smsNotifications": "Notificações por SMS", + "smsNotificationsDescription": "A Coinos lhe enviará notificações por SMS quando isto estiver habilitado.", + "theme": "Cor do tema", + "tipPrompt": "Mostrar opção gorjetas", + "tipPromptDescription": "Pergunte às pessoas se elas querem acrescentar uma gorjeta antes de pagar sua fatura", + "displayName": "Mostrar nome", + "themeDescription": "Selecionar entre os temas de cor padrão.", + "localCurrency": "Moeda", + "btcUnit": "Mostrar bitcoin em:", "bitcoin": "bitcoin", + "minutes": "minutos", + "hour": "hora", + "hours": "horas", "satoshis": "Satoshis", - "securityPIN": "Security PIN", - "securityPINDescription": "Set a security pin that will be used to unlock your settings and to send outgoing payments.", - "setPIN": "Set PIN", - "autoLock": "Auto-Lock Timer", - "autoLockDescription": "Your account will go into a secure mode automatically after a specified time:", - "twofa": "2-Factor Authentication", - "twofaDescription": "Add an extra level of security to your device by enableing 2-factor authentication.", - "twofaSetup": "Set up 2FA", - "twofaDisable": "Disable 2FA", - "select": "Select", - "verifyPIN": "Verify PIN", - "enablePIN": "Enable PIN", - "disablePIN": "Disable PIN" + "securityPIN": "PIN de segurança", + "securityPINDescription": "Estabeleça um PIN de segurança que será usado para desbloquear suas configurações e para enviar pagamentos.", + "setPIN": "Insira o PIN", + "autoLock": "Bloqueio automático", + "autoLockDescription": "Sua conta entrará em um modo seguro, exigindo que você reentre suas credenciais após um tempo especificado:", + "twofa": "Eutenticação de 2 fatores", + "twofaDescription": "Adicione um nível extra de segurança ao seu dispositivo, permitindo autenticação de 2 fatores.", + "twofaSetup": "Habilitar o 2FA", + "twofaDisable": "Desabilitar o 2FA", + "select": "Selecionar", + "verifyPIN": "Verificar PIN", + "enablePIN": "Habilitar PIN", + "disablePIN": "Desabilitar PIN" }, "support": { - "header": "Support", + "header": "Suporte", "email": "Email", - "accountName": "Account Name", - "message": "Message", - "send": "Send" + "accountName": "Usuário", + "message": "Mensagem", + "send": "Enviar" } }, "payments": { - "header": "Transactions", - "AMOUNT": "AMOUNT", - "TYPE": "TYPE", - "TIME": "TIME", - "sent": "Sent", - "received": "Received", - "empty": "No transactions yet.", - "send": "Send", - "next": "Next", - "sendersChoice": "Sender's choice", - "sendTo": "Send to", - "to": "to", - "pay": "Pay", - "requesting": "is requesting", - "sentInvoiceTo": "Sent invoice to", - "sentRequestTo": "Sent request to", - "pleasePay": "Please pay", - "day": "Day", - "week": "Week", - "month": "Month", - "all": "All", - "export": "Export", - "invoice": "Invoice", - "readyToPay": "is ready to pay", - "describeOrder": "Use this field to describe your order", - "requestAnInvoice": "Request an invoice", - "submit": "Submit", - "isRequesting": "is requesting", - "awaiting": "Awaiting" + "amount": "Quantidade", + "date": "Data", + "id": "ID de transação", + "details": "Details da transação", + "header": "Pagamentos", + "preimage": "Prova de pagamento", + "type": "Tipo", + "time": "Hora", + "sent": "Enviado", + "received": "Recebido", + "empty": "Nenhuma transação ainda.", + "send": "Enviar", + "next": "Continuar", + "sendersChoice": "A escolha do remetente", + "sendTo": "Enviar para", + "to": "para", + "pay": "Pagar", + "requesting": "solicitando", + "sentInvoiceTo": "Fatura enviada para", + "sentRequestTo": "Solicitação enviada!", + "pleasePay": "Por favor pague", + "day": "Dia", + "week": "Semana", + "month": "Mês", + "all": "Todos", + "export": "Exportar", + "invoice": "Fatura", + "readyToPay": "está pronta para pagar", + "describeOrder": "Use este campo para descrever seu pedido", + "requestAnInvoice": "Solicitar Fatura", + "submit": "O envio", + "isRequesting": "foi solicitado", + "invoiceFrom": "Fatura de", + "awaiting": "Aguardando", + "tapAnywhere": "Toque em qualquer lugar para continuar", + "from": "de", + "redeem": "Resgatar", + "redeemed": "Resgatado", + "redeemedBy": "Resgatado por", + "sendMessage": "Escreva uma mensagem..." } } diff --git a/src/routes/(app)/+layout.server.js b/src/routes/(app)/+layout.server.js index ef278744..e80e1e64 100644 --- a/src/routes/(app)/+layout.server.js +++ b/src/routes/(app)/+layout.server.js @@ -30,6 +30,9 @@ export async function load({ cookies, request, url, params }) { throw redirect(307, '/login'); } + if (user && !user.pubkey && !pathname.includes('generate')) + throw redirect(307, `/${user.username}/generate`); + try { rate = await get('/rate'); rates = await get('/rates'); diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 0dcdc1b2..891de9ac 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -2,7 +2,7 @@ import { SvelteToast } from '@zerodevx/svelte-toast'; import { onDestroy, onMount } from 'svelte'; import { close, connect, send } from '$lib/socket'; - import { last, selectedRate, invoice, rate, user, request, passwordPrompt } from '$lib/store'; + import { last, invoice, request, passwordPrompt, pin } from '$lib/store'; import { page } from '$app/stores'; import { browser } from '$app/environment'; import { goto } from '$app/navigation'; @@ -14,18 +14,13 @@ export let data; - let { subject } = $page.data; - let r, u, token, rates; + let { rate, user, subject, token, rates } = data; $: update(data); - let update = () => { - ({ user: u, rate: r, token, rates } = data); - $rate = r; - $user = u; + let update = (data) => { + ({ rate, user, subject, token, rates } = data); }; - $: $selectedRate = r * (rates[u?.currency || 'USD'] / rates.USD); - onMount(() => { let localStorageLocale = localStorage.getItem(localeLocalStorageKey); if (localStorageLocale) locale.set(localStorageLocale); @@ -34,27 +29,42 @@ if (lng) localStorage.setItem(localeLocalStorageKey, lng); }); - browser && checkSocket(); + if (browser) { + checkSocket(); + expirePin(); + } }); $: browser && connect(token); let lost, - timer, + checkTimer, + expireTimer, counter = 0; let checkSocket = () => { counter++; - lost = Date.now() - $last > 15000; + lost = Date.now() - $last > 30000; if (lost) connect(token); if (counter > 5) { - send('heartbeat'); + send('heartbeat', token); counter = 0; } - timer = setTimeout(checkSocket, 1000); + + checkTimer = setTimeout(checkSocket, 1000); + }; + + let expirePin = () => { + $pin = null; + expireTimer = setTimeout(expirePin, 300000); }; - onDestroy(() => browser && clearTimeout(timer)); + onDestroy(() => { + if (browser) { + clearTimeout(checkTimer); + clearTimeout(expireTimer); + } + }); </script> {#if browser && $passwordPrompt} @@ -78,20 +88,14 @@ <SvelteToast options={{ reversed: true, intro: { y: 192 } }} /> -{#if lost} - <div class="fixed bottom-12 right-12 text-red-600 bg-white z-50 px-4 py-2 rounded-full border"> - Lost connection to server, try refreshing - </div> -{/if} - <main data-sveltekit-prefetch class="pb-20"> <slot /> </main> {#if $invoice} - <Invoice user={u} /> + <Invoice {user} /> {/if} {#if $request} - <Request user={u} /> + <Request {user} /> {/if} diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index bbb2379e..df9b9ad8 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -1,13 +1,13 @@ <script> import { f, s, sats } from '$lib/utils'; - import { animatedRate, selectedRate } from '$lib/store'; + import { animatedRate } from '$lib/store'; import { Icon } from '$comp'; import { t } from '$lib/translations'; import { sign, send } from '$lib/nostr'; export let data; - let events, user, subject, src, text; - $: ({ events, user, subject, src, text } = data); + let events, user, subject, rates, src, text; + $: ({ events, user, rates, subject, src, text } = data); $: ({ currency, username: n, display } = subject); $: username = n.length > 60 ? n.substr(0, 6) : display || n; @@ -42,24 +42,6 @@ $: following = !!user?.follows.find((t) => t.includes(subject.pubkey)); - let ease = (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t); - let i; - - let o = $selectedRate; - $: animate($selectedRate); - let animate = (n) => { - clearInterval(i); - - let t = 0; - let d = o - n; - - i = setInterval(() => { - $animatedRate = (o - d * ease(t / 100)).toFixed(2); - if (t > 80) (o = n) && clearInterval(i); - t++; - }, 10); - }; - let hideBio = true; let toggleBio = () => (hideBio = false); </script> @@ -115,39 +97,31 @@ </button> </div> {/if} + {/if} + {#if !subject.anon && subject.username !== user?.username} <div class="w-full flex"> - <a href={`/${subject.username}/messages`} class="mx-auto"> - <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60"> - <div class="mx-auto flex"> - <Icon icon="support" style="mr-2 my-auto" /> - <div class="mt-1 my-auto">{$t('user.message')}</div> - </div> - </button> - </a> - </div> - - <div class="w-full flex"> - <a href={`/${subject.username}/request`} class="mx-auto"> + <a + href={user ? `/send/${subject.username}` : `/${subject.username}/receive`} + class="mx-auto" + > <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60"> <div class="mx-auto flex"> - <Icon icon="numpad" style="mr-2 my-auto" /> - <div class="mt-1 my-auto">{$t('payments.requestAnInvoice')}</div> + <Icon icon="send" style="mr-2" /> + <div class="mt-1">{$t('user.pay')}</div> </div> </button> </a> </div> {/if} - {#if !subject.anon && subject.username !== user?.username} + + {#if user && user.username !== subject.username && subject.pubkey} <div class="w-full flex"> - <a - href={user ? `/send/${subject.username}` : `/${subject.username}/receive`} - class="mx-auto" - > + <a href={`/${subject.username}/messages`} class="mx-auto"> <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60"> <div class="mx-auto flex"> - <Icon icon="send" style="mr-2" /> - <div class="mt-1">{$t('user.pay')}</div> + <Icon icon="support" style="mr-2 my-auto" /> + <div class="mt-1 my-auto">{$t('user.message')}</div> </div> </button> </a> @@ -163,7 +137,7 @@ </div> </div> -{#if currency && $animatedRate} +{#if currency && !isNaN($animatedRate)} <div class="flex fixed w-full px-4 bg-white py-2 bottom-0 bg-opacity-90 tabular-nums"> <div class="text-secondary flex mr-auto"> <div class="flex mr-1"> diff --git a/src/routes/(app)/[username]/(profile)/+page.server.js b/src/routes/(app)/[username]/(profile)/+page.server.js index d6cf3984..e37db1fe 100644 --- a/src/routes/(app)/[username]/(profile)/+page.server.js +++ b/src/routes/(app)/[username]/(profile)/+page.server.js @@ -5,31 +5,31 @@ export async function load({ cookies, params, parent, url }) { let { pubkey } = subject; let { since = 0 } = params; - let messages, invoices, sent, received; - messages = invoices = sent = received = []; + let messages, invoices, sent, received, notes; + messages = invoices = sent = received = notes = []; + + if (pubkey) { + if (user) { + try { + messages = await get(`/${user.pubkey}/${since}/messages`); + messages = messages.sort((a, b) => b.created_at - a.created_at); + } catch (e) { + console.log(`failed to fetch nostr messages`, e); + } + + ({ invoices, sent, received } = await get('/requests', auth(cookies))); + } - if (user) { try { - messages = await get(`/${user.pubkey}/${since}/messages`); - messages = messages.sort((a, b) => b.created_at - a.created_at); + notes = await get(`/${pubkey}/notes`); } catch (e) { - console.log(`failed to fetch nostr messages`, e); + console.log(`failed to fetch nostr notes for ${pubkey}`, e); } - ({ invoices, sent, received } = await get('/requests', auth(cookies))); + notes.map((e) => { + e.seen = e.created_at; + }); } - let notes = []; - - try { - notes = await get(`/${pubkey}/notes`); - } catch (e) { - console.log(`failed to fetch nostr notes for ${pubkey}`, e); - } - - notes.map((e) => { - e.seen = e.created_at; - }); - return { invoices, messages, notes, sent, received }; } diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte index 09335e9c..308289b0 100644 --- a/src/routes/(app)/[username]/(profile)/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/+page.svelte @@ -2,24 +2,23 @@ import { onMount } from 'svelte'; import { Icon, Avatar, Balance, Feed } from '$comp'; import { t } from '$lib/translations'; - import { f, sat, sats } from '$lib/utils'; + import { ease, f, sat, sats } from '$lib/utils'; import { sign, send, encrypt, decrypt } from '$lib/nostr'; - import { event as e, password } from '$lib/store'; + import { event as e, password, rate, animatedRate } from '$lib/store'; import { browser } from '$app/environment'; export let data; - let { messages, notes, invoices, sent, received, subject, user } = data; + let { messages, notes, invoices, sent, received, subject, user, rates } = data; $: refresh(data); - let refresh = (d) => ({ messages, notes, invoices, sent, received, subject, user } = d); + let refresh = (d) => ({ messages, notes, invoices, sent, received, subject, user, rates } = d); let keys = new Set(); let latest = []; - let i = 0; let ready; e.subscribe(async (event) => { if (!(event && ready)) return; - if (event.recipient.id === user?.id && !~latest.findIndex((m) => m.id === event.id)) { + if (event.recipient?.id === user.id && !~latest.findIndex((m) => m.id === event.id)) { event.content = await decrypt({ event, user }); let i = latest.findIndex((m) => m.pubkey === event.pubkey); @@ -34,22 +33,49 @@ $: initialize($password); let initialize = async (p) => { + let i = 0; ready = false; - if (!p) return; + if (!p || !user) return; while (i < messages.length) { let event = messages[i]; - let k = event.author.id === user.id ? event.recipient.pubkey : event.author.pubkey; + i++; + + if (!(event.author && event.recipient)) continue; + + let k = event.author.id === user.id ? event.recipient?.pubkey : event.author.pubkey; if (!keys.has(k)) { keys.add(k); event.content = await decrypt({ event, user }); if (event.content) latest.push(event); } - i++; } latest = latest; ready = true; }; + + let i; + let o; + + rate.subscribe((n) => { + if (!user?.currency) return; + clearInterval(i); + + let r = ((rates[user.currency] / rates['USD']) * n).toFixed(2); + + if (!r || isNaN(r)) return; + if (!o) o = r; + + let t = 0; + let d = o - r; + + i = setInterval(() => { + if (!r) return; + $animatedRate = (o - d * ease(t / 100)).toFixed(2); + if (t > 80) (o = r) && clearInterval(i); + t++; + }, 10); + }); </script> <div class="space-y-8"> diff --git a/src/routes/(app)/[username]/(profile)/followers/+page.svelte b/src/routes/(app)/[username]/(profile)/followers/+page.svelte index 77b337b6..56b3ba16 100644 --- a/src/routes/(app)/[username]/(profile)/followers/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/followers/+page.svelte @@ -1,4 +1,5 @@ <script> + import { t } from '$lib/translations'; import { Avatar } from '$comp'; import { browser } from '$app/environment'; import VirtualScroll from 'svelte-virtual-scroll-list'; @@ -14,7 +15,9 @@ <svelte:window bind:innerWidth={w} /> <div class="px-3 md:px-0 w-full md:w-[400px] mx-auto space-y-8"> - <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold">Followers</h1> + <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> + {$t('user.followers')} + </h1> {#if browser} {#if followers.length} diff --git a/src/routes/(app)/[username]/(profile)/follows/+page.svelte b/src/routes/(app)/[username]/(profile)/follows/+page.svelte index 78566fc4..b238c3a7 100644 --- a/src/routes/(app)/[username]/(profile)/follows/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/follows/+page.svelte @@ -1,4 +1,5 @@ <script> + import { t } from '$lib/translations'; import { Avatar } from '$comp'; import { browser } from '$app/environment'; import VirtualScroll from 'svelte-virtual-scroll-list'; @@ -13,7 +14,9 @@ <svelte:window bind:innerWidth={w} /> <div class="px-3 md:px-0 w-full md:w-[400px] mx-auto space-y-8"> - <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold">Following</h1> + <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold"> + {$t('user.following')} + </h1> {#if browser} {#if follows.length} diff --git a/src/routes/(app)/[username]/dashboard/+page.server.js b/src/routes/(app)/[username]/dashboard/+page.server.js deleted file mode 100644 index ae66a20d..00000000 --- a/src/routes/(app)/[username]/dashboard/+page.server.js +++ /dev/null @@ -1,4 +0,0 @@ -import { redirect } from '@sveltejs/kit'; -export async function load({ params: { username } }) { - throw redirect(301, `/${username}`); -} diff --git a/src/routes/(app)/[username]/dashboard/+page.svelte b/src/routes/(app)/[username]/dashboard/+page.svelte deleted file mode 100644 index d8761ea9..00000000 --- a/src/routes/(app)/[username]/dashboard/+page.svelte +++ /dev/null @@ -1,163 +0,0 @@ -<script> - import { enhance } from '$app/forms'; - import { f, s, post, fail, sat, sats } from '$lib/utils'; - import { tick } from 'svelte'; - import { Avatar, Icon } from '$comp'; - import { rate, password, selectedRate } from '$lib/store'; - import { goto } from '$app/navigation'; - import { t } from '$lib/translations'; - import { events } from '$lib/store'; - - export let data; - $: ({ invoices, requests } = data); - - let ease = (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t); - let interval, rateInterval; - - let oldRate = $selectedRate; - let animatedRate = oldRate; - $: animateRate($selectedRate); - let animateRate = (newRate) => { - if (!accountBalanceFiat) return; - let t = 0; - let newBalance = (user.balance * newRate) / sats; - let oldBalance = parseFloat(accountBalanceFiat); - let diff = oldBalance - newBalance; - let rateDiff = oldRate - newRate; - - clearInterval(rateInterval); - rateInterval = setInterval(() => { - let delta = rateDiff * ease(t / 100); - animatedRate = (oldRate - delta).toFixed(2); - if (t > 80) clearInterval(interval); - t++; - }, 10); - - oldRate = newRate; - - clearInterval(interval); - interval = setInterval(() => { - let delta = diff * ease(t / 100); - accountBalanceFiat = (oldBalance - delta).toFixed(2); - if (t > 80) clearInterval(interval); - t++; - }, 10); - }; - - $: user = data.user; - $: accountBalanceFiat = - accountBalanceFiat || - (user && $selectedRate && ((user.balance * $selectedRate) / sats).toFixed(2)); - - let payreq = '', - payreqField, - loading; - - $: btcPrice = f($selectedRate, user.currency); - - $: accountBalanceBtc = user.balance / sats; -</script> - -<div class="container px-4 max-w-md mx-auto mt-20 space-y-12 mb-8"> - <div class="mt-24 space-y-2"> - <div class="text-5xl font-bold"> - {$selectedRate ? f(accountBalanceFiat, user.currency) : $t('user.dashboard.fetchingRate')} - </div> - - <div class="text-secondary text-2xl"> - ⚡️{s(user.balance)} - </div> - </div> - - <div class="grid sm:grid-cols-2 gap-2 text-lg"> - <a href="/send"> - <button class="rounded-full border py-4 px-5 font-bold hover:opacity-80 w-full" - >{$t('user.dashboard.send')}</button - > - </a> - <a href={`/${user.username}/receive`}> - <button class="rounded-full border py-4 px-5 font-bold hover:opacity-80 w-full" - >{$t('user.dashboard.receive')}</button - > - </a> - </div> - - <!-- <div> --> - <!-- {#each invoices as { request_id, amount, currency, rate, user: recipient, requester, id }} --> - <!-- {@const requested = user.username === requester?.username} --> - <!-- <a href={`/invoice/${id}` + (requested ? '/tip' : '')}> --> - <!-- <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> --> - <!-- <div class="flex gap-2"> --> - <!-- <div> --> - <!-- {#if requested} --> - <!-- <div class="flex"> --> - <!-- <Avatar user={recipient} size={20} /> --> - <!-- <div class="my-auto text-left"> --> - <!-- <p class="text-secondary">{$t('payments.invoiceFrom')}</p> --> - <!-- <p class="ml-1 text-lg break-words">{recipient.username}</p> --> - <!-- </div> --> - <!-- </div> --> - <!-- {:else} --> - <!-- <div class="flex"> --> - <!-- <Avatar user={requester} size={20} /> --> - <!-- <div class="my-auto text-left"> --> - <!-- <p class="text-secondary">{$t('payments.awaiting')}</p> --> - <!-- <p class="ml-1 text-lg break-words">{requester.username}</p> --> - <!-- </div> --> - <!-- </div> --> - <!-- {/if} --> - <!-- </div> --> - <!-- <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> --> - <!-- <div class="font-bold"> --> - <!-- {f(amount * (rate / sats), currency)} --> - <!-- </div> --> - <!-- --> - <!-- <div class="text-secondary">{sat(amount)}</div> --> - <!-- </div> --> - <!-- </div> --> - <!-- </div> --> - <!-- </a> --> - <!-- {/each} --> - <!-- </div> --> - - <!-- <div> --> - <!-- {#each requests as { id, invoice, memo, requester: r }} --> - <!-- <a href={`/${user.username}/receive/${id}`}> --> - <!-- <div class="border-b p-2 last:border-b-0 hover:bg-gray-100"> --> - <!-- <div class="flex"> --> - <!-- <div> --> - <!-- <div class="flex"> --> - <!-- <Avatar user={r} size={20} /> --> - <!-- <div class="my-auto text-left"> --> - <!-- <p class="ml-1 text-lg break-words">{r.username}</p> --> - <!-- <p class="ml-1 text-secondary">{memo}</p> --> - <!-- </div> --> - <!-- </div> --> - <!-- </div> --> - <!-- <div class="whitespace-nowrap my-auto ml-auto flex gap-2"> --> - <!-- <button class="rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full" --> - <!-- >Invoice</button --> - <!-- > --> - <!-- </div> --> - <!-- </div> --> - <!-- </div> --> - <!-- </a> --> - <!-- {/each} --> - <!-- </div> --> -</div> - -<div class="flex fixed w-full px-4 bg-white py-2 bottom-0 bg-opacity-90"> - <div class="text-secondary flex mr-auto"> - <div class="flex mr-1"> - <div class="my-auto mr-1">1</div> - <img src="/images/bitcoin.svg" class="w-5 my-auto" alt="Bitcoin" /> - </div> - <div>= {f(animatedRate, user.currency)}</div> - </div> - <div class="text-secondary flex ml-auto"> - <div class="flex"> - <div class="mr-1">⚡️{s((1 * sats) / animatedRate)} =</div> - <div>{f(1, user.currency)}</div> - </div> - </div> -</div> diff --git a/src/routes/(app)/[username]/generate/+page.svelte b/src/routes/(app)/[username]/generate/+page.svelte index 32a38592..3548ccd4 100644 --- a/src/routes/(app)/[username]/generate/+page.svelte +++ b/src/routes/(app)/[username]/generate/+page.svelte @@ -1,9 +1,6 @@ <script> import { fail, post, wait } from '$lib/utils'; - import { Buffer } from 'buffer'; import { onMount } from 'svelte'; - import { bech32m } from 'bech32'; - import { generateMnemonic, mnemonicToEntropy, mnemonicToSeedSync } from 'bip39'; import { goto, invalidate } from '$app/navigation'; import { Pin } from '$comp'; import { loginRedirect, pin, password, passwordPrompt } from '$lib/store'; diff --git a/src/routes/(app)/[username]/generate/+server.js b/src/routes/(app)/[username]/generate/+server.js index 1335c3e2..e1df08e3 100644 --- a/src/routes/(app)/[username]/generate/+server.js +++ b/src/routes/(app)/[username]/generate/+server.js @@ -3,7 +3,8 @@ import { error, json } from '@sveltejs/kit'; export async function POST({ cookies, request }) { try { - return json(await post(`/user`, await request.json(), auth(cookies))); + let body = await request.json(); + return json(await post(`/user`, body, auth(cookies))); } catch (e) { throw error(500, e.message); } diff --git a/src/routes/(app)/[username]/invoice/+server.js b/src/routes/(app)/[username]/invoice/+server.js index 1321739e..461d380c 100644 --- a/src/routes/(app)/[username]/invoice/+server.js +++ b/src/routes/(app)/[username]/invoice/+server.js @@ -1,5 +1,7 @@ import { auth, post } from '$lib/utils'; import { json } from '@sveltejs/kit'; -export let POST = async ({ cookies: c, request: r }) => - json(await post('/invoice', await r.json(), auth(c))); +export let POST = async ({ cookies: c, request: r }) => { + let j = await r.json(); + return json(await post('/invoice', j, auth(c))); +}; diff --git a/src/routes/(app)/[username]/invoice/[hash]/+layout.server.js b/src/routes/(app)/[username]/invoice/[hash]/+layout.server.js index b5323d3f..875c4e11 100644 --- a/src/routes/(app)/[username]/invoice/[hash]/+layout.server.js +++ b/src/routes/(app)/[username]/invoice/[hash]/+layout.server.js @@ -12,14 +12,14 @@ export async function load({ depends, params, url, parent }) { if (user && invoice.uid !== user.id && !url.pathname.includes('tip')) throw redirect(307, `/send/invoice/${hash}`); - let { amount, received } = invoice; + let { amount, pending, received } = invoice; amount = parseInt(amount); - let paid = (!amount && received) || (amount > 0 && received >= amount); + let paid = (!amount && received) || (amount > 0 && (pending >= amount || received >= amount)); if (paid && !url.pathname.endsWith('paid')) throw redirect(307, `/${params.username}/invoice/${hash}/paid`); - let sm = Qr.drawImg(invoice.text, { size: 300 }); + let sm = Qr.drawImg(invoice.text || '', { size: 300 }); return { hash, invoice, sm, lg: sm }; } diff --git a/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte b/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte index 3a281728..c9619cee 100644 --- a/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte +++ b/src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte @@ -42,7 +42,7 @@ {#if pending} <h1 class="text-3xl md:text-4xl font-bold mb-6">Payment detected</h1> <h2 class="text-2xl md:text-3xl font-semibold"> - {f(fiat(pending, rate), currency)} + {f(fiat(pending - tip, rate), currency)} {#if tip} + {f(fiat(tip, rate), currency)} {/if} diff --git a/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte b/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte index 296ef9c6..fe6c67be 100644 --- a/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte +++ b/src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte @@ -56,9 +56,10 @@ export let data; $: refresh(data); - let { invoice, id, user } = data; + let { invoice, id, user, rates } = data; let { amount, + hash, type, rate, received, @@ -79,18 +80,21 @@ ({ invoice, id } = data); ({ amount, + hash, type, - rate, received, prompt, text, tip, - user: { username, currency } + user: { username } } = invoice); tipPercent = (tip / amount) * 100; }; + $: currency = user?.currency || invoice.currency; + $: rate = invoice.rate * (rates[currency] / rates[invoice.currency]); + $: tip = Math.round((amount / 100) * tipPercent); $: amountFiat = parseFloat(((amount * rate) / sats).toFixed(2)); @@ -107,9 +111,11 @@ <input type="hidden" name="amount" value={amount} /> <input type="hidden" name="tip" value={tip} /> <input type="hidden" name="username" value={username} /> - <input type="hidden" name="rate" value={rate} /> + <input type="hidden" name="rate" value={invoice.rate} /> <input type="hidden" name="prompt" value="false" /> <input type="hidden" name="type" value={type} /> + <input type="hidden" name="hash" value={hash} /> + <input type="hidden" name="request_id" value={invoice.request?.id} /> <h1 class="text-4xl font-semibold my-8">{$t('invoice.addTipq')}</h1> diff --git a/src/routes/(app)/[username]/messages/+page.server.js b/src/routes/(app)/[username]/messages/+page.server.js index 8347c184..ec5c5eee 100644 --- a/src/routes/(app)/[username]/messages/+page.server.js +++ b/src/routes/(app)/[username]/messages/+page.server.js @@ -8,8 +8,9 @@ export async function load({ params, parent, url }) { try { messages = await get(`/${user.pubkey}/${since}/messages`); + messages = messages - .filter((m) => m.recipient.id === subject.id || m.author.id === subject.id) + .filter((m) => m.recipient?.id === subject.id || m.author?.id === subject.id) .sort((a, b) => a.created_at - b.created_at); if (user.id === subject.id) messages = messages.filter((m) => m.recipient.id === m.author.id); diff --git a/src/routes/(app)/[username]/messages/+page.svelte b/src/routes/(app)/[username]/messages/+page.svelte index 7a10d605..cfa7f741 100644 --- a/src/routes/(app)/[username]/messages/+page.svelte +++ b/src/routes/(app)/[username]/messages/+page.svelte @@ -10,7 +10,7 @@ import { sign, send, encrypt, decrypt } from '$lib/nostr'; import { event as e, password } from '$lib/store'; import { tick, onMount } from 'svelte'; - import { calculateId } from 'nostr'; + import { getEventHash } from 'nostr-tools'; export let data; @@ -77,8 +77,8 @@ tick().then(() => pane && (pane.scrollTop = pane.scrollHeight)); try { - event.id = await calculateId(event); event.content = await encrypt({ message: event.message, recipient: subject.pubkey, user }); + event.id = getEventHash(event); await sign({ event, user }); await send(event); diff --git a/src/routes/(app)/[username]/payments/[...page]/+page.svelte b/src/routes/(app)/[username]/payments/[...page]/+page.svelte index a502f8c3..744b35ec 100644 --- a/src/routes/(app)/[username]/payments/[...page]/+page.svelte +++ b/src/routes/(app)/[username]/payments/[...page]/+page.svelte @@ -60,9 +60,11 @@ payments = payments.map((p) => ({ ...p, created: new Date(p.created), + total: p.amount + (p.tip || 0), amount_fiat: f((p.amount * p.rate) / sats, p.currency), fee_fiat: p.fee ? f((p.fee * p.rate) / sats, p.currency) : null, - tip_fiat: p.tip ? f((p.tip * p.rate) / sats, p.currency) : null + tip_fiat: p.tip ? f((p.tip * p.rate) / sats, p.currency) : null, + total_fiat: f(((p.amount + (p.tip || 0)) * p.rate) / sats, p.currency) })); let keys = [ @@ -72,11 +74,13 @@ 'rate', 'currency', 'amount', - 'fee', 'tip', + 'total', + 'fee', 'amount_fiat', - 'fee_fiat', - 'tip_fiat' + 'tip_fiat', + 'total_fiat', + 'fee_fiat' ]; let csv = @@ -143,24 +147,17 @@ <a href={`/payment/${p.id}`}> <div class="grid grid-cols-3 border-b h-24 hover:bg-gray-100 px-4"> <div class="whitespace-nowrap my-auto"> - <div class="font-bold"> - {f(p.amount * (p.rate / sats), p.currency)} - + <div class="font-bold" class:text-red-800={p.amount < 0}> + {f(Math.abs(p.amount) * (p.rate / sats), p.currency)} {#if p.tip} - <span class="text-sm"> - +{f(p.tip * (p.rate / sats), p.currency)} + <span class="text-sm text-secondary"> + +{Math.round((p.tip / Math.abs(p.amount)) * 100)}% </span> {/if} </div> <div class="text-secondary"> - {sat(p.amount)} - - {#if p.tip} - <span class="text-sm"> - +{sat(p.tip)} - </span> - {/if} + {sat(Math.abs(p.amount) + (p.tip || 0))} </div> </div> diff --git a/src/routes/(app)/[username]/receive/[...param]/+page.svelte b/src/routes/(app)/[username]/receive/[...param]/+page.svelte index 5d37ccd3..27b339aa 100644 --- a/src/routes/(app)/[username]/receive/[...param]/+page.svelte +++ b/src/routes/(app)/[username]/receive/[...param]/+page.svelte @@ -18,11 +18,10 @@ let lightning = true; let { currency, username, prompt } = data.subject; - let { user } = data; + let { rates, user } = data; let submit; - console.log("PROMPTY", prompt) - + $: rate = rates[currency]; $: type = lightning ? 'lightning' : 'bitcoin'; </script> @@ -39,7 +38,7 @@ <input type="hidden" name="prompt" value={prompt} /> <div class="space-y-3 w-[300px] mx-auto"> - <Numpad bind:amount bind:currency {submit} /> + <Numpad bind:amount bind:currency bind:rate {submit} /> <button bind:this={submit} type="submit" diff --git a/src/routes/(app)/[username]/reset/+server.js b/src/routes/(app)/[username]/reset/+server.js new file mode 100644 index 00000000..52fff367 --- /dev/null +++ b/src/routes/(app)/[username]/reset/+server.js @@ -0,0 +1,10 @@ +import { json } from '@sveltejs/kit'; +import { auth, post } from '$lib/utils'; + +export async function POST({ cookies, request, params }) { + let { username } = params; + let r = await request.json(); + let { password } = r; + let result = await post('/reset', { password, username }, auth(cookies)); + return json(result); +} diff --git a/src/routes/(app)/[username]/settings/[...tab]/+page.svelte b/src/routes/(app)/[username]/settings/[...tab]/+page.svelte index 5638a22b..2bd465a7 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/+page.svelte +++ b/src/routes/(app)/[username]/settings/[...tab]/+page.svelte @@ -5,7 +5,7 @@ import { Icon, Spinner, Pin } from '$comp'; import { t } from '$lib/translations'; - import { fail, post, success } from '$lib/utils'; + import { fail, post, warning, success } from '$lib/utils'; import { avatar, banner, password, pin } from '$lib/store'; import { upload } from '$lib/upload'; import { page } from '$app/stores'; @@ -33,6 +33,8 @@ $: form?.success && success('Settings saved!'); + $: form?.message && fail(form.message); + $: if (form?.message?.startsWith('Pin')) { fail('Wrong pin, try again'); $pin = ''; @@ -55,7 +57,12 @@ let data = new FormData(formElement); if (data.get('password')) { - data.set('cipher', await reEncryptEntropy(user, data.get('password'))); + try { + data.set('cipher', await reEncryptEntropy(user, data.get('password'))); + } catch (e) { + console.log('Failed to encrypt keys with new password'); + throw e; + } } if ($avatar) { @@ -80,8 +87,12 @@ tags: [] }; - await sign({ event, user }); - await send(event); + try { + await sign({ event, user }); + await send(event); + } catch (e) { + warning('nostr profile could not be updated'); + } const response = await fetch(formElement.action, { method: 'POST', @@ -103,13 +114,10 @@ loading = false; } - - let loaded; - onMount(() => setTimeout(() => (loaded = true), 50)); </script> -{#if loaded && user.haspin && $pin?.length !== 6} - <Pin bind:value={$pin} /> +{#if user.haspin && $pin?.length !== 6} + <Pin /> {/if} <form @@ -150,7 +158,7 @@ {#if loading} <Spinner /> {:else} - <div class="my-auto">Save Settings</div> + <div class="my-auto">{$t('user.settings.saveSettings')}</div> {/if} </button> </div> diff --git a/src/routes/(app)/[username]/settings/[...tab]/_security.svelte b/src/routes/(app)/[username]/settings/[...tab]/_security.svelte index eb3b405e..40947bda 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/_security.svelte +++ b/src/routes/(app)/[username]/settings/[...tab]/_security.svelte @@ -148,9 +148,9 @@ {$t('user.settings.securityPINDescription')} </p> {#if verifying} - <Pin bind:value={verify} {cancel} /> + <Pin bind:value={verify} {cancel} notify={false} /> {:else if settingPin} - <Pin bind:value={pin} title={$t('user.settings.setPIN')} {cancel} /> + <Pin bind:value={pin} title={$t('user.settings.setPIN')} {cancel} notify={false} /> {:else} <button type="button" class="primary" on:click={togglePin} ><Icon icon="lock" style="mr-1" /> @@ -192,7 +192,7 @@ {/if} {#if confirming2fa || disabling2fa} - <Pin bind:value={token} title="Enter 2FA Code" {cancel} /> + <Pin bind:value={token} title="Enter 2FA Code" {cancel} persist={false} notify={false} /> {/if} </div> diff --git a/src/routes/(app)/admin/+layout.server.js b/src/routes/(app)/admin/+layout.server.js new file mode 100644 index 00000000..26449467 --- /dev/null +++ b/src/routes/(app)/admin/+layout.server.js @@ -0,0 +1,11 @@ +import { auth, get } from '$lib/utils'; +import { redirect } from '@sveltejs/kit'; + +export async function load({ cookies, parent }) { + let token = cookies.get('token'); + if (!token) throw redirect(307, '/login'); + let user = await get('/me', auth(cookies)); + if (!user) throw redirect(307, '/login'); + + return { user }; +} diff --git a/src/routes/(app)/admin/+layout.svelte b/src/routes/(app)/admin/+layout.svelte new file mode 100644 index 00000000..d7561e32 --- /dev/null +++ b/src/routes/(app)/admin/+layout.svelte @@ -0,0 +1,10 @@ +<script> + import { LandingHeader } from '$comp'; + export let data; + let { user } = data; +</script> + +<LandingHeader {user} /> +<main class="container px-10 md:px-40 mx-auto min-h-[600px] pb-20"> + <slot /> +</main> diff --git a/src/routes/(app)/admin/+page.server.js b/src/routes/(app)/admin/+page.server.js new file mode 100644 index 00000000..e19f894c --- /dev/null +++ b/src/routes/(app)/admin/+page.server.js @@ -0,0 +1,10 @@ +import { auth, get } from '$lib/utils'; +import { redirect } from '@sveltejs/kit'; + +export async function load({ cookies, parent }) { + let { user } = await parent(); + if (!user) throw redirect(307, '/login'); + + let users = (await get('/users', auth(cookies))).sort((a, b) => b.balance - a.balance); + return { users }; +} diff --git a/src/routes/(app)/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte new file mode 100644 index 00000000..529d2f77 --- /dev/null +++ b/src/routes/(app)/admin/+page.svelte @@ -0,0 +1,22 @@ +<script> + import { post } from '$lib/utils'; + export let data; + let reset = (username) => post(`/${username}/reset`, { password }); + let { users } = data; + let password; +</script> + +{#each users as { balance, username, expected }} + <div class="grid grid-cols-5"> + <div>{username}</div> + <div>{balance}</div> + <div>{expected}</div> + <div><input bind:value={password} /></div> + <div> + <button + class="text-sm md:text-lg rounded-full border py-2 px-4 hover:opacity-80 min-w-[72px] bg-black text-white" + on:click={() => reset(username)}>Reset</button + > + </div> + </div> +{/each} diff --git a/src/routes/(app)/buy/+page.svelte b/src/routes/(app)/buy/+page.svelte index 2f556ada..22e7e9ec 100644 --- a/src/routes/(app)/buy/+page.svelte +++ b/src/routes/(app)/buy/+page.svelte @@ -1,7 +1,7 @@ <script> import { enhance } from '$app/forms'; import { Icon, Lock, Card, Calendar, Spinner } from '$comp'; - import { animatedRate, selectedRate } from '$lib/store'; + import { animatedRate } from '$lib/store'; import { f } from '$lib/utils'; export let data, form; @@ -10,24 +10,6 @@ let loading; let amount, n, d, cvc; - - let ease = (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t); - let i; - - let o = $selectedRate; - $: animate($selectedRate); - let animate = (n) => { - clearInterval(i); - - let t = 0; - let d = o - n; - - i = setInterval(() => { - $animatedRate = (o - d * ease(t / 100)).toFixed(2); - if (t > 80) (o = n) && clearInterval(i); - t++; - }, 10); - }; </script> <div class="container px-4 max-w-xl mx-auto mt-10 space-y-5"> @@ -141,7 +123,6 @@ <div class="my-auto mr-1">1</div> <img src="/images/bitcoin.svg" class="w-5 my-auto" alt="Bitcoin" /> </div> - <div>= {f($animatedRate, currency)}</div> </div> <p class="text-secondary">3% credit card fee applies</p> </div> diff --git a/src/routes/(app)/launch/purchase/+page.svelte b/src/routes/(app)/launch/purchase/+page.svelte index 3273752a..fbaa58c0 100644 --- a/src/routes/(app)/launch/purchase/+page.svelte +++ b/src/routes/(app)/launch/purchase/+page.svelte @@ -1,7 +1,6 @@ <script> import { onMount } from 'svelte'; import { enhance } from '$app/forms'; - import { selectedRate } from '$lib/store'; import { post } from '$lib/utils'; import { goto } from '$app/navigation'; diff --git a/src/routes/(app)/login/+page.svelte b/src/routes/(app)/login/+page.svelte index 75998cd0..eb3ae26b 100644 --- a/src/routes/(app)/login/+page.svelte +++ b/src/routes/(app)/login/+page.svelte @@ -1,5 +1,13 @@ <script> import { RegisterOrLogin } from '$comp'; + import { onMount } from 'svelte'; + import { pin } from '$lib/store'; + + onMount(() => { + $pin = undefined; + localStorage.clear(); + sessionStorage.clear(); + }); export let form; </script> diff --git a/src/routes/(app)/payment/[id]/+page.svelte b/src/routes/(app)/payment/[id]/+page.svelte index 2e170a43..1da3f872 100644 --- a/src/routes/(app)/payment/[id]/+page.svelte +++ b/src/routes/(app)/payment/[id]/+page.svelte @@ -1,16 +1,22 @@ <script> import { t } from '$lib/translations'; - import { back, copy, f, s, fiat, sats } from '$lib/utils'; + import { back, copy, f, s, fiat, post, sats, success } from '$lib/utils'; import { Avatar, Icon } from '$comp'; import { format } from 'date-fns'; import { PUBLIC_EXPLORER as expl } from '$env/static/public'; export let data; let { user, payment: p } = data; + let { username } = user; let { id, amount, created, rate, type, ref, tip, ourfee, fee, currency } = p; let a = Math.abs(amount); + let print = async () => { + await post(`/payment/${id}/print`, { id }); + success('Printing!'); + }; + fee = fee || 0; </script> @@ -36,33 +42,42 @@ </a> </div> {/if} - <div> <span class="text-lg text-secondary">Amount</span> <div> {f(fiat(a, rate), currency)} - {#if tip} - <span class="text-lg"> - + {f(fiat(tip, rate), currency)} - </span> - {/if} - <span class="text-secondary" - >⚡️{`${s(a)}`} - - {#if tip} - <span class="text-lg"> - + ⚡️{s(tip)} - </span> - {/if} + <span class="text-secondary text-lg"> + ⚡️{s(a)} </span> </div> </div> + {#if tip} + <div> + <span class="text-lg text-secondary">Tip</span> + <div> + {f(fiat(tip, rate), currency)} + ({Math.round((tip * 100) / Math.abs(a))}%) + <span class="text-secondary text-lg"> + ⚡️{s(tip)} + </span> + </div> + </div> + + <div> + <span class="text-lg text-secondary">Total</span> + <div> + {f(fiat(a + (tip || 0), rate), currency)} + <span class="text-secondary text-lg">⚡️{`${s(a + (tip || 0))}`} </span> + </div> + </div> + {/if} + <div> <span class="text-lg text-secondary">Network fee</span> <div> {f(fiat(fee, rate), currency)} - <span class="text-secondary">⚡️{`${s(fee)}`}</span> + <span class="text-secondary text-lg">⚡️{`${s(fee)}`}</span> </div> </div> @@ -97,4 +112,11 @@ </div> </div> {/if} + + {#if ['laughingbean', 'burnabyartigiano', 'milanowestend'].includes(user.username.toLowerCase()) } + <button + class="text-sm md:text-lg rounded-full border py-2 px-4 hover:opacity-80 min-w-[72px] bg-black text-white" + on:click={print}>Print Receipt</button + > + {/if} </div> diff --git a/src/routes/(app)/payment/[id]/print/+server.js b/src/routes/(app)/payment/[id]/print/+server.js new file mode 100644 index 00000000..fde834d1 --- /dev/null +++ b/src/routes/(app)/payment/[id]/print/+server.js @@ -0,0 +1,7 @@ +import { auth, post } from '$lib/utils'; +import { json } from '@sveltejs/kit'; + +export async function POST({ cookies, request }) { + let body = await request.json(); + return json(post('/print', body, auth(cookies))); +} diff --git a/src/routes/(app)/pot/[name]/+page.svelte b/src/routes/(app)/pot/[name]/+page.svelte index 2d93abfc..7b3d890b 100644 --- a/src/routes/(app)/pot/[name]/+page.svelte +++ b/src/routes/(app)/pot/[name]/+page.svelte @@ -6,7 +6,7 @@ import { toast } from '@zerodevx/svelte-toast'; import { t } from '$lib/translations'; import { copy, f, s, sat, sats } from '$lib/utils'; - import { loginRedirect, selectedRate } from '$lib/store'; + import { loginRedirect } from '$lib/store'; export let data; diff --git a/src/routes/(app)/register/+page.server.js b/src/routes/(app)/register/+page.server.js index e71b2a60..8a8ca81c 100644 --- a/src/routes/(app)/register/+page.server.js +++ b/src/routes/(app)/register/+page.server.js @@ -16,7 +16,7 @@ export const actions = { if (loginRedirect === 'undefined') loginRedirect = undefined; try { - await post('/register', { user }); + await post('/register', { user }, { "cf-connecting-ip": request.headers.get('cf-connecting-ip') }); } catch (e) { if (e.message.includes('taken')) error = e.message; } diff --git a/src/routes/(app)/send/+layout.svelte b/src/routes/(app)/send/+layout.svelte index 2fb732e8..47c11f1c 100644 --- a/src/routes/(app)/send/+layout.svelte +++ b/src/routes/(app)/send/+layout.svelte @@ -1,37 +1,15 @@ <script> - import { browser } from '$app/environment'; import { goto } from '$app/navigation'; import { Pin } from '$comp'; import { pin } from '$lib/store'; - import { onMount } from 'svelte'; - import { success, fail, post } from '$lib/utils'; export let data; - let loaded; let { user } = data; - - onMount(() => setTimeout(() => (loaded = true), 500)); let cancel = () => goto(`/${user.username}`); - - $: browser && loaded && checkPin($pin); - let checkPin = async (p) => { - if (p?.length !== 6 || p === JSON.parse(sessionStorage.getItem('pin'))) return; - let result; - try { - result = await post('/pin', { pin: p }); - } catch (e) {} - - if (result) { - success('Pin confirmed'); - } else { - fail('Invalid pin'); - $pin = ''; - } - }; </script> -{#if loaded && user.haspin && $pin?.length !== 6} +{#if user.haspin && (!$pin || $pin?.length !== 6)} <Pin bind:value={$pin} {cancel} /> {/if} diff --git a/src/routes/(app)/send/[...text]/+page.server.js b/src/routes/(app)/send/[...text]/+page.server.js index 9c7d4185..dbe2b660 100644 --- a/src/routes/(app)/send/[...text]/+page.server.js +++ b/src/routes/(app)/send/[...text]/+page.server.js @@ -6,6 +6,8 @@ import { fail, redirect } from '@sveltejs/kit'; let parse = async (t, host) => { if (!t) return; + if (t.startsWith('http')) throw redirect(307, t); + let amount, user, id; t = t.trim(); diff --git a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte index 77d5fde6..342eeebe 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte @@ -5,15 +5,18 @@ import { Toggle, Slider, Icon, Numpad, Spinner } from '$comp'; import { page } from '$app/stores'; import { fiat as toFiat, f, back, s, sat } from '$lib/utils'; - import { pin, selectedRate } from '$lib/store'; + import { pin } from '$lib/store'; export let data; export let form; + let { rates } = data; let { address, amount } = $page.params; let { balance, currency } = data.user; let loading, submit, fiat, confirmed, feeRate, min, max, fee, ourfee, stale, subtract, adjusting; + $: rate = rates[currency]; + let toggle = () => (loading = true); let toggleAdjusting = () => (adjusting = !adjusting); @@ -64,7 +67,7 @@ {#if confirmed} <div class="text-center"> <h2 class="text-2xl md:text-3xl font-semibold"> - {f(toFiat(amount, $selectedRate), currency)} + {f(toFiat(amount, rate), currency)} </h2> <h3 class="text-secondary md:text-lg mb-6 mt-1">⚡️{s(amount)}</h3> </div> @@ -74,7 +77,7 @@ <div class="relative"> <h2 class="text-xl font-semibold"> - {f(toFiat(fee + ourfee, $selectedRate), currency)} + {f(toFiat(fee + ourfee, rate), currency)} </h2> <h3 class="text-secondary mb-6 mt-1">⚡️{s(fee + ourfee)}</h3> <button class="absolute right-1/4 top-0 text-secondary p-4" on:click={toggleAdjusting} @@ -83,7 +86,7 @@ </div> </div> {:else} - <Numpad bind:amount bind:fiat {currency} {submit} /> + <Numpad bind:amount bind:fiat {currency} {submit} bind:rate /> {/if} <form method="POST" use:enhance on:submit={toggle}> diff --git a/src/routes/(app)/send/invoice/[id]/+page.server.js b/src/routes/(app)/send/invoice/[id]/+page.server.js index 3db03123..4cbbb03f 100644 --- a/src/routes/(app)/send/invoice/[id]/+page.server.js +++ b/src/routes/(app)/send/invoice/[id]/+page.server.js @@ -1,21 +1,31 @@ import { error, fail, redirect } from '@sveltejs/kit'; import { fd, auth, get, post } from '$lib/utils'; -export async function load({ params: { id }, parent }) { +export async function load({ params: { id }, parent, url }) { let { user } = await parent(); - let { amount, address, tip, text, user: recipient } = await get(`/invoice/${id}`); + let { + amount, + address, + currency, + hash, + rate, + prompt, + tip, + text, + user: recipient + } = await get(`/invoice/${id}`); + + if (prompt && tip === null) throw redirect(307, `/${recipient.username}/invoice/${id}/tip`); if (recipient.username === user.username) throw error(500, { message: 'Cannot send to self' }); - if (tip) amount += tip; - return { amount, address, payreq: text, recipient }; + return { amount, address, currency, hash, tip, rate, payreq: text, recipient }; } export const actions = { - default: async ({ cookies, params: { hash }, request }) => { + default: async ({ cookies, params: { id }, request }) => { try { let body = await fd(request); - body.amount = parseInt(body.amount); - body.hash = hash; + body.hash = id; await post('/payments', body, auth(cookies)); } catch (e) { diff --git a/src/routes/(app)/send/invoice/[id]/+page.svelte b/src/routes/(app)/send/invoice/[id]/+page.svelte index 7570a61a..d5c548a7 100644 --- a/src/routes/(app)/send/invoice/[id]/+page.svelte +++ b/src/routes/(app)/send/invoice/[id]/+page.svelte @@ -1,21 +1,21 @@ <script> import { t } from '$lib/translations'; import { goto } from '$app/navigation'; - import { pin, selectedRate } from '$lib/store'; + import { pin } 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'; + import { back, f, fiat as toFiat, s, sats } from '$lib/utils'; export let data; export let form; - let { address, payreq, recipient, user } = data; + let { address, rate, payreq, recipient, tip, rates, user } = data; let { currency } = user; let amount = form?.amount || data.amount; let a, af, - amountFiat = amount * ($selectedRate / sats), + amountFiat = amount * (rate / sats), fiat = !amount; let setAmount = () => { @@ -26,6 +26,8 @@ let loading; let submit = () => (loading = true); + $: rate = data.rate * (rates[user.currency] / rates[data.currency]); + $: update(form); let update = () => { if (form?.message.includes('pin')) $pin = undefined; @@ -49,9 +51,23 @@ <h1 class="text-xl md:text-2xl text-secondary mb-2"> {$t('payments.send')} </h1> - <p class="text-6xl break-words mb-4"> - {fiat ? f(amountFiat, currency) : '⚡️' + s(amount)} - </p> + <h2 class="text-2xl md:text-3xl font-semibold"> + {f(toFiat(amount, rate), currency)} + {#if tip} + <span class="text-lg"> + + {f(toFiat(tip, rate), currency)} + </span> + {/if} + </h2> + <h3 class="text-secondary md:text-lg mb-6 mt-1"> + ⚡️{s(amount)} + + {#if tip} + <span class="text-lg"> + + ⚡️{s(tip)} + </span> + {/if} + </h3> <h1 class="text-xl md:text-2xl text-secondary mb-2">{$t('payments.to')}</h1> @@ -61,7 +77,7 @@ </div> </div> {:else} - <Numpad bind:amount={a} bind:amountFiat={af} {currency} bind:fiat /> + <Numpad bind:amount={a} bind:amountFiat={af} {currency} bind:fiat bind:rate /> {/if} <form method="POST" use:enhance on:submit={submit}> diff --git a/src/routes/(app)/send/lightning/[payreq]/+page.server.js b/src/routes/(app)/send/lightning/[payreq]/+page.server.js index 94231376..d358bbca 100644 --- a/src/routes/(app)/send/lightning/[payreq]/+page.server.js +++ b/src/routes/(app)/send/lightning/[payreq]/+page.server.js @@ -11,7 +11,6 @@ export const actions = { send: async ({ cookies, request }) => { try { let body = await fd(request); - let { amount } = body; await post('/payments', body, auth(cookies)); } catch (e) { diff --git a/src/routes/(app)/send/lightning/[payreq]/+page.svelte b/src/routes/(app)/send/lightning/[payreq]/+page.svelte index af07a565..cb1b539e 100644 --- a/src/routes/(app)/send/lightning/[payreq]/+page.svelte +++ b/src/routes/(app)/send/lightning/[payreq]/+page.svelte @@ -10,13 +10,14 @@ export let form; let { payreq } = $page.params; - let { alias } = data; + let { alias, rates } = data; let { currency } = data.user; let a; let maxfee = 100; $: amount = form?.amount || data.amount; + $: rate = rates[currency]; let loading; let submit = () => (loading = true); @@ -91,7 +92,7 @@ {:else} <form method="POST" action="?/setAmount" class="w-[300px] mx-auto" use:enhance> <input type="hidden" value={a} name="amount" /> - <Numpad bind:amount={a} {currency} /> + <Numpad bind:amount={a} {currency} {rate} /> <button type="submit" class="bg-black text-white rounded-xl h-[48px] flex w-full justify-center items-center font-semibold diff --git a/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte b/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte index da761cc2..6aecf8f3 100644 --- a/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte +++ b/src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte @@ -10,9 +10,12 @@ export let form; let { amount, name } = $page.params; + let { rates } = data; let { currency } = data.user; let loading; + $: rate = rates[currency]; + let submit = () => (loading = true); $: update(form); let update = () => { @@ -32,7 +35,7 @@ {/if} <div class="container px-4 mt-20 max-w-xl mx-auto"> - <Numpad bind:amount {currency} /> + <Numpad bind:amount {currency} bind:rate /> <form method="POST" use:enhance on:submit={submit}> <input name="name" value={name} type="hidden" /> diff --git a/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js b/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js index c63328e2..a01cdcfc 100644 --- a/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js +++ b/src/routes/(app)/send/user/[username]/[...amount]/+page.server.js @@ -1,12 +1,34 @@ import { error, fail, redirect } from '@sveltejs/kit'; -import { fd, auth, get, post } from '$lib/utils'; +import { types, sats, fd, auth, get, post } from '$lib/utils'; -export async function load({ params, parent }) { - let { user } = await parent(); - let { amount } = params; +export async function load({ cookies, params, parent }) { let subject = await get(`/users/${params.username}`); + + let { rates, user } = await parent(); + let { username } = params; + let [amount, currency] = params.amount.split('/'); + if (subject.username === user?.username) throw error(500, { message: 'Cannot send to self' }); - return { amount, subject }; + + let rate; + if (amount) { + rate = rates[currency ? currency.toUpperCase() : subject.currency]; + if (!rate) throw error(500, 'Invalid currency symbol'); + if (currency) amount = (amount * sats) / rate; + + let { hash } = await post( + '/invoice', + { + invoice: { amount, prompt: false, type: types.lightning }, + user: { username } + }, + auth(cookies) + ); + + throw redirect(307, `/${username}/invoice/${hash}`); + } + + return { amount, rate, subject }; } export const actions = { diff --git a/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte b/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte index 1bb88277..9ce82d2e 100644 --- a/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte +++ b/src/routes/(app)/send/user/[username]/[...amount]/+page.svelte @@ -1,7 +1,7 @@ <script> import { t } from '$lib/translations'; import { goto } from '$app/navigation'; - import { pin, selectedRate } from '$lib/store'; + import { pin } from '$lib/store'; import { enhance } from '$app/forms'; import { AppHeader, Avatar, Icon, Numpad, Spinner } from '$comp'; import { page } from '$app/stores'; @@ -10,31 +10,34 @@ export let data; export let form; - let { subject, rates, user } = data; - let currency = user?.currency || 'USD'; + let { subject, rate, rates, user } = data; + let currency = user.currency || subject.currency || 'USD'; let amount = form?.amount || data.amount; let a, af, fiat = !amount, hash, - rate, amountFiat = 0; - selectedRate.subscribe((r) => rate || (amountFiat = amount * (r / sats))); + $: r = rate || rates[user.currency || subject.currency]; let setAmount = async () => { amount = a; amountFiat = parseFloat(af).toFixed(2); - rate = fiat ? (sats * amountFiat) / amount : $selectedRate; + rate = fiat ? (sats * amountFiat) / amount : r; ({ hash } = await post(`/${subject.username}/invoice`, { invoice: { amount, + hash: 'internal', rate: rates[subject.currency], + prompt: subject.prompt, type: 'internal' }, user: subject })); + + goto(`/${subject.username}/invoice/${hash}`); }; let loading; @@ -56,17 +59,7 @@ {/if} <div class="container px-4 mt-20 max-w-xl mx-auto space-y-8"> - {#if amount} - <h1 class="text-center text-3xl md:text-4xl font-bold mb-6">Sending</h1> - <div class="text-center mb-8"> - <h2 class="text-2xl md:text-3xl font-semibold"> - {f(amountFiat, currency)} - </h2> - <h3 class="text-secondary md:text-lg mb-6 mt-1">{sat(amount)}</h3> - </div> - {:else} - <Numpad bind:amount={a} bind:amountFiat={af} {currency} bind:fiat /> - {/if} + <Numpad bind:amount={a} bind:amountFiat={af} {currency} bind:fiat bind:rate={r} /> <form method="POST" use:enhance on:submit={submit}> <input name="amount" value={amount} type="hidden" /> diff --git a/src/routes/(app)/sent/+page.svelte b/src/routes/(app)/sent/+page.svelte index 760d2e1d..dbd18c2e 100644 --- a/src/routes/(app)/sent/+page.svelte +++ b/src/routes/(app)/sent/+page.svelte @@ -20,7 +20,7 @@ <div class="flex w-full max-w-[200px] mx-auto py-20" in:scale={{ start: 0.5 }}> <Icon icon="check" style="mx-auto" /> </div> - <h1 class="text-3xl md:text-4xl font-bold mb-6">Sent!</h1> + <h1 class="text-3xl md:text-4xl font-bold mb-6">{$t('payments.sent')}!</h1> <h2 class="text-2xl md:text-3xl font-semibold"> {f(fiat(amount, rate), currency)} {#if tip} diff --git a/src/routes/(app)/support/+page.svelte b/src/routes/(app)/support/+page.svelte index 8dbfc1af..b2f82e89 100644 --- a/src/routes/(app)/support/+page.svelte +++ b/src/routes/(app)/support/+page.svelte @@ -1,4 +1,5 @@ <script> + import { onDestroy } from 'svelte'; import { fail, success, post } from '$lib/utils'; import { scale } from 'svelte/transition'; import { AppHeader, Icon } from '$comp'; @@ -14,19 +15,32 @@ let email; let message; let sent; + let recaptchaSiteKey = '6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM'; let submit = (e) => { e.preventDefault(); grecaptcha.ready(() => { - grecaptcha - .execute('6LfCd8YkAAAAANmVJgzN3SQY3n3fv1RhiS5PgMYM', { action: 'submit' }) - .then((token) => - post('/support', { email, message, token }) - .then(() => (sent = true)) - .catch(() => fail('problem submitting')) - ); + grecaptcha.execute(recaptchaSiteKey, { action: 'submit' }).then((token) => + post('/support', { username: user?.username, email, message, token }) + .then(() => (sent = true)) + .catch(() => fail('problem submitting')) + ); }); }; + + onDestroy(() => { + const nodeBadge = document.querySelector('.grecaptcha-badge'); + if (nodeBadge) { + document.body.removeChild(nodeBadge.parentNode); + } + + const scriptSelector = + "script[src='https://www.google.com/recaptcha/api.js?render=" + recaptchaSiteKey + "']"; + const script = document.querySelector(scriptSelector); + if (script) { + script.remove(); + } + }); </script> <svelte:head> diff --git a/src/routes/(static)/docs/+page.svelte b/src/routes/(static)/docs/+page.svelte index 8ddb98cd..e453464c 100644 --- a/src/routes/(static)/docs/+page.svelte +++ b/src/routes/(static)/docs/+page.svelte @@ -3,23 +3,20 @@ import { Code, Icon } from '$comp'; import { PUBLIC_COINOS_URL } from '$env/static/public'; export let data; - let { token } = data; - let api = PUBLIC_COINOS_URL + '/api'; + let { user, token } = data; + let api = 'https://coinos.io/api'; - if (!token) token = '<login to see your token here>'; let tokenSample = `export token="${token}"`; </script> -<div class="space-y-8 lg:text-xl mt-20 w-full max-w-full"> - <h1 class="text-4xl">API Documentation</h1> +<div class="space-y-8 lg:text-xl mt-20 pt-20 md:pt-0 w-full max-w-full"> + <h1 class="text-4xl">Documentation</h1> <p class="text-secondary"> - Coinos has a simple REST API that can be used to register accounts, fetch data, and make - payments + Coinos has a simple REST API that can be used to register accounts and make payments and + queries. </p> - <p class="text-secondary">* = required parameter</p> - <h2 class="text-2xl">Base URL</h2> <div class="bg-black text-white rounded-lg p-4 flex gap-4"> <div>{api}</div> @@ -28,14 +25,16 @@ > </div> - <h2 class="text-2xl">Auth Token</h2> - <p class="text-secondary">This JWT authorizes you to use the API</p> - <div class="bg-black text-white rounded-lg p-4 flex gap-4"> - <div class="w-full break-all">{tokenSample}</div> - <button class="ml-auto my-auto invert opacity-90" on:click={() => copy(tokenSample)} - ><Icon icon="copy" style="w-10 max-w-none" /></button - > - </div> + {#if token} + <h2 class="text-2xl">Auth Token</h2> + <p class="text-secondary">This token authorizes you to use the API as <b>{user.username}</b></p> + <div class="bg-black text-white rounded-lg p-4 flex gap-4"> + <div class="w-full break-all">{tokenSample}</div> + <button class="ml-auto my-auto invert opacity-90" on:click={() => copy(tokenSample)} + ><Icon icon="copy" style="w-10 max-w-none" /></button + > + </div> + {/if} <h2 class="text-2xl">POST /register</h2> <p class="text-secondary">Register a new user account with a username and password</p> @@ -47,91 +46,54 @@ <h2 class="text-2xl">POST /invoice</h2> <p class="text-secondary">Create an invoice.</p> - <div> - <div class="text-secondary"> - <span class="font-mono font-bold">type</span>: bitcoin or lightning - </div> - <div class="text-secondary"> - <span class="font-mono font-bold">amount</span>: amount in satoshis - </div> - <div class="text-secondary"> - <span class="font-mono font-bold">webhook</span>: an endpoint to hit when the invoice is paid - </div> - <div class="text-secondary"> - <span class="font-mono font-bold">secret</span>: a secret string for the webhook to check + + <div class="text-secondary"> + <div>Request params</div> + <div class="grid grid-cols-3 ml-2"> + <div class="font-bold">type</div> + <div class="col-span-2">bitcoin or lightning</div> + <div class="font-bold">amount</div> + <div class="col-span-2">amount in satoshis</div> + <div class="font-bold">webhook</div> + <div class="col-span-2">(optional) endpoint to hit when the invoice is paid</div> + <div class="font-bold">secret</div> + <div class="col-span-2">a secret for the webhook to check</div> </div> </div> - hey <Code sample="invoice" /> + <p>Sample response</p> + <Code sample="invoiceResponse" /> + <p>You can check the <b>received</b> field to see how many satoshis have been paid so far</p> + <h2 class="text-2xl">GET /invoice/:hash</h2> <p class="text-secondary"> Fetch an invoice by passing a bitcoin address or lightning payment hash </p> <Code sample="fetchInvoice" /> -</div> -<!-- echo Login and get an API token --> -<!-- token=$(curl -s "$api/taboggan" -H "$json" -d '{"username": "'$username'", "password": "'$password'"}' | jq -r .token) --> -<!-- echo $token --> -<!-- echo "" --> -<!-- --> -<!-- username="apidemo" --> -<!-- token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFwaWRlbW8iLCJpYXQiOjE2NDAxMzIwNjd9.aBtcrnLjDS_Zixn5_X_N8sqX0KxTDrVySSqh1fa96rk" --> -<!-- auth="Authorization: Bearer $token" --> -<!-- --> -<!-- echo Get account balance in satoshis --> -<!-- curl -s "$api/me" -H "$json" -H "$auth" | jq '.account.balance' --> -<!-- echo "" --> -<!-- --> -<!-- echo Get the current price of bitcoin in USD --> -<!-- curl -s "$api/rates" -H "$json" | jq '.USD' --> -<!-- echo "" --> -<!-- --> -<!-- echo Create a legacy Bitcoin address --> -<!-- address=$(curl -s "$api/address?network=bitcoin&type=legacy" -H "$auth" -H "$json" | jq -r .address) --> -<!-- echo $address --> -<!-- echo "" --> -<!-- --> -<!-- echo Create an invoice to associate the address with the user --> -<!-- curl -s "$api/invoice" -H "$json" -d '{"invoice": {"address": "'$address'", "network": "bitcoin"}, "user": {"username": "'$username'"}}' > /dev/null --> -<!-- echo "" --> -<!-- --> -<!-- msg="testingtesting123" --> -<!-- echo Sign a message with the address: $msg $address --> -<!-- curl -s "$api/signMessage" -H "$json" -H "$auth" -d '{"address": "'$address'", "message": "'$msg'"}' --> -<!-- echo "" --> -<!-- --> -<!-- echo Create a bech32 Bitcoin address --> -<!-- address=$(curl -s "$api/address?network=bitcoin&type=bech32" -H "$auth" -H "$json" | jq -r .address) --> -<!-- echo $address --> -<!-- echo "" --> -<!-- --> -<!-- echo Create a lightning invoice --> -<!-- text=$(curl -s "$api/lightning/invoice" -H "$auth" -H "$json" -d '{"amount": 100}') --> -<!-- echo $text --> -<!-- echo "" --> -<!-- --> -<!-- echo Associate the lightning invoice with the user --> -<!-- curl -s "$api/invoice" -H "$json" -d '{"invoice": {"text": "'$text'", "network": "bitcoin"}, "user": {"username": "'$username'"}}' > /dev/null --> -<!-- echo "" --> -<!-- --> -<!-- echo Get an external invoice --> -<!-- payreq=$(curl -s https://legend.lnbits.com/api/v1/payments -H "$json" -H "x-api-key: 698e5b6744f8442880ba3ad630756c02" -d '{ "out": false, "amount": 50, "memo": "test", "unit": "sat", "lnurl_callback": null }' | jq -r .payment_request) --> -<!-- echo $payreq --> -<!-- echo "" --> -<!-- --> -<!-- echo Send a lightning payment --> -<!-- curl -s "$api/lightning/send" -H "$json" -H "$auth" -d '{"payreq": "'$payreq'"}' | jq -r '{ amount, fee, balance: (.account.balance)}' --> -<!-- echo "" --> -<!-- --> -<!-- echo Send a bitcoin payment --> -<!-- tx=$(curl -s "$api/bitcoin/fee" -H "$json" -H "$auth" -d '{"address": "bc1qqc9azhdjjpdxenrndt8cw4tw9del5pyrjn0wln", "amount": 1200, "feeRate": 1000 }') --> -<!-- hex=$(echo $tx | jq -r .tx.hex) --> -<!-- fee=$(echo $tx | jq -r .tx.fee) --> -<!-- --> -<!-- curl -s "$api/bitcoin/send" -H "$json" -H "$auth" -d '{"address": "bc1qqc9azhdjjpdxenrndt8cw4tw9del5pyrjn0wln", "tx": { "hex": "'$hex'", "fee": "'$fee'" }}' | jq -r '{ amount, fee, balance: (.account.balance)}' --> -<!-- echo "" --> + <h2 class="text-2xl">GET /payments</h2> + <p class="text-secondary">Get all payments sent or received by the current user</p> + + <div class="text-secondary"> + <div>Query params</div> + <div class="grid grid-cols-3 ml-2"> + <div class="font-bold">start</div> + <div class="col-span-2">only payments after this unix time</div> + <div class="font-bold">end</div> + <div class="col-span-2">only payments before this unix time</div> + <div class="font-bold">limit</div> + <div class="col-span-2">limit to this integer number of results</div> + <div class="font-bold">offset</div> + <div class="col-span-2">start limiting from this integer offset</div> + </div> + </div> + <Code sample="payments" /> -<!-- `} --> + <h2 class="text-2xl">Websocket API</h2> + <p class="text-secondary">Subscribe to real time rates and payments</p> + + <div>Authenticate</div> + <Code sample="socketAuth" /> +</div> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 65c19cb2..ec0642db 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -18,8 +18,9 @@ import { onMount } from 'svelte'; export let data; - let { user } = data; - let { locations } = data; + let { locations, user } = data; + let update = (data) => ({ locations, user } = data); + $: update(data); let howItWorks; let faq; |
