wip: login pages finished

- success message with continue button left to do on WalletLogin
- User Panel QR-Scan with native + TextCode left to do
pull/33/head
Laurin Weger 5 months ago
parent 26e67a0bc4
commit a62c0cc342
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 34
      ng-app/src/locales/en.json
  2. 92
      ng-app/src/routes/ScanQR.svelte
  3. 19
      ng-app/src/routes/WalletInfo.svelte
  4. 64
      ng-app/src/routes/WalletLogin.svelte
  5. 271
      ng-app/src/routes/WalletLoginQr.svelte
  6. 322
      ng-app/src/routes/WalletLoginTextCode.svelte
  7. 13
      ng-app/src/store.ts
  8. 24
      ng-app/src/styles.css

@ -28,16 +28,15 @@
"remove_wallet": "Remove wallet from Device", "remove_wallet": "Remove wallet from Device",
"remove_wallet_modal.title": "Remove wallet?", "remove_wallet_modal.title": "Remove wallet?",
"remove_wallet_modal.confirm": "Are you sure you want to remove this wallet from your device?", "remove_wallet_modal.confirm": "Are you sure you want to remove this wallet from your device?",
"offline_warning": "You cannot export your Wallet with QR-Code while being offline.<br />Please connect to the internet and to your broker first.",
"scan_qr.title": "Export by scanning QR-Code", "scan_qr.title": "Export by scanning QR-Code",
"scan_qr.notes": "Scan the QR-Code on the device that you want to transfer your wallet to.<br />During this wallet import, your wallet will be temporarily and securely stored on our servers for up to 5 minutes, using two levels of encryption.", "scan_qr.notes": "Scan the QR-Code on the device that you want to transfer your wallet to.<br />.",
"scan_qr.scan_btn": "Scan QR Code ", "scan_qr.scan_btn": "Scan QR Code ",
"scan_qr.scanner.title": "Scan your QR Code", "scan_qr.scanner.title": "Scan your QR Code",
"scan_qr.scanner.loading": "Loading scanner", "scan_qr.scanner.loading": "Loading scanner",
"scan_qr.syncing": "Synchronizing wallet", "scan_qr.syncing": "Synchronizing wallet",
"scan_qr.scan_successful": "Success!<br />Your wallet has been synchronized to the new device.", "scan_qr.scan_successful": "Success!<br />Your wallet has been synchronized to the new device.",
"gen_qr.title": "Export with generated QR-Code", "gen_qr.title": "Export with generated QR-Code",
"gen_qr.notes": "Use the following QR-Code to scan with the device that you want to transfer your wallet to.<br />During this wallet import, your wallet will be temporarily and securely stored on our servers for up to 5 minutes, using two levels of encryption.", "gen_qr.notes": "Use the following QR-Code to scan with the device that you want to transfer your wallet to.",
"gen_qr.img_title": "Your Export QR Code to Scan", "gen_qr.img_title": "Your Export QR Code to Scan",
"gen_qr.img_alt": "Your Export QR Code to Scan", "gen_qr.img_alt": "Your Export QR Code to Scan",
"gen_qr.gen_button": "Display QR Code" "gen_qr.gen_button": "Display QR Code"
@ -243,29 +242,28 @@
"with_another_wallet": "Log in with another wallet", "with_another_wallet": "Log in with another wallet",
"import_wallet": "Import your wallet", "import_wallet": "Import your wallet",
"import_file": "Import a Wallet File", "import_file": "Import a Wallet File",
"import_qr": "Import with QRCode", "import_qr": "Import with QR-Code",
"import_link": "Enter a Wallet Link", "import_link": "Import with TextCode",
"new_wallet": "Create a new Wallet", "new_wallet": "Create a new Wallet",
"logged_in": "You are logged in.<br /> please wait while the app is loading" "logged_in": "You are logged in.<br /> please wait while the app is loading"
}, },
"wallet_login_qr": { "wallet_login_qr": {
"title": "Import Wallet with QR-Code", "title": "Import Wallet with QR-Code",
"offline_warning": "Cannot connect to the wallet synchronization service. Are you offline?", "scan.description": "To import your wallet from another device, generate a wallet QR-Code there. On the other device, go to<br /><span class=\"path\">User Panel > Wallet > Generate QR</span> to export.",
"scan.description": "To import your wallet from another device, generate a wallet QR-Code there. On the other device, go to<br /><span class=\"path\">User Panel > Wallet > Generate QR</span> to export.<br />Both devices need to be online.<br />For the wallet transfer, your wallet will be temporarily and securely stored on our servers for up to 5 minutes, using double encryption.",
"scan.button": "Scan QR-Code", "scan.button": "Scan QR-Code",
"scan.syncing": "Importing wallet",
"scan.error": "An error occurred transferring wallet: {error}",
"scan.modal.title": "Scan Wallet QR-Code", "scan.modal.title": "Scan Wallet QR-Code",
"scan.success": "Your wallet has been imported!",
"gen.button": "Generate", "gen.button": "Generate",
"gen.description": "To import your wallet from another device, you have to display a QR-Code here on this device, and then scan it with your other device. On the other device, go to<br /><span class=\"path\">User Panel > Wallet > Scan QR</span> to export.<br />Both devices need to be online.<br />For the wallet transfer, your wallet will be temporarily and securely stored on our servers for up to 5 minutes, using double encryption. If your other device does not have a camera, then you should use the \"Enter a TextCode\" option on the previous screen.", "gen.description": "To import your wallet from another device, you have to display a QR-Code here on this device, and then scan it with your other device. On the other device, go to<br /><span class=\"path\">User Panel > Wallet > Scan QR</span> to export.",
"gen.generated": "Scan this QR-Code from the the other device.", "gen.generated": "Scan this QR-Code from the the other device.",
"gen.success": "Your wallet has been imported!",
"success_btn": "Continue to Login" "success_btn": "Continue to Login"
}, },
"wallet_login_textcode": { "wallet_login_textcode": {
"title": "Import Wallet from TextCode", "title": "Import Wallet from TextCode",
"warning": "You have to exchange this TextCode with the other device by any means available to you (email, other messenger apps, etc...). It is highly recommended to use a tool that is end-to-end encrypted. If you can, you should use instead the \"Import with QRCode\" option, as it is safer. If your devices are not connected to the internet, then you can use the \"Import a Wallet File\" option. In this case, you will transfer the wallet file with a USB key, from one device to the other, or for mobile, by connecting your mobile with the USB cable, to the computer, and then transferring the file with the File Transfer utility on Android, or AirDrop/Finder/iTunes on an iPhone/mac/PC. We do not recommend uploading your wallet file to any cloud service." "description": "To generate a TextCode, open a logged in device and go to<br /><span class=\"path\">User Panel > Wallet > Generate TextCode</span> to export.",
"login_btn": "Import with TextCode"
},
"scan_qr": {
"scanning": "Scan the QR-Code"
} }
}, },
"buttons": { "buttons": {
@ -346,7 +344,8 @@
"LocalBrokerIsNotHeadless": "The local broker is not headless.", "LocalBrokerIsNotHeadless": "The local broker is not headless.",
"InvalidNuri": "Invalid NextGraph URI.", "InvalidNuri": "Invalid NextGraph URI.",
"InvalidTarget": "Cannot resolve target.", "InvalidTarget": "Cannot resolve target.",
"ExportWalletTimeOut": "Export of wallet has expired." "ExportWalletTimeOut": "Export of wallet has expired.",
"ConnectionError": "Could not connect to the server."
}, },
"connectivity": { "connectivity": {
"stopped": "Stopped", "stopped": "Stopped",
@ -365,6 +364,13 @@
"about_nextgraph": "About NextGraph", "about_nextgraph": "About NextGraph",
"donate_nextgraph": "Donate to NextGraph" "donate_nextgraph": "Donate to NextGraph"
}, },
"wallet_sync": {
"offline_warning": "You cannot transfer your wallet when offline.<br />Please make sure, you are connected first.",
"textcode.usage_warning": "You have to exchange this TextCode with the other device by any means available to you (email, other messenger apps, etc...). It is highly recommended to use a tool that is end-to-end encrypted. If you can, you should use instead the \"Import with QRCode\" option, as it is safer. If your devices are not connected to the internet, then you can use the \"Import a Wallet File\" option. In this case, you will transfer the wallet file with a USB key, from one device to the other, or for mobile, by connecting your mobile with the USB cable, to the computer, and then transferring the file with the File Transfer utility on Android, or AirDrop/Finder/iTunes on an iPhone/mac/PC. We do not recommend uploading your wallet file to any cloud service.",
"server_transfer_notice": "Both devices need to be online.<br />During this wallet import, your wallet will be temporarily and securely stored on our servers for up to 5 minutes, using two levels of encryption.",
"importing": "Importing wallet",
"error": "An error occurred while synchronizing your wallet:<br />{error}"
},
"emojis": { "emojis": {
"category": { "category": {
"face": "Face", "face": "Face",

@ -10,42 +10,94 @@
--> -->
<!-- <!--
Home page to display for logged in users. @component
Redirects to no-wallet or login page, if not logged in. QR Scanner Component and Route
--> -->
<script> <script lang="ts">
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import { import { t } from "svelte-i18n";
wallet_import_qrcode import { scanned_qr_code } from "../store";
} from "../store"; import { ArrowLeft } from "svelte-heros-v2";
import { Spinner } from "flowbite-svelte";
let tauri_platform = import.meta.env.TAURI_PLATFORM; let tauri_platform = import.meta.env.TAURI_PLATFORM;
let mobile = tauri_platform == "android" || tauri_platform == "ios"; let mobile = tauri_platform == "android" || tauri_platform == "ios";
let webScanner;
let nativeScanner;
function on_qr_scanned(content) {
scanned_qr_code.set(content);
window.history.go(-1);
}
onMount(async () => { onMount(async () => {
//TODO: here we should also take into account the case of a webapp with camera feature, and sue the lib https://www.npmjs.com/package/html5-qrcode
if (mobile) { if (mobile) {
const scanner = await import("@tauri-apps/plugin-barcode-scanner"); // Load Native Scanner
let perms = await scanner.requestPermissions(); nativeScanner = await import("@tauri-apps/plugin-barcode-scanner");
let perms = await nativeScanner.requestPermissions();
console.log(perms); console.log(perms);
wallet_import_qrcode.set(""); scanned_qr_code.set("");
let result = await scanner.scan({ windowed: false, cameraDirection: "back", formats: [scanner.Format.QRCode] }) let result = await nativeScanner.scan({
console.log(result) windowed: false,
wallet_import_qrcode.set(result.content); cameraDirection: "back",
window.history.go(-1); formats: [nativeScanner.Format.QRCode],
});
console.log(result);
on_qr_scanned(result.content);
} else {
// Load Web Scanner
const { Html5QrcodeScanner } = await import("html5-qrcode");
// Init scanner object
webScanner = new Html5QrcodeScanner(
"scanner-div",
{ fps: 10, qrbox: { width: 300, height: 300 }, formatsToSupport: [0] },
false
);
// Add scanner to Screen.
webScanner.render((decoded_text, decoded_result) => {
// Handle scan result
on_qr_scanned(decoded_text);
}, undefined);
// Auto-Request camera permissions (there's no native way, unfortunately...)
setTimeout(() => {
// Auto-start by clicking button
document
.getElementById("html5-qrcode-button-camera-permission")
?.click();
}, 100);
} }
}); });
onDestroy(async () => { onDestroy(async () => {
if (mobile) { if (mobile) {
const scanner = await import("@tauri-apps/plugin-barcode-scanner"); if (nativeScanner) await nativeScanner.cancel();
await scanner.cancel(); } else {
if (webScanner) webScanner.clear();
} }
}); });
</script> </script>
<div class="text-center"> <div class="text-center">
<!-- please translate this too. i didnt want to do it to avoid a merge conflict--> <div>
Scanning the QRcode <h2 class="text-xl mb-6">{$t("pages.scan_qr.scanning")}</h2>
</div> </div>
<!-- Web Scanner -->
<div id="scanner-div"><Spinner /></div>
<div class="mx-auto max-w-xs">
<button
on:click={() => window.history.go(-1)}
class="mt-8 w-full text-gray-500 dark:text-gray-400 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
><ArrowLeft
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>{$t("buttons.back")}</button
>
</div>
</div>

@ -43,7 +43,7 @@
active_session, active_session,
active_wallet, active_wallet,
display_error, display_error,
online online,
} from "../store"; } from "../store";
import { default as ng } from "../api"; import { default as ng } from "../api";
@ -65,7 +65,7 @@
let generated_text_code: string | null = null; let generated_text_code: string | null = null;
// TODO: do that only when needed // generated_text_code = await ng.wallet_export_get_textcode($active_session.session_id); // TODO: do that only when needed // generated_text_code = await ng.wallet_export_get_textcode($active_session.session_id);
let scanner_open = false; let scanner_open = false;
let scanned_qr = null; let scanned_qr = null;
let scan_successful: null | true = null; let scan_successful: null | true = null;
@ -116,7 +116,7 @@
async function on_qr_scanned(text: string) { async function on_qr_scanned(text: string) {
scanned_qr = text; scanned_qr = text;
// TODO: API calls for synchronization @niko // TODO: API calls for synchronization @niko
// //
// example : // example :
// try { // try {
// await ng.wallet_export_rendezvous($active_session.session_id, text); // await ng.wallet_export_rendezvous($active_session.session_id, text);
@ -266,7 +266,7 @@
<div> <div>
<QrCode <QrCode
tabindex="-1" tabindex="-1"
class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white" class="w-7 h-7 text-black transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/> />
</div> </div>
<span class="ml-3">{$t("pages.wallet_info.generate_qr")}</span> <span class="ml-3">{$t("pages.wallet_info.generate_qr")}</span>
@ -462,7 +462,7 @@
{#if !$online} {#if !$online}
<li class="text-left"> <li class="text-left">
<Alert color="red"> <Alert color="red">
{@html $t("pages.wallet_info.offline_warning")} {@html $t("wallet_sync.offline_warning")}
</Alert> </Alert>
</li> </li>
{/if} {/if}
@ -539,13 +539,15 @@
<!-- Notes about generated QR --> <!-- Notes about generated QR -->
<li class="text-left"> <li class="text-left">
{@html $t("pages.wallet_info.gen_qr.notes")} {@html $t("pages.wallet_info.gen_qr.notes")}
<br />
{@html $t("wallet_sync.transfer_notice")}
</li> </li>
<!-- Warning if offline --> <!-- Warning if offline -->
{#if !$online} {#if !$online}
<li class="text-left"> <li class="text-left">
<Alert color="red"> <Alert color="red">
{@html $t("pages.wallet_info.offline_warning")} {@html $t("wallet_sync.offline_warning")}
</Alert> </Alert>
</li> </li>
{/if} {/if}
@ -569,9 +571,8 @@
<QrCode class="w-full h-full" /> <QrCode class="w-full h-full" />
</div> </div>
{:else} {:else}
{@html generated_qr}
{@html generated_qr}
<!--img <!--img
src={generated_qr} src={generated_qr}
title={$t("pages.wallet_info.gen_qr.img_title")} title={$t("pages.wallet_info.gen_qr.img_title")}

@ -32,8 +32,9 @@
active_session, active_session,
set_active_session, set_active_session,
has_wallets, has_wallets,
wallet_import_qrcode, scanned_qr_code,
display_error, display_error,
wallet_from_import,
} from "../store"; } from "../store";
import { QrCode } from "svelte-heros-v2"; import { QrCode } from "svelte-heros-v2";
@ -59,7 +60,6 @@
} }
onMount(async () => { onMount(async () => {
step = "open"; step = "open";
wallets_unsub = wallets.subscribe((value) => { wallets_unsub = wallets.subscribe((value) => {
wallet = selected && $wallets[selected]?.wallet; wallet = selected && $wallets[selected]?.wallet;
@ -96,50 +96,32 @@
} }
} }
}); });
if ($wallet_import_qrcode) {
// Coming from the import Wallet with QR / TextCode ...
let code = $wallet_import_qrcode; if ($wallet_from_import) {
wallet_import_qrcode.set(""); wallet = wallet_from_import;
try { importing = true;
let temp_wallet = await ng.wallet_import_from_code(code); wallet_from_import.set(null);
// TODO: in 2 steps. first display: wallet was retrieved successfully.
// then when user clicks on "continue to login", do: // TODO: Show component: "We got wallet from other device. Please log in to your wallet, to import the device."
wallet = temp_wallet;
importing = true;
} catch(e) {
error = e;
}
} }
// example of getting wallet from TextCode // <!-- TODO: QR / TextCode Success -->
// async () => { // <div class="mt-4">
// try { // <CheckBadge class="w-full" color="green" size="3em" />
// let temp_wallet = await ng.wallet_import_from_code("AABAOAAAAHNb4y7hdWADqFWDgER3J0xvD3K5D9pZ1wd7Bja4c9cWAOFNpmUIZOFRro0UIpZWr5Ah8U7PlRFe1GFZSKuIextFAA8A45zZUJmUPhfdBrcho1vYPfgda0BAgIT1qjzgEkBQAA"); // </div>
// // TODO: in 2 steps. first display: wallet was retrieved successfully. // <div class="mt-4">
// // then when user clicks on "continue to login", do: // {@html $t("pages.wallet_login_qr.scan.success")}
// wallet = temp_wallet; // </div>
// importing = true;
// } catch (e) { // Sample textcode AABAOAAAAHNb4y7hdWADqFWDgER3J0xvD3K5D9pZ1wd7Bja4c9cWAOFNpmUIZOFRro0UIpZWr5Ah8U7PlRFe1GFZSKuIextFAA8A45zZUJmUPhfdBrcho1vYPfgda0BAgIT1qjzgEkBQAA"
// error = e;
// }
// }
// example of rendezvous for desktop and web without cam (please remove it)
// qrcode = await ng.wallet_import_rendezvous(300);
// try {
// let temp_wallet = await ng.wallet_import_from_code(qrcode[1]);
// // TODO: in 2 steps. first display: wallet was retrieved successfully.
// // then when user clicks on "continue to login", do:
// wallet = temp_wallet;
// importing = true;
// } catch (e) {
// error = e;
// }
// TODO: display with QRcode with : // TODO: display with QRcode with :
// {#if qrcode} // {#if qrcode}
// {@html qrcode[0]} // {@html qrcode[0]}
// {/if} // {/if}
}); });
function loggedin() { function loggedin() {
step = "loggedin"; step = "loggedin";
push("#/"); push("#/");
@ -356,7 +338,7 @@
<a href="/wallet/login-qr" use:link> <a href="/wallet/login-qr" use:link>
<button <button
style="min-width: 250px;justify-content: left;" style="min-width: 250px;justify-content: left;"
class="mt-1 text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center justify-center dark:focus:ring-primary-100/55 mb-2" class="mt-1 text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center justify-center dark:focus:ring-primary-100/55 mb-2"
> >
<QrCode class="w-8 h-8 mr-2 -ml-1" /> <QrCode class="w-8 h-8 mr-2 -ml-1" />
{$t("pages.wallet_login.import_qr")} {$t("pages.wallet_login.import_qr")}
@ -365,8 +347,8 @@
<a href="/wallet/login-text-code" use:link> <a href="/wallet/login-text-code" use:link>
<button <button
style="min-width: 250px;justify-content: left;" style="min-width: 250px;justify-content: left;"
class="mt-1 text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mb-2" class="mt-1 text-primary-700 bg-primary-100 hover:bg-primary-100/90 focus:ring-4 focus:ring-primary-700/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-100/55 mb-2"
> >
<svg <svg
class="w-8 h-8 mr-2 -ml-1" class="w-8 h-8 mr-2 -ml-1"
fill="none" fill="none"

@ -1,17 +1,6 @@
<script lang="ts"> <script lang="ts">
import { t } from "svelte-i18n"; import { t } from "svelte-i18n";
import { import { Alert, Modal, Spinner } from "flowbite-svelte";
type Html5QrcodeResult,
type Html5QrcodeScanner,
} from "html5-qrcode";
import {
Alert,
Modal,
Sidebar,
SidebarGroup,
SidebarWrapper,
Spinner,
} from "flowbite-svelte";
import { import {
ArrowLeft, ArrowLeft,
ArrowRightCircle, ArrowRightCircle,
@ -19,49 +8,36 @@
CheckBadge, CheckBadge,
QrCode, QrCode,
} from "svelte-heros-v2"; } from "svelte-heros-v2";
import CenteredLayout from "../lib/CenteredLayout.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { push } from "svelte-spa-router"; import { push } from "svelte-spa-router";
import CenteredLayout from "../lib/CenteredLayout.svelte";
import { wallet_from_import, scanned_qr_code, display_error } from "../store";
import ng from "../api";
let WebQRScannerClassPromise: Promise<typeof Html5QrcodeScanner>; // <a href="/wallet/scanqr" use:link>
let html5QrcodeScanner: Html5QrcodeScanner;
async function load_qr_scanner_lib() {
// Load in browser only
if (!tauri_platform && !WebQRScannerClassPromise) {
WebQRScannerClassPromise = new Promise((resolve) => {
import("html5-qrcode").then((lib) => resolve(lib.Html5QrcodeScanner)); // comment: why you don't use await ?
});
}
// TODO: Load alternative for native apps?
// <a href="/wallet/scanqr" use:link>
}
let top; let top;
const tauri_platform: string | undefined = import.meta.env.TAURI_PLATFORM; const tauri_platform: string | undefined = import.meta.env.TAURI_PLATFORM;
const use_native_cam =
tauri_platform === "ios" || tauri_platform === "android";
// TODO: Check connectivity to sync service.
let connected = true;
let has_camera: boolean | "checking" = "checking"; let has_camera: boolean | "checking" = "checking";
let login_method: "scan" | "gen" | undefined = undefined; let login_method: "scan" | "gen" | undefined = undefined;
let scan_state: let error;
| "before_start"
| "scanning" let scan_state: "before_start" | "importing" = "before_start";
| "has_scanned"
| "success"
| Error = "before_start";
let scanned_qr: string | undefined = undefined;
let gen_state: let gen_state: "before_start" | "generating" | "generated" = "before_start";
| "before_start" let qr_code_html: string | undefined = undefined;
| "generating"
| "generated" const open_scanner = () => {
| "success" push("#/wallet/scanqr");
| Error = "before_start"; };
let generated_qr: string | undefined = undefined;
const check_has_camera = async () => { const check_has_camera = async () => {
if (!tauri_platform) { if (!use_native_cam) {
// If there is a camera, go to scan mode, else gen mode. // If there is a camera, go to scan mode, else gen mode.
try { try {
const devices = await navigator.mediaDevices.enumerateDevices(); const devices = await navigator.mediaDevices.enumerateDevices();
@ -70,81 +46,59 @@
} catch { } catch {
has_camera = false; has_camera = false;
} }
has_camera = false;
login_method = has_camera ? "scan" : "gen"; login_method = has_camera ? "scan" : "gen";
// Load Scanner lib, if necessary.
if (has_camera) load_qr_scanner_lib();
} else { } else {
// TODO: rust API @niko // TODO: There does not seem to be an API for checking, if the native device
// really supports cameras, as far as I can tell?
// https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/barcode-scanner/guest-js/index.ts
has_camera = true;
} }
}; };
check_has_camera();
function on_qr_scanned(text: string) { async function on_qr_scanned(code) {
scan_state = "has_scanned"; login_method = "scan";
scanned_qr = text; scan_state = "importing";
// TODO: API calls for synchronization @niko try {
// ToRemove: const imported_wallet = await ng.wallet_import_from_code(code);
setTimeout(() => { wallet_from_import.set(imported_wallet);
scan_state = "success"; // Login in with imported wallet.
}, 2_000); push("#/wallet/login");
} } catch (e) {
error = e;
function clear_scanner() {
if (html5QrcodeScanner) html5QrcodeScanner.clear();
html5QrcodeScanner = null;
}
async function open_scanner() {
scan_state = "scanning";
const WebQRScanner = await WebQRScannerClassPromise;
html5QrcodeScanner = new WebQRScanner(
"scanner-div",
{ fps: 10, qrbox: { width: 300, height: 300 }, formatsToSupport: [0] },
false
);
html5QrcodeScanner.render((decoded_text, decoded_result) => {
// Handle scan result
on_qr_scanned(decoded_text);
clear_scanner();
}, undefined);
// Auto-Request camera permissions (there's no native way, unfortunately...)
setTimeout(() => {
// Auto-start by clicking button
document.getElementById("html5-qrcode-button-camera-permission")?.click();
}, 100);
}
function close_scanner_modal() {
clear_scanner();
if (scanned_qr) {
scan_state = "has_scanned";
} else {
scan_state = "before_start";
} }
} }
function generate_qr() { async function generate_qr() {
gen_state = "generating"; gen_state = "generating";
// TODO: @niko generated_qr = await ng.generate_export_qr(); try {
// ToRemove: const [qr_code_el, code] = await ng.wallet_import_rendezvous(300);
setTimeout(() => { qr_code_html = qr_code_el;
gen_state = "generated"; gen_state = "generated";
generated_qr = "dummy"; const imported_wallet = await ng.wallet_import_from_code(code);
}, 1500); wallet_from_import.set(imported_wallet);
setTimeout(() => { // Login in with imported wallet.
gen_state = "success"; push("#/wallet/login");
}, 3500); } catch (e) {
error = e;
}
} }
function continue_to_login(wallet) {}
function scrollToTop() { function scrollToTop() {
top.scrollIntoView(); top.scrollIntoView();
} }
onMount(() => scrollToTop());
onMount(() => {
// Handle return from QR scanner.
if ($scanned_qr_code) {
on_qr_scanned($scanned_qr_code);
scanned_qr_code.set("");
} else {
// Or check, if a camera exists and offer scanner or QR generator, respectively.
check_has_camera();
}
scrollToTop();
});
</script> </script>
<CenteredLayout> <CenteredLayout>
@ -159,45 +113,37 @@
<!-- Checking, if camera is available... --> <!-- Checking, if camera is available... -->
{#if login_method === undefined} {#if login_method === undefined}
<!-- TODO: Check connectivity here-->
<div><Spinner /></div> <div><Spinner /></div>
{:else if false} {:else if !connected}
<!-- Warning, if offline --> <!-- Warning, if offline -->
<!-- TODO: just use $online from store to know if it is online --> <!-- TODO: just use $online from store to know if it is online -->
<!-- @Niko isnt online only true, when logged in and connected to a broker? -->
<div class="text-left"> <div class="text-left">
<Alert color="red"> <Alert color="red">
{@html $t("pages.wallet_login_qr.offline_warning")} {@html $t("wallet_sync.offline_warning")}
</Alert> </Alert>
</div> </div>
{:else if error}
<Alert color="red">
{@html $t("wallet_sync.error", {
values: { error: display_error(error) },
})}
</Alert>
{:else if login_method === "scan"} {:else if login_method === "scan"}
<!-- Scan Mode -->
{#if scan_state === "before_start"} {#if scan_state === "before_start"}
<!-- Scan Mode -->
<!-- Notes about QR --> <!-- Notes about QR -->
<div class="text-left text-sm"> <div class="text-left text-sm">
{@html $t("pages.wallet_login_qr.scan.description")} {@html $t("pages.wallet_login_qr.scan.description")}
<br />
{@html $t("wallet_sync.server_transfer_notice")}
</div> </div>
{:else if scan_state === "scanning"} {:else if scan_state === "importing"}
<!-- Modal is down at the bottom --> <div class="mb-4 w-full">
{:else if scan_state === "has_scanned"} {@html $t("wallet_sync.importing")}
<!-- Scanned QR -->
<div>
<Spinner />
</div> </div>
<div class="mt-2">
{$t("pages.wallet_login_qr.scan.syncing")} <div class="w-full"><Spinner /></div>
</div>
{:else if scan_state === "success"}
<div class="mt-4">
<CheckBadge class="w-full" color="green" size="3em" />
</div>
<div class="mt-4">
{@html $t("pages.wallet_login_qr.scan.success")}
</div>
{:else}
<!-- Error -->
{$t("pages.wallet_login_qr.scan.error", {
values: { error: $t("errors." + scan_state) },
})}
{/if} {/if}
{:else if login_method === "gen"} {:else if login_method === "gen"}
<!-- Generate QR Code to log in with another device --> <!-- Generate QR Code to log in with another device -->
@ -205,6 +151,8 @@
<!-- Notes about QR Generation --> <!-- Notes about QR Generation -->
<div class="text-left text-sm"> <div class="text-left text-sm">
{@html $t("pages.wallet_login_qr.gen.description")} {@html $t("pages.wallet_login_qr.gen.description")}
<br />
{@html $t("wallet_sync.transfer_notice")}
</div> </div>
{:else if gen_state === "generating"} {:else if gen_state === "generating"}
<div> <div>
@ -218,34 +166,13 @@
<!-- Generated QR Code --> <!-- Generated QR Code -->
<div> <div>
{#if generated_qr === "dummy"} {@html qr_code_html}
<div title={$t("pages.wallet_info.gen_qr.img_title")}>
<QrCode class="w-full h-full" />
</div>
{:else}
<img
src={generated_qr}
title={$t("pages.wallet_info.gen_qr.img_title")}
alt="pages.wallet_info.gen_qr_alt"
class="w-full h-full"
/>
{/if}
</div>
{:else if gen_state === "success"}
<div class="mt-4">
<CheckBadge class="w-full" color="green" size="3em" />
</div>
<div class="mt-4">
{@html $t("pages.wallet_login_qr.gen.success")}
</div> </div>
{:else}
<!-- gen_state has Error -->
{$t("pages.wallet_login_qr.gen.error")}
{/if} {/if}
{/if} {/if}
<div class="mx-auto"> <div class="mx-auto">
<div class="my-4"> <div class="my-4 mx-1">
{#if login_method === "scan" && scan_state === "before_start"} {#if login_method === "scan" && scan_state === "before_start"}
<!-- Open Scanner Button--> <!-- Open Scanner Button-->
<button <button
@ -270,45 +197,19 @@
/> />
{$t("pages.wallet_login_qr.gen.button")} {$t("pages.wallet_login_qr.gen.button")}
</button> </button>
{:else if scan_state === "success" || gen_state === "success"}
<a href="#/wallet/login">
<button
class="mt-4 w-full text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
<ArrowRightCircle
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>
{$t("pages.wallet_login_qr.success_btn")}
</button>
</a>
{/if} {/if}
<!-- Go Back --> <!-- Go Back -->
{#if scan_state !== "success" && gen_state !== "success"} <button
<button on:click={() => window.history.go(-1)}
on:click={() => window.history.go(-1)} class="mt-8 w-full text-gray-500 dark:text-gray-400 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
class="mt-8 w-full text-gray-500 dark:text-gray-400 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55" ><ArrowLeft
><ArrowLeft tabindex="-1"
tabindex="-1" class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white" />{$t("buttons.back")}</button
/>{$t("buttons.back")}</button >
>
{/if}
</div> </div>
</div> </div>
</div> </div>
<!-- Scanner Open-->
<Modal
title={$t("pages.wallet_login_qr.scan.modal.title")}
placement="center"
on:hide={close_scanner_modal}
open={scan_state === "scanning"}
class="h-[85vh]"
>
<div id="scanner-div" class="h-full">
{$t("pages.wallet_login_qr.scan.modal.loading")}...
</div>
</Modal>
</div> </div>
</CenteredLayout> </CenteredLayout>

@ -1,17 +1,6 @@
<script lang="ts"> <script lang="ts">
import { t } from "svelte-i18n"; import { t } from "svelte-i18n";
import { import { Alert, Modal, Spinner } from "flowbite-svelte";
type Html5QrcodeResult,
type Html5QrcodeScanner,
} from "html5-qrcode";
import {
Alert,
Modal,
Sidebar,
SidebarGroup,
SidebarWrapper,
Spinner,
} from "flowbite-svelte";
import { import {
ArrowLeft, ArrowLeft,
ArrowRightCircle, ArrowRightCircle,
@ -22,31 +11,9 @@
import CenteredLayout from "../lib/CenteredLayout.svelte"; import CenteredLayout from "../lib/CenteredLayout.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { push } from "svelte-spa-router"; import { push } from "svelte-spa-router";
import { online, scanned_qr_code } from "../store";
let WebQRScannerClassPromise: Promise<typeof Html5QrcodeScanner>;
let html5QrcodeScanner: Html5QrcodeScanner;
async function load_qr_scanner_lib() {
// Load in browser only
if (!tauri_platform && !WebQRScannerClassPromise) {
WebQRScannerClassPromise = new Promise((resolve) => {
import("html5-qrcode").then((lib) => resolve(lib.Html5QrcodeScanner));
});
}
// TODO: Load alternative for native apps?
}
let top; let top;
const tauri_platform: string | undefined = import.meta.env.TAURI_PLATFORM;
let has_camera: boolean | "checking" = "checking";
let login_method: "scan" | "gen" | undefined = undefined;
let scan_state:
| "before_start"
| "scanning"
| "has_scanned"
| "success"
| Error = "before_start";
let scanned_qr: string | undefined = undefined;
let gen_state: let gen_state:
| "before_start" | "before_start"
@ -54,93 +21,15 @@
| "generated" | "generated"
| "success" | "success"
| Error = "before_start"; | Error = "before_start";
let generated_qr: string | undefined = undefined; let textcode: string | undefined = undefined;
const check_has_camera = async () => {
if (!tauri_platform) {
// If there is a camera, go to scan mode, else gen mode.
try {
const devices = await navigator.mediaDevices.enumerateDevices();
has_camera =
devices.filter((device) => device.kind === "videoinput").length > 0;
} catch {
has_camera = false;
}
login_method = has_camera ? "scan" : "gen";
// Load Scanner lib, if necessary.
if (has_camera) load_qr_scanner_lib();
} else {
// TODO: rust API @niko
}
};
check_has_camera();
function on_qr_scanned(text: string) {
scan_state = "has_scanned";
scanned_qr = text;
// TODO: API calls for synchronization @niko
// ToRemove:
setTimeout(() => {
scan_state = "success";
}, 2_000);
}
function clear_scanner() {
if (html5QrcodeScanner) html5QrcodeScanner.clear();
html5QrcodeScanner = null;
}
async function open_scanner() {
scan_state = "scanning";
const WebQRScanner = await WebQRScannerClassPromise;
html5QrcodeScanner = new WebQRScanner(
"scanner-div",
{ fps: 10, qrbox: { width: 300, height: 300 }, formatsToSupport: [0] },
false
);
html5QrcodeScanner.render((decoded_text, decoded_result) => {
// Handle scan result
on_qr_scanned(decoded_text);
clear_scanner();
}, undefined);
// Auto-Request camera permissions (there's no native way, unfortunately...) // TODO: Check connectivity to sync service.
setTimeout(() => { let connected = true;
// Auto-start by clicking button
document.getElementById("html5-qrcode-button-camera-permission")?.click();
}, 100);
}
function close_scanner_modal() { const textcode_submit = () => {
clear_scanner(); scanned_qr_code.set(textcode);
if (scanned_qr) { window.history.go(-1);
scan_state = "has_scanned"; };
} else {
scan_state = "before_start";
}
}
function generate_qr() {
gen_state = "generating";
// TODO: @niko generated_qr = await ng.generate_export_qr();
// ToRemove:
setTimeout(() => {
gen_state = "generated";
generated_qr = "dummy";
}, 1500);
setTimeout(() => {
gen_state = "success";
}, 3500);
}
function continue_to_login(wallet) {}
function scrollToTop() {
top.scrollIntoView();
}
onMount(() => scrollToTop());
</script> </script>
<CenteredLayout> <CenteredLayout>
@ -150,161 +39,64 @@
> >
<!-- Title --> <!-- Title -->
<div> <div>
<h2 class="text-xl mb-6">{$t("pages.wallet_login_qr.title")}</h2> <h2 class="text-xl mb-6">{$t("pages.wallet_login_textcode.title")}</h2>
</div> </div>
<!-- Checking, if camera is available... --> <div class="text-left">
{#if login_method === undefined} <Alert color="yellow">
<!-- TODO: Check connectivity here--> {@html $t("wallet_sync.textcode.usage_warning")}
<div><Spinner /></div> </Alert>
{:else if false} </div>
<!-- Warning, if offline -->
<!-- TODO: get connection status to nextgraph.one --> <!-- Disconnection Warning -->
<div class="text-left"> {#if !connected}
<div class="text-left my-4">
<Alert color="red"> <Alert color="red">
{@html $t("pages.wallet_login_qr.offline_warning")} {@html $t("wallet_sync.offline_warning")}
</Alert> </Alert>
</div> </div>
{:else if login_method === "scan"}
<!-- Scan Mode -->
{#if scan_state === "before_start"}
<!-- Notes about QR -->
<div class="text-left text-sm">
{@html $t("pages.wallet_login_qr.scan.description")}
</div>
{:else if scan_state === "scanning"}
<!-- Modal is down at the bottom -->
{:else if scan_state === "has_scanned"}
<!-- Scanned QR -->
<div>
<Spinner />
</div>
<div class="mt-2">
{$t("pages.wallet_login_qr.scan.syncing")}
</div>
{:else if scan_state === "success"}
<div class="mt-4">
<CheckBadge class="w-full" color="green" size="3em" />
</div>
<div class="mt-4">
{@html $t("pages.wallet_login_qr.scan.success")}
</div>
{:else}
<!-- Error -->
{$t("pages.wallet_login_qr.scan.error", {
values: { error: $t("errors." + scan_state) },
})}
{/if}
{:else if login_method === "gen"}
<!-- Generate QR Code to log in with another device -->
{#if gen_state == "before_start"}
<!-- Notes about QR Generation -->
<div class="text-left text-sm">
{@html $t("pages.wallet_login_qr.gen.description")}
</div>
{:else if gen_state === "generating"}
<div>
<Spinner class="w-full" />
</div>
{:else if gen_state === "generated"}
<!-- Notes about generated QR -->
<div class="text-left text-sm">
{@html $t("pages.wallet_login_qr.gen.generated")}
</div>
<!-- Generated QR Code -->
<div>
{#if generated_qr === "dummy"}
<div title={$t("pages.wallet_info.gen_qr.img_title")}>
<QrCode class="w-full h-full" />
</div>
{:else}
<img
src={generated_qr}
title={$t("pages.wallet_info.gen_qr.img_title")}
alt="pages.wallet_info.gen_qr_alt"
class="w-full h-full"
/>
{/if}
</div>
{:else if gen_state === "success"}
<div class="mt-4">
<CheckBadge class="w-full" color="green" size="3em" />
</div>
<div class="mt-4">
{@html $t("pages.wallet_login_qr.gen.success")}
</div>
{:else}
<!-- gen_state has Error -->
{$t("pages.wallet_login_qr.gen.error")}
{/if}
{/if} {/if}
<div class="mx-auto"> <!-- Notes about TextCode entering -->
<div class="my-4"> <div class="text-left text-sm mb-4">
{#if login_method === "scan" && scan_state === "before_start"} {@html $t("pages.wallet_login_textcode.description")}
<!-- Open Scanner Button--> <br />
<button {@html $t("wallet_sync.transfer_notice")}
on:click={open_scanner} </div>
class="mt-4 w-full text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
> <!-- TextCode Input -->
<Camera <textarea
tabindex="-1" rows="6"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white" bind:value={textcode}
/> class="col-span-6 pr-11 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500"
{$t("pages.wallet_login_qr.scan.button")} />
</button>
{:else if login_method === "gen" && gen_state === "before_start"}
<!-- Generate QR Button -->
<button
on:click={generate_qr}
class="mt-4 w-full text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
<QrCode
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>
{$t("pages.wallet_login_qr.gen.button")}
</button>
{:else if scan_state === "success" || gen_state === "success"}
<a href="#/wallet/login">
<button
class="mt-4 w-full text-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
>
<ArrowRightCircle
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>
{$t("pages.wallet_login_qr.success_btn")}
</button>
</a>
{/if}
<!-- Go Back --> <div class="mx-auto">
{#if scan_state !== "success" && gen_state !== "success"} <!-- Submit Button-->
<button <div class="my-4 mx-1">
on:click={() => window.history.go(-1)} <button
class="mt-8 w-full text-gray-500 dark:text-gray-400 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55" class="mt-4 w-full text-white bg-primary-700 disabled:bg-primary-700/50 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
><ArrowLeft on:click={textcode_submit}
tabindex="-1" disabled={!connected || !textcode}
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white" >
/>{$t("buttons.back")}</button <ArrowRightCircle
> tabindex="-1"
{/if} class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>
{$t("pages.wallet_login_textcode.login_btn")}
</button>
<!-- Back Button -->
<button
on:click={() => window.history.go(-1)}
class="mt-8 w-full text-gray-500 dark:text-gray-400 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
><ArrowLeft
tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/>{$t("buttons.back")}</button
>
</div> </div>
</div> </div>
</div> </div>
<!-- Scanner Open-->
<Modal
title={$t("pages.wallet_login_qr.scan.modal.title")}
placement="center"
on:hide={close_scanner_modal}
open={scan_state === "scanning"}
class="h-[85vh]"
>
<div id="scanner-div" class="h-full">
{$t("pages.wallet_login_qr.scan.modal.loading")}...
</div>
</Modal>
</div> </div>
</CenteredLayout> </CenteredLayout>

@ -43,14 +43,16 @@ init({
initialLocale: "en", initialLocale: "en",
}); });
export const display_error = (error:string) => { export const display_error = (error: string) => {
// Check, if error tranlsation does not exist
const parts = error.split(":"); const parts = error.split(":");
let res = get(format)("errors."+parts[0]);
let res = get(format)("errors." + parts[0]);
if (parts[1]) { if (parts[1]) {
res += " "+get(format)("errors."+parts[1]); res += " " + get(format)("errors." + parts[1]);
} }
return res; return res;
} }
export const select_default_lang = async () => { export const select_default_lang = async () => {
let locales = await ng.locales(); let locales = await ng.locales();
@ -157,7 +159,8 @@ export const cur_tab = writable({
}); });
export const wallet_import_qrcode = writable(""); export const scanned_qr_code = writable("");
export const wallet_from_import = writable<null | object>(null);
export const opened_wallets = writable({}); export const opened_wallets = writable({});

@ -16,6 +16,30 @@
} }
/* TODO: hide qr scanner and info button */ /* TODO: hide qr scanner and info button */
#scanner-div {
border: none !important;
}
#scanner-div * {
display: none;
}
#scanner-div__scan_region {
display: block;
}
#scanner-div__scan_region * {
display: block;
}
#scanner-div__dashboard_section_csr {
display: none !important;
}
#html5-qrcode-anchor-scan-type-change {
display: none !important;
}
[alt="Camera based scan"] {
display: none !important;
}
[alt="Info icon"] {
display: none !important;
}
.logo { .logo {

Loading…
Cancel
Save