aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2023-04-20 08:42:02 -0700
committerAdam Soltys <[email protected]>2023-04-20 08:42:02 -0700
commitaa187ee0a4c5d5132fae5255800efaa0c0d425ce (patch)
tree50287dcf34b182b68d3493424d40024b688c47ff /src
parentd7af57e0e48dfce401e486a572664ac11112a164 (diff)
parent4b82d5755b52979c11046b6328a95b7f52212f2c (diff)
downloadcoinos-ui-aa187ee0a4c5d5132fae5255800efaa0c0d425ce.tar.gz
coinos-ui-aa187ee0a4c5d5132fae5255800efaa0c0d425ce.zip
Merge branch 'master' into static-qr
Diffstat (limited to 'src')
-rw-r--r--src/app.html2
-rw-r--r--src/components/About.svelte17
-rw-r--r--src/components/Balance.svelte35
-rw-r--r--src/components/Lnurlp.svelte6
-rw-r--r--src/components/Lnurlw.svelte5
-rw-r--r--src/components/LocationsMap.svelte44
-rw-r--r--src/components/Numpad.svelte17
-rw-r--r--src/components/Pin.svelte62
-rw-r--r--src/components/Pinpad.svelte3
-rw-r--r--src/components/RegisterOrLogin.svelte8
-rw-r--r--src/lib/nostr.js86
-rw-r--r--src/lib/samples.js23
-rw-r--r--src/lib/socket.js2
-rw-r--r--src/lib/store.js5
-rw-r--r--src/lib/translations/index.js18
-rw-r--r--src/lib/translations/lang.json4
-rw-r--r--src/lib/utils.js54
-rw-r--r--src/locales/el.json283
-rw-r--r--src/locales/en.json27
-rw-r--r--src/locales/es.json29
-rw-r--r--src/locales/fa.json283
-rw-r--r--src/locales/pt.json384
-rw-r--r--src/routes/(app)/+layout.server.js3
-rw-r--r--src/routes/(app)/+layout.svelte50
-rw-r--r--src/routes/(app)/[username]/(profile)/+layout.svelte60
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.server.js38
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.svelte44
-rw-r--r--src/routes/(app)/[username]/(profile)/followers/+page.svelte5
-rw-r--r--src/routes/(app)/[username]/(profile)/follows/+page.svelte5
-rw-r--r--src/routes/(app)/[username]/dashboard/+page.server.js4
-rw-r--r--src/routes/(app)/[username]/dashboard/+page.svelte163
-rw-r--r--src/routes/(app)/[username]/generate/+page.svelte3
-rw-r--r--src/routes/(app)/[username]/generate/+server.js3
-rw-r--r--src/routes/(app)/[username]/invoice/+server.js6
-rw-r--r--src/routes/(app)/[username]/invoice/[hash]/+layout.server.js6
-rw-r--r--src/routes/(app)/[username]/invoice/[hash]/paid/+page.svelte2
-rw-r--r--src/routes/(app)/[username]/invoice/[hash]/tip/+page.svelte14
-rw-r--r--src/routes/(app)/[username]/messages/+page.server.js3
-rw-r--r--src/routes/(app)/[username]/messages/+page.svelte4
-rw-r--r--src/routes/(app)/[username]/payments/[...page]/+page.svelte29
-rw-r--r--src/routes/(app)/[username]/receive/[...param]/+page.svelte7
-rw-r--r--src/routes/(app)/[username]/reset/+server.js10
-rw-r--r--src/routes/(app)/[username]/settings/[...tab]/+page.svelte28
-rw-r--r--src/routes/(app)/[username]/settings/[...tab]/_security.svelte6
-rw-r--r--src/routes/(app)/admin/+layout.server.js11
-rw-r--r--src/routes/(app)/admin/+layout.svelte10
-rw-r--r--src/routes/(app)/admin/+page.server.js10
-rw-r--r--src/routes/(app)/admin/+page.svelte22
-rw-r--r--src/routes/(app)/buy/+page.svelte21
-rw-r--r--src/routes/(app)/launch/purchase/+page.svelte1
-rw-r--r--src/routes/(app)/login/+page.svelte8
-rw-r--r--src/routes/(app)/payment/[id]/+page.svelte54
-rw-r--r--src/routes/(app)/payment/[id]/print/+server.js7
-rw-r--r--src/routes/(app)/pot/[name]/+page.svelte2
-rw-r--r--src/routes/(app)/register/+page.server.js2
-rw-r--r--src/routes/(app)/send/+layout.svelte24
-rw-r--r--src/routes/(app)/send/[...text]/+page.server.js2
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[...amount]/+page.svelte11
-rw-r--r--src/routes/(app)/send/invoice/[id]/+page.server.js24
-rw-r--r--src/routes/(app)/send/invoice/[id]/+page.svelte32
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/+page.server.js1
-rw-r--r--src/routes/(app)/send/lightning/[payreq]/+page.svelte5
-rw-r--r--src/routes/(app)/send/pot/[name]/[...amount]/+page.svelte5
-rw-r--r--src/routes/(app)/send/user/[username]/[...amount]/+page.server.js32
-rw-r--r--src/routes/(app)/send/user/[username]/[...amount]/+page.svelte27
-rw-r--r--src/routes/(app)/sent/+page.svelte2
-rw-r--r--src/routes/(app)/support/+page.svelte28
-rw-r--r--src/routes/(static)/docs/+page.svelte148
-rw-r--r--src/routes/+page.svelte5
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">&mdash;</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">&mdash;</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&#38;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&#38;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;