ng-app: wallet info panel qr scanner and generator frontend

pull/33/head
Laurin Weger 2 months ago
parent 9882667604
commit 4edb821e5e
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 1
      ng-app/package.json
  2. 18
      ng-app/src/locales/en.json
  3. 552
      ng-app/src/routes/WalletInfo.svelte
  4. 3670
      pnpm-lock.yaml

@ -23,6 +23,7 @@
"classnames": "^2.3.2",
"flowbite": "^1.6.5",
"flowbite-svelte": "^0.43.3",
"html5-qrcode": "^2.3.8",
"ng-sdk-js": "workspace:^0.1.0-preview.1",
"svelte-i18n": "^4.0.0",
"svelte-spa-router": "^3.3.0",

@ -18,13 +18,29 @@
},
"wallet_info": {
"title": "Wallet",
"scan_qr": "Scan QR to export",
"generate_qr": "Generate QR to export",
"download": "Download Wallet File",
"download_failed": "Download Failed:<br/>{error}",
"download_in_progress": "Download in progress...",
"download_successful": "You will find the file named \"{wallet_file}\" in your Downloads folder",
"download_file_button": "Click here to download the wallet file",
"remove_wallet": "Remove wallet from Device",
"remove_wallet_confirm": "Are you sure you want to remove this wallet from your 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 />For the wallet transfer, you agree that your double-encrypted wallet is stored on our servers for no more than 5 minutes.",
"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 />For the wallet transfer, you agree for your double-encrypted wallet to be stored on our servers for no more than 5 minutes.",
"gen_qr.img_title": "Your Export QR Code to Scan",
"gen_qr.img_alt": "Your Export QR Code to Scan",
"gen_qr.gen_button": "Generate QR Code"
},
"settings": {
"title": "Settings"

@ -15,8 +15,8 @@
Provides info about wallet, broker, etc. and download option.
-->
<script>
import { Modal } from "flowbite-svelte";
<script lang="ts">
import { Alert, Button, Modal, Spinner } from "flowbite-svelte";
import { link, push } from "svelte-spa-router";
import CenteredLayout from "../lib/CenteredLayout.svelte";
import {
@ -27,14 +27,28 @@
QrCode,
Link,
ArrowDownOnSquare,
Camera,
CheckBadge,
} from "svelte-heros-v2";
import { onMount, tick } from "svelte";
import { Sidebar, SidebarGroup, SidebarWrapper } from "flowbite-svelte";
import { t } from "svelte-i18n";
import { close_active_wallet, active_session, active_wallet } from "../store";
import {
type Html5QrcodeResult,
type Html5QrcodeScanner,
} from "html5-qrcode";
import {
close_active_wallet,
active_session,
active_wallet,
online,
} from "../store";
import { default as ng } from "../api";
let WebQRScannerClassPromise: Promise<typeof Html5QrcodeScanner>;
let tauri_platform = import.meta.env.TAURI_PLATFORM;
let error;
let nonActiveClass =
@ -42,6 +56,29 @@
let top;
let sub_menu: "scan_qr" | "generate_qr" | "text_code" | null = null;
/** QR source / blob URL */
let generation_state: "loading" | "generated" | null = null;
let generated_qr: string | undefined = undefined;
let generated_text_code: string | null = null;
let scanner_open = false;
let scanned_qr = null;
let scan_successful: null | true = null;
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?
}
async function scrollToTop() {
await tick();
top.scrollIntoView();
@ -54,6 +91,68 @@
}
});
function open_scan_menu() {
sub_menu = "scan_qr";
load_qr_scanner_lib();
}
async function open_gen_menu() {
sub_menu = "generate_qr";
generation_state = null;
}
async function gen_qr() {
generation_state = "loading"; // TODO: @niko = await ng.generate_export_qr();
// ToRemove:
setTimeout(() => {
generation_state = "generated";
generated_qr = "dummy";
}, 3000);
}
function on_qr_scanned(text: string) {
scanned_qr = text;
// TODO: API calls for synchronization @niko
// ToRemove:
setTimeout(() => {
scan_successful = true;
}, 2_000);
}
async function open_scanner() {
scanner_open = true;
const onScanSuccess = (
decoded_text: string,
decoded_result: Html5QrcodeResult
) => {
// handle the scanned code as you like, for example:
on_qr_scanned(decoded_text);
close_scanner();
// console.log(`Code matched = ${decoded_text}`, decodedResult);
};
const WebQRScanner = await WebQRScannerClassPromise;
html5QrcodeScanner = new WebQRScanner(
"scanner-div",
{ fps: 10, qrbox: { width: 300, height: 300 }, formatsToSupport: [0] },
/* verbose= */ false
);
html5QrcodeScanner.render(onScanSuccess, undefined);
}
function close_scanner() {
scanner_open = false;
if (html5QrcodeScanner) html5QrcodeScanner.clear();
html5QrcodeScanner = null;
}
function to_main_menu() {
sub_menu = null;
generated_qr = "loading";
generated_text_code = null;
}
let downloading = false;
let wallet_file_ready = false;
let download_link = false;
@ -102,206 +201,371 @@
<SidebarWrapper
divClass="bg-gray-60 overflow-y-auto py-4 px-3 rounded dark:bg-gray-800"
>
<SidebarGroup ulClass="space-y-2" role="menu">
<li>
<h2 class="text-xl mb-6">{$t("pages.wallet_info.title")}</h2>
</li>
<!-- Go Back -->
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={() => window.history.go(-1)}
on:click={() => window.history.go(-1)}
>
<ArrowLeft
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"
/>
<span class="ml-3">Back</span>
</li>
{#if sub_menu === null}
<SidebarGroup ulClass="space-y-2" role="menu">
<li>
<h2 class="text-xl mb-6">{$t("pages.wallet_info.title")}</h2>
</li>
<!-- Download Wallet -->
<!-- Go Back -->
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={() => window.history.go(-1)}
on:click={() => window.history.go(-1)}
>
<ArrowLeft
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"
/>
<span class="ml-3">{$t("buttons.back")}</span>
</li>
{#if !downloading}
<!-- Scan QR Code to log in with another device -->
<li
tabindex="0"
role="menuitem"
class="flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={download_wallet}
on:click={download_wallet}
on:keypress={open_scan_menu}
on:click={open_scan_menu}
>
<div>
<DocumentArrowDown
<Camera
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"
/>
</div>
<span class="ml-3">{$t("pages.wallet_info.download")}</span>
<span class="ml-3">{$t("pages.wallet_info.scan_qr")}</span>
</li>
{:else if download_error}
<!-- Generate QR Code to log in with another device -->
<li
tabindex="-1"
class="flex items-center p-2 text-base font-normal text-red-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
tabindex="0"
role="menuitem"
class="flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={open_gen_menu}
on:click={open_gen_menu}
>
<div>
<NoSymbol
<QrCode
tabindex="-1"
class="w-7 h-7 text-red-700 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 text-left"
>{$t("pages.wallet_info.download_failed", {
values: { error: download_error },
})}</span
>
<span class="ml-3">{$t("pages.wallet_info.generate_qr")}</span>
</li>
{:else if !wallet_file_ready}
<li
tabindex="-1"
class="flex items-center p-2 text-base font-normal text-blue-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<div>
<DocumentArrowDown
tabindex="-1"
class="w-7 h-7 text-blue-700 transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</div>
<span class="ml-3 text-left"
>{$t("pages.wallet_info.download_in_progress")}</span
<!-- Download Wallet -->
{#if !downloading}
<li
tabindex="0"
role="menuitem"
class="flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={download_wallet}
on:click={download_wallet}
>
</li>
{:else if download_link === true}
<li
tabindex="-1"
class="flex p-2 text-sm text-left break-all font-normal text-blue-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<span
>{@html $t("pages.wallet_info.download_successful", {
values: { wallet_file: wallet_file_ready },
})}</span
<div>
<DocumentArrowDown
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"
/>
</div>
<span class="ml-3">{$t("pages.wallet_info.download")}</span>
</li>
{:else if download_error}
<li
tabindex="-1"
class="flex items-center p-2 text-base font-normal text-red-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
</li>
{:else}
<li
tabindex="-1"
class="flex items-center text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<a
href={download_link}
target="_blank"
download={wallet_file_ready}
<div>
<NoSymbol
tabindex="-1"
class="w-7 h-7 text-red-700 transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</div>
<span class="ml-3 text-left"
>{$t("pages.wallet_info.download_failed", {
values: { error: download_error },
})}</span
>
</li>
{:else if !wallet_file_ready}
<li
tabindex="-1"
class="flex items-center p-2 text-base font-normal text-blue-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<button
tabindex="-1"
class=" text-white bg-primary-700 hover:bg-primary-700/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-700/55"
<div>
<DocumentArrowDown
tabindex="-1"
class="w-7 h-7 text-blue-700 transition duration-75 dark:text-white group-hover:text-gray-900 dark:group-hover:text-white"
/>
</div>
<span class="ml-3 text-left"
>{$t("pages.wallet_info.download_in_progress")}</span
>
<div>
<DocumentArrowDown
tabindex="-1"
class="w-14 h-14 transition duration-75 dark:text-white dark:group-hover:text-white"
/>
</div>
{$t("pages.wallet_info.download_file_button")}
</button>
</a>
</li>
{/if}
<!-- Remove Wallet -->
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={remove_wallet_clicked}
on:click={remove_wallet_clicked}
>
<div>
<Trash
</li>
{:else if download_link === true}
<li
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"
/>
</div>
<span class="ml-3">{$t("pages.wallet_info.remove_wallet")}</span>
</li>
<Modal
autoclose
outsideclose
bind:open={wallet_remove_modal_open}
title="Remove Wallet"
>
<p class="mt-4">
{$t("pages.wallet_info.remove_confirm")}
</p>
<div class="mt-4 flex justify-end">
<button on:click={close_modal}>{$t("buttons.cancel")}</button>
<button
class="mr-2 bg-primary-700 text-white"
on:click={remove_wallet_confirmed}
class="flex p-2 text-sm text-left break-all font-normal text-blue-700 rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
{$t("buttons.remove")}
</button>
</div>
</Modal>
<span
>{@html $t("pages.wallet_info.download_successful", {
values: { wallet_file: wallet_file_ready },
})}</span
>
</li>
{:else}
<li
tabindex="-1"
class="flex items-center text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<a
href={download_link || ""}
target="_blank"
download={wallet_file_ready}
>
<button
tabindex="-1"
class=" text-white bg-primary-700 hover:bg-primary-700/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-700/55"
>
<div>
<DocumentArrowDown
tabindex="-1"
class="w-14 h-14 transition duration-75 dark:text-white dark:group-hover:text-white"
/>
</div>
{$t("pages.wallet_info.download_file_button")}
</button>
</a>
</li>
{/if}
<!-- TODO: Show QRCode -->
{#if false}
<!-- Remove Wallet -->
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={remove_wallet_clicked}
on:click={remove_wallet_clicked}
>
<div>
<QrCode
<Trash
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"
/>
</div>
<span class="ml-3">{$t("qr_code")}</span>
<span class="ml-3">{$t("pages.wallet_info.remove_wallet")}</span
>
</li>
<!-- Confirm Remove Wallet Modal -->
<Modal
autoclose
outsideclose
title={$t("pages.wallet_info.qr_modal_title")}
>{@html $t("pages.wallet_info.qr_modal_description")}
bind:open={wallet_remove_modal_open}
title={$t("pages.wallet_info.remove_wallet_modal.title")}
>
<p class="mt-4">
{$t("pages.wallet_info.remove_wallet_modal.confirm")}
</p>
<div class="mt-4 flex justify-end">
<button class="mr-2" on:click={close_modal}
>{$t("buttons.cancel")}</button
>
<button
class=" 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"
on:click={remove_wallet_confirmed}
>
{$t("buttons.remove")}
</button>
</div>
</Modal>
{/if}
<!-- TODO: Copy Wallet Link -->
{#if false}
<!-- TODO: Copy Wallet Link -->
{#if false}
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<div>
<Link
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"
/>
</div>
<span class="ml-3">{$t("pages.login.copy_wallet_link")}</span>
</li>
{/if}
<!-- TODO: Save to Device -->
{#if false}
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
>
<!-- TODO: Same as with the trash icon, this is not same-sized as the others. -->
<ArrowDownOnSquare
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"
/>
<span class="ml-3">{$t("pages.login.keep_wallet")}</span>
</li>
{/if}
</SidebarGroup>
{:else if sub_menu === "scan_qr"}
<SidebarGroup ulClass="space-y-2" role="menu">
<li>
<h2 class="text-xl mb-6">
{$t("pages.wallet_info.scan_qr.title")}
</h2>
</li>
<!-- Go Back -->
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={to_main_menu}
on:click={to_main_menu}
>
<div>
<Link
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"
/>
</div>
<span class="ml-3">{$t("pages.login.copy_wallet_link")}</span>
<ArrowLeft
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"
/>
<span class="ml-3">{$t("buttons.back")}</span>
</li>
<!-- NOTES ABOUT QR-->
<li class="text-left">
{@html $t("pages.wallet_info.scan_qr.notes")}
</li>
{/if}
<!-- TODO: Save to Device -->
{#if false}
<!-- Warning if offline -->
{#if !$online}
<li class="text-left">
<Alert color="red">
{@html $t("pages.wallet_info.offline_warning")}
</Alert>
</li>
{/if}
{#if scan_successful}
<li class="text-green-800 flex flex-col items-center">
<div class="mt-4">
<CheckBadge color="green" size="3em" />
</div>
<div class="mt-4">
{@html $t("pages.wallet_info.scan_qr.scan_successful")}
</div>
</li>
{:else if scanned_qr}
<li class="">
<Spinner class="mt-4 mb-2" />
<div>
{@html $t("pages.wallet_info.scan_qr.syncing")}...
<br />
<br />
{scanned_qr}
</div>
</li>
{:else if !scanner_open}
<Button
on:click={open_scanner}
disabled={false || !$online}
class="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"
>
{#if false}
<Spinner class="mr-2" size="6" />
{/if}
{$t("pages.wallet_info.scan_qr.scan_btn")}
</Button>
{:else}
<!-- Scanner Open-->
<Modal
title={$t("pages.wallet_info.scan_qr.scanner.title")}
placement="center"
on:hide={close_scanner}
open={scanner_open}
class="h-[90vh]"
>
<div id="scanner-div" class="h-full">
{$t("pages.wallet_info.scan_qr.scanner.loading")}...
</div>
</Modal>
{/if}
</SidebarGroup>
<!-- Generate QR-Code screen -->
{:else if sub_menu === "generate_qr"}
<SidebarGroup ulClass="space-y-2" role="menu">
<li>
<h2 class="text-xl mb-6">
{$t("pages.wallet_info.gen_qr.title")}
</h2>
</li>
<!-- Go Back -->
<li
tabindex="0"
role="menuitem"
class="text-left flex items-center p-2 text-base font-normal text-gray-900 clickable rounded-lg dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700"
on:keypress={to_main_menu}
on:click={to_main_menu}
>
<!-- TODO: Same as with the trash icon, this is not same-sized as the others. -->
<ArrowDownOnSquare
<ArrowLeft
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"
/>
<span class="ml-3">{$t("pages.login.keep_wallet")}</span>
<span class="ml-3">{$t("buttons.back")}</span>
</li>
<!-- Notes about generated QR -->
<li class="text-left">
{@html $t("pages.wallet_info.gen_qr.notes")}
</li>
{/if}
</SidebarGroup>
<!-- Warning if offline -->
{#if !$online}
<li class="text-left">
<Alert color="red">
{@html $t("pages.wallet_info.offline_warning")}
</Alert>
</li>
{/if}
{#if !generated_qr || generation_state === "loading"}
<Button
on:click={gen_qr}
disabled={generation_state === "loading" || !$online}
class="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"
>
{#if generation_state === "loading"}
<Spinner class="mr-2" size="6" />
{/if}
{$t("pages.wallet_info.gen_qr.gen_button")}
</Button>
{:else}
<!-- 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>
{/if}
</SidebarGroup>
{:else if sub_menu === "text_code"}
TODO: Export with text code
{/if}
</SidebarWrapper>
</Sidebar>
</div>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save