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 2 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_modal.title": "Remove wallet?",
"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.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.scanner.title": "Scan your QR Code",
"scan_qr.scanner.loading": "Loading scanner",
"scan_qr.syncing": "Synchronizing wallet",
"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.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_alt": "Your Export QR Code to Scan",
"gen_qr.gen_button": "Display QR Code"
@ -243,29 +242,28 @@
"with_another_wallet": "Log in with another wallet",
"import_wallet": "Import your wallet",
"import_file": "Import a Wallet File",
"import_qr": "Import with QRCode",
"import_link": "Enter a Wallet Link",
"import_qr": "Import with QR-Code",
"import_link": "Import with TextCode",
"new_wallet": "Create a new Wallet",
"logged_in": "You are logged in.<br /> please wait while the app is loading"
},
"wallet_login_qr": {
"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.<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.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.button": "Scan QR-Code",
"scan.syncing": "Importing wallet",
"scan.error": "An error occurred transferring wallet: {error}",
"scan.modal.title": "Scan Wallet QR-Code",
"scan.success": "Your wallet has been imported!",
"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.success": "Your wallet has been imported!",
"success_btn": "Continue to Login"
},
"wallet_login_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": {
@ -346,7 +344,8 @@
"LocalBrokerIsNotHeadless": "The local broker is not headless.",
"InvalidNuri": "Invalid NextGraph URI.",
"InvalidTarget": "Cannot resolve target.",
"ExportWalletTimeOut": "Export of wallet has expired."
"ExportWalletTimeOut": "Export of wallet has expired.",
"ConnectionError": "Could not connect to the server."
},
"connectivity": {
"stopped": "Stopped",
@ -365,6 +364,13 @@
"about_nextgraph": "About 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": {
"category": {
"face": "Face",

@ -10,42 +10,94 @@
-->
<!--
Home page to display for logged in users.
Redirects to no-wallet or login page, if not logged in.
@component
QR Scanner Component and Route
-->
<script>
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import {
wallet_import_qrcode
} from "../store";
import { t } from "svelte-i18n";
import { scanned_qr_code } from "../store";
import { ArrowLeft } from "svelte-heros-v2";
import { Spinner } from "flowbite-svelte";
let tauri_platform = import.meta.env.TAURI_PLATFORM;
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 () => {
//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) {
const scanner = await import("@tauri-apps/plugin-barcode-scanner");
let perms = await scanner.requestPermissions();
// Load Native Scanner
nativeScanner = await import("@tauri-apps/plugin-barcode-scanner");
let perms = await nativeScanner.requestPermissions();
console.log(perms);
wallet_import_qrcode.set("");
let result = await scanner.scan({ windowed: false, cameraDirection: "back", formats: [scanner.Format.QRCode] })
console.log(result)
wallet_import_qrcode.set(result.content);
window.history.go(-1);
scanned_qr_code.set("");
let result = await nativeScanner.scan({
windowed: false,
cameraDirection: "back",
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 () => {
if (mobile) {
const scanner = await import("@tauri-apps/plugin-barcode-scanner");
await scanner.cancel();
if (nativeScanner) await nativeScanner.cancel();
} else {
if (webScanner) webScanner.clear();
}
});
</script>
<div class="text-center">
<!-- please translate this too. i didnt want to do it to avoid a merge conflict-->
Scanning the QRcode
</div>
<div>
<h2 class="text-xl mb-6">{$t("pages.scan_qr.scanning")}</h2>
</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_wallet,
display_error,
online
online,
} from "../store";
import { default as ng } from "../api";
@ -65,7 +65,7 @@
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);
let scanner_open = false;
let scanned_qr = null;
let scan_successful: null | true = null;
@ -116,7 +116,7 @@
async function on_qr_scanned(text: string) {
scanned_qr = text;
// TODO: API calls for synchronization @niko
//
//
// example :
// try {
// await ng.wallet_export_rendezvous($active_session.session_id, text);
@ -266,7 +266,7 @@
<div>
<QrCode
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>
<span class="ml-3">{$t("pages.wallet_info.generate_qr")}</span>
@ -462,7 +462,7 @@
{#if !$online}
<li class="text-left">
<Alert color="red">
{@html $t("pages.wallet_info.offline_warning")}
{@html $t("wallet_sync.offline_warning")}
</Alert>
</li>
{/if}
@ -539,13 +539,15 @@
<!-- Notes about generated QR -->
<li class="text-left">
{@html $t("pages.wallet_info.gen_qr.notes")}
<br />
{@html $t("wallet_sync.transfer_notice")}
</li>
<!-- Warning if offline -->
{#if !$online}
<li class="text-left">
<Alert color="red">
{@html $t("pages.wallet_info.offline_warning")}
{@html $t("wallet_sync.offline_warning")}
</Alert>
</li>
{/if}
@ -569,9 +571,8 @@
<QrCode class="w-full h-full" />
</div>
{:else}
{@html generated_qr}
{@html generated_qr}
<!--img
src={generated_qr}
title={$t("pages.wallet_info.gen_qr.img_title")}

@ -32,8 +32,9 @@
active_session,
set_active_session,
has_wallets,
wallet_import_qrcode,
scanned_qr_code,
display_error,
wallet_from_import,
} from "../store";
import { QrCode } from "svelte-heros-v2";
@ -59,7 +60,6 @@
}
onMount(async () => {
step = "open";
wallets_unsub = wallets.subscribe((value) => {
wallet = selected && $wallets[selected]?.wallet;
@ -96,50 +96,32 @@
}
}
});
if ($wallet_import_qrcode) {
let code = $wallet_import_qrcode;
wallet_import_qrcode.set("");
try {
let temp_wallet = await ng.wallet_import_from_code(code);
// 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;
}
// Coming from the import Wallet with QR / TextCode ...
if ($wallet_from_import) {
wallet = wallet_from_import;
importing = true;
wallet_from_import.set(null);
// TODO: Show component: "We got wallet from other device. Please log in to your wallet, to import the device."
}
// example of getting wallet from TextCode
// async () => {
// try {
// let temp_wallet = await ng.wallet_import_from_code("AABAOAAAAHNb4y7hdWADqFWDgER3J0xvD3K5D9pZ1wd7Bja4c9cWAOFNpmUIZOFRro0UIpZWr5Ah8U7PlRFe1GFZSKuIextFAA8A45zZUJmUPhfdBrcho1vYPfgda0BAgIT1qjzgEkBQAA");
// // 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: QR / TextCode 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>
// Sample textcode AABAOAAAAHNb4y7hdWADqFWDgER3J0xvD3K5D9pZ1wd7Bja4c9cWAOFNpmUIZOFRro0UIpZWr5Ah8U7PlRFe1GFZSKuIextFAA8A45zZUJmUPhfdBrcho1vYPfgda0BAgIT1qjzgEkBQAA"
// 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 :
// {#if qrcode}
// {@html qrcode[0]}
// {/if}
});
function loggedin() {
step = "loggedin";
push("#/");
@ -356,7 +338,7 @@
<a href="/wallet/login-qr" use:link>
<button
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" />
{$t("pages.wallet_login.import_qr")}
@ -365,8 +347,8 @@
<a href="/wallet/login-text-code" use:link>
<button
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
class="w-8 h-8 mr-2 -ml-1"
fill="none"

@ -1,17 +1,6 @@
<script lang="ts">
import { t } from "svelte-i18n";
import {
type Html5QrcodeResult,
type Html5QrcodeScanner,
} from "html5-qrcode";
import {
Alert,
Modal,
Sidebar,
SidebarGroup,
SidebarWrapper,
Spinner,
} from "flowbite-svelte";
import { Alert, Modal, Spinner } from "flowbite-svelte";
import {
ArrowLeft,
ArrowRightCircle,
@ -19,49 +8,36 @@
CheckBadge,
QrCode,
} from "svelte-heros-v2";
import CenteredLayout from "../lib/CenteredLayout.svelte";
import { onMount } from "svelte";
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>;
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>
}
// <a href="/wallet/scanqr" use:link>
let top;
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 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 error;
let scan_state: "before_start" | "importing" = "before_start";
let gen_state:
| "before_start"
| "generating"
| "generated"
| "success"
| Error = "before_start";
let generated_qr: string | undefined = undefined;
let gen_state: "before_start" | "generating" | "generated" = "before_start";
let qr_code_html: string | undefined = undefined;
const open_scanner = () => {
push("#/wallet/scanqr");
};
const check_has_camera = async () => {
if (!tauri_platform) {
if (!use_native_cam) {
// If there is a camera, go to scan mode, else gen mode.
try {
const devices = await navigator.mediaDevices.enumerateDevices();
@ -70,81 +46,59 @@
} catch {
has_camera = false;
}
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
// 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) {
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...)
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";
async function on_qr_scanned(code) {
login_method = "scan";
scan_state = "importing";
try {
const imported_wallet = await ng.wallet_import_from_code(code);
wallet_from_import.set(imported_wallet);
// Login in with imported wallet.
push("#/wallet/login");
} catch (e) {
error = e;
}
}
function generate_qr() {
async function generate_qr() {
gen_state = "generating";
// TODO: @niko generated_qr = await ng.generate_export_qr();
// ToRemove:
setTimeout(() => {
try {
const [qr_code_el, code] = await ng.wallet_import_rendezvous(300);
qr_code_html = qr_code_el;
gen_state = "generated";
generated_qr = "dummy";
}, 1500);
setTimeout(() => {
gen_state = "success";
}, 3500);
const imported_wallet = await ng.wallet_import_from_code(code);
wallet_from_import.set(imported_wallet);
// Login in with imported wallet.
push("#/wallet/login");
} catch (e) {
error = e;
}
}
function continue_to_login(wallet) {}
function scrollToTop() {
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>
<CenteredLayout>
@ -159,45 +113,37 @@
<!-- Checking, if camera is available... -->
{#if login_method === undefined}
<!-- TODO: Check connectivity here-->
<div><Spinner /></div>
{:else if false}
{:else if !connected}
<!-- Warning, if offline -->
<!-- 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">
<Alert color="red">
{@html $t("pages.wallet_login_qr.offline_warning")}
{@html $t("wallet_sync.offline_warning")}
</Alert>
</div>
{:else if error}
<Alert color="red">
{@html $t("wallet_sync.error", {
values: { error: display_error(error) },
})}
</Alert>
{:else if login_method === "scan"}
<!-- Scan Mode -->
{#if scan_state === "before_start"}
<!-- Scan Mode -->
<!-- Notes about QR -->
<div class="text-left text-sm">
{@html $t("pages.wallet_login_qr.scan.description")}
<br />
{@html $t("wallet_sync.server_transfer_notice")}
</div>
{:else if scan_state === "scanning"}
<!-- Modal is down at the bottom -->
{:else if scan_state === "has_scanned"}
<!-- Scanned QR -->
<div>
<Spinner />
{:else if scan_state === "importing"}
<div class="mb-4 w-full">
{@html $t("wallet_sync.importing")}
</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) },
})}
<div class="w-full"><Spinner /></div>
{/if}
{:else if login_method === "gen"}
<!-- Generate QR Code to log in with another device -->
@ -205,6 +151,8 @@
<!-- Notes about QR Generation -->
<div class="text-left text-sm">
{@html $t("pages.wallet_login_qr.gen.description")}
<br />
{@html $t("wallet_sync.transfer_notice")}
</div>
{:else if gen_state === "generating"}
<div>
@ -218,34 +166,13 @@
<!-- 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")}
{@html qr_code_html}
</div>
{:else}
<!-- gen_state has Error -->
{$t("pages.wallet_login_qr.gen.error")}
{/if}
{/if}
<div class="mx-auto">
<div class="my-4">
<div class="my-4 mx-1">
{#if login_method === "scan" && scan_state === "before_start"}
<!-- Open Scanner Button-->
<button
@ -270,45 +197,19 @@
/>
{$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 -->
{#if scan_state !== "success" && gen_state !== "success"}
<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
>
{/if}
<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>
<!-- 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>
</CenteredLayout>

@ -1,17 +1,6 @@
<script lang="ts">
import { t } from "svelte-i18n";
import {
type Html5QrcodeResult,
type Html5QrcodeScanner,
} from "html5-qrcode";
import {
Alert,
Modal,
Sidebar,
SidebarGroup,
SidebarWrapper,
Spinner,
} from "flowbite-svelte";
import { Alert, Modal, Spinner } from "flowbite-svelte";
import {
ArrowLeft,
ArrowRightCircle,
@ -22,31 +11,9 @@
import CenteredLayout from "../lib/CenteredLayout.svelte";
import { onMount } from "svelte";
import { push } from "svelte-spa-router";
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?
}
import { online, scanned_qr_code } from "../store";
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:
| "before_start"
@ -54,93 +21,15 @@
| "generated"
| "success"
| Error = "before_start";
let generated_qr: 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);
let textcode: string | undefined = 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);
}
// TODO: Check connectivity to sync service.
let connected = true;
function close_scanner_modal() {
clear_scanner();
if (scanned_qr) {
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());
const textcode_submit = () => {
scanned_qr_code.set(textcode);
window.history.go(-1);
};
</script>
<CenteredLayout>
@ -150,161 +39,64 @@
>
<!-- Title -->
<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>
<!-- Checking, if camera is available... -->
{#if login_method === undefined}
<!-- TODO: Check connectivity here-->
<div><Spinner /></div>
{:else if false}
<!-- Warning, if offline -->
<!-- TODO: get connection status to nextgraph.one -->
<div class="text-left">
<div class="text-left">
<Alert color="yellow">
{@html $t("wallet_sync.textcode.usage_warning")}
</Alert>
</div>
<!-- Disconnection Warning -->
{#if !connected}
<div class="text-left my-4">
<Alert color="red">
{@html $t("pages.wallet_login_qr.offline_warning")}
{@html $t("wallet_sync.offline_warning")}
</Alert>
</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}
<div class="mx-auto">
<div class="my-4">
{#if login_method === "scan" && scan_state === "before_start"}
<!-- Open Scanner Button-->
<button
on:click={open_scanner}
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"
>
<Camera
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.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}
<!-- Notes about TextCode entering -->
<div class="text-left text-sm mb-4">
{@html $t("pages.wallet_login_textcode.description")}
<br />
{@html $t("wallet_sync.transfer_notice")}
</div>
<!-- TextCode Input -->
<textarea
rows="6"
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"
/>
<!-- Go Back -->
{#if scan_state !== "success" && gen_state !== "success"}
<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
>
{/if}
<div class="mx-auto">
<!-- Submit Button-->
<div class="my-4 mx-1">
<button
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"
on:click={textcode_submit}
disabled={!connected || !textcode}
>
<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_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>
<!-- 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>
</CenteredLayout>

@ -43,14 +43,16 @@ init({
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(":");
let res = get(format)("errors."+parts[0]);
let res = get(format)("errors." + parts[0]);
if (parts[1]) {
res += " "+get(format)("errors."+parts[1]);
res += " " + get(format)("errors." + parts[1]);
}
return res;
}
}
export const select_default_lang = async () => {
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({});

@ -16,6 +16,30 @@
}
/* 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 {

Loading…
Cancel
Save