Import wallet with QR- and TextCode #32

Closed
laurin wants to merge 13 commits from feat/ng-app/sync-wallets into master
  1. 81
      nextgraph/src/local_broker.rs
  2. 6
      ng-app/src-tauri/src/lib.rs
  3. 26
      ng-app/src/api.ts
  4. 2
      ng-app/src/classes.ts
  5. 4
      ng-app/src/lib/Login.svelte
  6. 2
      ng-app/src/lib/NoWallet.svelte
  7. 2
      ng-app/src/lib/components/PasswordInput.svelte
  8. 54
      ng-app/src/locales/en.json
  9. 89
      ng-app/src/routes/ScanQR.svelte
  10. 22
      ng-app/src/routes/WalletCreate.svelte
  11. 378
      ng-app/src/routes/WalletInfo.svelte
  12. 13
      ng-app/src/routes/WalletLogin.svelte
  13. 173
      ng-app/src/routes/WalletLoginQr.svelte
  14. 13
      ng-app/src/routes/WalletLoginTextCode.svelte
  15. 35
      ng-app/src/store.ts
  16. 206
      ng-app/src/styles.css
  17. 1
      ng-net/src/actors/ext/mod.rs
  18. 15
      ng-wallet/src/emojis.rs
  19. 21
      ng-wallet/src/lib.rs

@ -44,8 +44,10 @@ use ng_verifier::types::*;
use ng_verifier::verifier::Verifier;
use ng_wallet::bip39::encode_mnemonic;
use ng_wallet::emojis::encode_pazzle;
use ng_wallet::{create_wallet_first_step_v0, create_wallet_second_step_v0, types::*};
use ng_wallet::emojis::{display_pazzle, encode_pazzle};
use ng_wallet::{
create_wallet_first_step_v0, create_wallet_second_step_v0, display_mnemonic, types::*,
};
#[cfg(not(target_family = "wasm"))]
use ng_client_ws::remote_ws::ConnectionWebSocket;
@ -1582,10 +1584,10 @@ pub fn wallet_to_wallet_recovery(
/// Generates the Recovery PDF containing the Wallet, PIN, Pazzle and Mnemonic.
pub async fn wallet_recovery_pdf(
content: NgQRCodeWalletRecoveryV0,
recovery: NgQRCodeWalletRecoveryV0,
size: u32,
) -> Result<Vec<u8>, NgError> {
let ser = serde_bare::to_vec(&content)?;
let ser = serde_bare::to_vec(&recovery)?;
if ser.len() > 2_953 {
return Err(NgError::InvalidPayload);
}
@ -1607,20 +1609,7 @@ pub async fn wallet_recovery_pdf(
let tree = svg2pdf::usvg::Tree::from_str(&wallet_svg, &options)
.map_err(|e| NgError::WalletError(e.to_string()))?;
// PDF uses A4 format (21cm x 29.7cm)
// TODO: instead of to_pdf in the next line, do to_chunk, and then add the text below the SVG.
// the SVG should take all the width of the A4 (so that only 29.7-21 = 8cm remains below the SVG, for all the following)
// the text is :
// - one line with : "PIN = 1234 pazzle = cat_slug:emoji_slug cat_slug:emoji_slug ...[x9]"
// - one line with the 9 emoji SVGs (with size so they fit in one line, width of the A4)
// - one line with : "mnemonic = [12 words of mnemonic]"
// - one line with recovery_str (it is quite long. choose a font size that make it fit here so the whole document is only one page)
// you can use the methods of pdf_writer library.
let (chunk, qrcode_ref) = svg2pdf::to_chunk(&tree, ConversionOptions::default());
// probably then: add the text with chunk.stream() or chunk.indirect()
//let pdf_buf = svg2pdf::to_pdf(&tree, ConversionOptions::default(), PageOptions::default());
// Define some indirect reference ids we'll use.
@ -1632,17 +1621,52 @@ pub async fn wallet_recovery_pdf(
let font_name = Name(b"F1");
let qrcode_name = Name(b"Im1");
let chunks = recovery_str
.as_bytes()
.chunks(92)
.map(|buf| buf)
.collect::<Vec<&[u8]>>();
let mut content = Content::new();
content.begin_text();
content.set_font(font_name, 14.0);
content.next_line(108.0, 734.0);
content.show(Str(b"Hello World from Rust!"));
content.end_text();
content.begin_text();
content.set_font(font_name, 14.0);
content.next_line(15.0, 810.0);
content.show(Str(recovery_str.as_bytes()));
content.end_text();
for (line, string) in chunks.iter().enumerate() {
content.begin_text();
content.set_font(font_name, 10.0);
content.next_line(20.0, 810.0 - line as f32 * 15.0);
content.show(Str(*string));
content.end_text();
}
let pazzle: Vec<String> = display_pazzle(&recovery.pazzle)
.iter()
.map(|p| p.1.to_string())
.collect();
let mnemonic = display_mnemonic(&recovery.mnemonic);
let credentials = format!(
"PIN:{}{}{}{} PAZZLE:{} MNEMONIC:{}",
recovery.pin[0],
recovery.pin[1],
recovery.pin[2],
recovery.pin[3],
pazzle.join(" "),
mnemonic.join(" ")
);
let chunks = credentials
.as_bytes()
.chunks(92)
.map(|buf| buf)
.collect::<Vec<&[u8]>>();
for (line, string) in chunks.iter().enumerate() {
content.begin_text();
content.set_font(font_name, 10.0);
content.next_line(20.0, 630.0 - line as f32 * 15.0);
content.show(Str(*string));
content.end_text();
}
content.save_state();
content.transform([595.0, 0.0, 0.0, 595.0, 0.0, 0.0]);
content.x_object(qrcode_name);
@ -1704,7 +1728,7 @@ lazy_static! {
/// with the help of the function [wallet_open_with_pazzle_words]
/// followed by [wallet_import]
pub async fn wallet_import_from_code(code: String) -> Result<Wallet, NgError> {
let qr = NgQRCode::from_code(code)?;
let qr = NgQRCode::from_code(code.trim().to_string())?;
match qr {
NgQRCode::WalletTransferV0(NgQRCodeWalletTransferV0 {
broker,
@ -2924,7 +2948,6 @@ mod test {
let mnemonic = encode_mnemonic(&mnemonic_words).expect("encode_mnemonic");
let wallet_recovery = wallet_to_wallet_recovery(&wallet, pazzle, mnemonic, pin);
let pdf_buffer = wallet_recovery_pdf(wallet_recovery, 600)
.await
.expect("wallet_recovery_pdf");

@ -572,7 +572,9 @@ impl AppBuilder {
pub fn run(self) {
let setup = self.setup;
let builder = tauri::Builder::default()
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.setup(move |app| {
if let Some(setup) = setup {
(setup)(app)?;
@ -592,7 +594,7 @@ impl AppBuilder {
#[cfg(mobile)]
{
let builder = builder.plugin(tauri_plugin_barcode_scanner::init());
builder = builder.plugin(tauri_plugin_barcode_scanner::init());
}
builder

@ -72,6 +72,7 @@ const handler = {
}
} else {
let tauri = await import("@tauri-apps/api/tauri");
try {
if (path[0] === "client_info") {
let from_rust = await tauri.invoke("client_info_rust",{});
@ -105,6 +106,11 @@ const handler = {
};
//console.log(info,res);
return res;
} else if (path[0] === "get_device_name") {
let tauri_platform = import.meta.env.TAURI_PLATFORM;
if (tauri_platform == 'android') return "Android Phone";
else if (tauri_platform == 'ios') return "iPhone";
else return await tauri.invoke(path[0],{});
} else if (path[0] === "locales") {
let from_rust = await tauri.invoke("locales",{});
let from_js = window.navigator.languages;
@ -166,6 +172,15 @@ const handler = {
}
return res || {};
} else if (path[0] === "wallet_import_from_code") {
let arg = {};
args.map((el,ix) => arg[mapping[path[0]][ix]]=el);
let res = await tauri.invoke(path[0],arg);
if (res) {
res.V0.content.security_img = Uint8Array.from(res.V0.content.security_img);
}
return res || {};
} else if (path[0] === "upload_chunk") {
let session_id = args[0];
let upload_id = args[1];
@ -202,8 +217,17 @@ const handler = {
} else {
let arg = {};
args.map((el,ix) => arg[mapping[path[0]][ix]]=el)
return tauri.invoke(path[0],arg)
return await tauri.invoke(path[0],arg)
}
}catch (e) {
let error;
try {
error = JSON.parse(e);
} catch (f) {
error = e;
}
throw error;
}
}
},
};

@ -16,7 +16,7 @@
// "media/image", "media/reel", "media/album", "media/video", "media/audio", "media/song", "media/subtitle", "media/overlay",
// "social/channel", "social/stream", "social/contact", "social/event", "social/calendar", "social/scheduler", "social/reaction"
// "prod/task", "prod/project", "prod/issue", "prod/form", "prod/filling", "prod/cad", "prod/slides", "prod/question", "prod/answer", "prod/poll", "prod/vote"
// "file", "file/iana/*", "file/gimp", "file/inkscape", "file/kdenlive", "file/blender", "file/openscad", "file/lyx", "file/scribus", "file/libreoffice",
// "file", "file/iana/*", "file/gimp", "file/inkscape", "file/kdenlive", "file/blender", "file/openscad", "file/lyx", "file/scribus", "file/libreoffice", "file/audacity"
// application/vnd.api+json

@ -89,7 +89,7 @@
unlockWith = "pazzle";
scrollToTop();
}
function start_with_mnemonic() {
async function start_with_mnemonic() {
loaded = false;
step = "mnemonic";
unlockWith = "mnemonic";
@ -169,7 +169,7 @@
pazzle.push((emoji.cat << 4) + emoji.index);
}
const mnemonic_words = mnemonic.split(" ");
const mnemonic_words = mnemonic.split(" ").filter((t) => t !== "");
// open the wallet
try {

@ -39,7 +39,7 @@
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 mb-2"
>
<svg
class="w-8 h-8 -ml-1"
class="w-8 h-8 -ml-1 mr-2"
fill="none"
stroke="currentColor"
stroke-width="1.5"

@ -40,12 +40,14 @@
</script>
<div class="relative">
<!-- svelte-ignore a11y-autofocus -->
<input
bind:this={input}
{value}
{placeholder}
{id}
{type}
autofocus
on:input={handleInput}
class={`${className} pr-12 text-md block`}
autocomplete={auto_complete}

@ -28,18 +28,21 @@
"remove_wallet_modal.title": "Remove wallet?",
"remove_wallet_modal.confirm": "Are you sure you want to remove this wallet from your device?",
"create_text_code": "Generate TextCode to export",
"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.",
"scan_qr.scan_btn": "Scan QR Code ",
"scan_qr.scanner.title": "Scan your QR Code",
"scan_qr.title": "Export by scanning a QR-Code",
"scan_qr.no_camera": "If to the contrary, the other device does not have a camera, ",
"scan_qr.other_has_camera": "If the other device where you want to import the Wallet, has a camera, then you can just click on the Back button and select <span class=\"path\">Generate QR to export</span>",
"scan_qr.notes": "You will now scan the QR-Code that appears on the screen of the other device (the one you want to transfer your wallet to). You will be asked to allow the app to use your camera.",
"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 transferred to the new device.",
"scan_qr.scan_successful": "Success!<br />Your wallet has been transferred to the other 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.",
"gen_qr.img_title": "Your Export QR Code to Scan",
"gen_qr.img_alt": "Your Export QR Code to Scan",
"gen_qr.gen_button": "Show QR Code",
"gen_qr.notes": "In order to transfer your wallet to another device, you will now display a QR-Code here on this device, and you will then scan it with the other device where you want to transfer your wallet to.",
"gen_qr.no_camera": "If the device where you want to import the Wallet, does not have a camera, then you will have to choose another method.",
"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",
"gen_text_code.title": "Export with TextCode",
"gen_text_code.gen_btn": "Generate TextCode",
"gen_text_code.label": "Your TextCode:"
@ -242,7 +245,7 @@
"wallet_login": {
"select_wallet": "Select a wallet to login with",
"from_import.title": "Your wallet has been transferred",
"from_import.description": "Your wallet has been received:",
"from_import.description": "Your wallet has been received from the other device!",
"from_import.instruction": "To finish the import, please log in.",
"with_another_wallet": "Log in with another wallet",
"import_wallet": "Import your wallet",
@ -257,18 +260,21 @@
"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.modal.title": "Scan Wallet QR-Code",
"gen.button": "Generate",
"gen.description": "To import your wallet from another device, you have to generate 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.button": "Generate QR-Code",
"gen.description": "To import your wallet from another device, you have to generate a QR-Code here on this device, and then scan it with your other device (the one where your wallet is located for now).<br/>If your other device does not have a camera, then you have to use another method for importing your wallet here.",
"offline_advice": "If you do not have internet on this device, you can use the \"Import a Wallet file\" method instead.",
"gen.letsgo": "Ready? On your other device, you first have to be logged-in (wallet is opened) and then you go to<br /><span class=\"path\">User Panel > Wallet > Scan QR to export</span>.<br />Then on this present device, click below on the<br/><span class=\"path\">Generate QR-Code</span> button.",
"gen.generated": "Scan this QR-Code from the other device.<br/>You have 5 minutes to do so.",
"success_btn": "Continue to Login"
},
"wallet_login_textcode": {
"title": "Import Wallet from TextCode",
"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"
"description": "To generate a TextCode, open a logged in device and go to<br /><span class=\"path\">User Panel > Wallet > Generate TextCode to export</span>.",
"login_btn": "Import with TextCode",
"enter_here": "Enter your TextCode here below and click on Import button"
},
"scan_qr": {
"scanning": "Scan the QR-Code"
"scanning": "Scanning the QR-Code"
}
},
"buttons": {
@ -291,7 +297,7 @@
"AlreadyExists": "The user is already registered with the selected broker.<br />Try logging in instead.",
"InvalidSignature": "The signature is invalid.",
"IncompleteSignature": "The signature is incomplete.",
"SerializationError": "The object could not be serialized.",
"SerializationError": "The data could not be serialized.",
"EncryptionError": "Your wallet could not be opened. You probably did a mistake.",
"DecryptionError": "Error with decryption.",
"InvalidValue": "The value is invalid.",
@ -352,7 +358,8 @@
"ExportWalletTimeOut": "Export of wallet has expired.",
"ConnectionError": "Could not connect to the server.",
"IncompatibleQrCode": "You scanned a NextGraph QR-Code that is of the wrong type.",
"NotARendezVous": "You scanned an invalid QR-Code."
"NotARendezVous": "You scanned an invalid QR-Code.",
"camera_unavailable": "Camera is unavailable."
},
"connectivity": {
"stopped": "Stopped",
@ -372,11 +379,14 @@
"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 the \"Import with QRCode\" option instead, 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.",
"offline_warning": "You cannot transfer your wallet when offline.<br />Please make sure you are connected to the internet 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 the \"Import with QR-Code\" option instead, as it is safer and simpler. 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 transfer, your wallet will be temporarily and securely stored on our servers for up to 5 minutes, using two levels of encryption.<br/> We at NextGraph will never be able to read your wallet, your PIN, your pazzle nor your mnemonic.",
"importing": "Importing wallet",
"error": "An error occurred while synchronizing your wallet:<br />{error}"
"expiry": "The TextCode will be valid for 5 minutes.",
"error": "An error occurred while synchronizing your wallet:<br />{error}",
"no_camera": "Unfortunately, your device does not have a camera.<br /> You cannot scan any QR-Code.<br /> In order to export your wallet to another device, you will have to use another method.",
"no_camera_alternatives": "You have 2 other options: \"Import a Wallet file\" (transfer it using a USB key by example, useful if you are offline) or \"Import a TextCode\" (which is a text you will have to transfer with another messaging app)."
},
"emojis": {
"category": {

@ -18,14 +18,14 @@
import { onMount, onDestroy } from "svelte";
import { t } from "svelte-i18n";
import { scanned_qr_code } from "../store";
import { ArrowLeft } from "svelte-heros-v2";
import { ArrowLeft, ExclamationTriangle } 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;
let error = false;
function on_qr_scanned(content) {
scanned_qr_code.set(content);
@ -49,35 +49,72 @@
on_qr_scanned(result.content);
} else {
// Load Web Scanner
const { Html5QrcodeScanner } = await import("html5-qrcode");
const { Html5QrcodeScanner, Html5Qrcode } = await import("html5-qrcode");
// Init scanner object
webScanner = new Html5QrcodeScanner(
"scanner-div",
{ fps: 10, qrbox: { width: 300, height: 300 }, formatsToSupport: [0] },
false
);
// webScanner = new Html5QrcodeScanner(
// "scanner-div",
// { fps: 10, qrbox: { width: 300, height: 300 }, formatsToSupport: [0] },
// false
// );
try {
webScanner = new Html5Qrcode ("scanner-div");
await webScanner.start({ facingMode: { exact: "environment"} }, { fps: 10, qrbox: { width: 300, height: 300 }, formatsToSupport: [0] }, (decoded_text, decoded_result) => {
//console.log(decoded_result);
// Handle scan result
on_qr_scanned(decoded_text);
});
} catch (e) {
try {
webScanner = new Html5Qrcode ("scanner-div");
await webScanner.start({ facingMode: "environment" }, { fps: 10, qrbox: { width: 300, height: 300 }, formatsToSupport: [0] }, (decoded_text, decoded_result) => {
//console.log(decoded_result);
// Handle scan result
on_qr_scanned(decoded_text);
});
} catch (e) {
webScanner = null;
error = true;
}
}
// Add scanner to Screen.
webScanner.render((decoded_text, decoded_result) => {
// Handle scan result
on_qr_scanned(decoded_text);
}, undefined);
// // Add scanner to Screen.
// webScanner.render((decoded_text, decoded_result) => {
// //console.log(decoded_result);
// // Handle scan result
// on_qr_scanned(decoded_text);
// }, (error) => {
// //console.error(error);
// });
// 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);
// setTimeout(() => {
// // Auto-start by clicking button
// document
// .getElementById("html5-qrcode-button-camera-permission")
// ?.click();
// }, 100);
// setTimeout(check_ready_and_start, 1000);
}
});
// const check_ready_and_start = () => {
// // Auto-start by clicking button
// let start_btn = document
// .getElementById("html5-qrcode-button-camera-start");
// if (start_btn) {
// start_btn.click();
// } else {
// setTimeout(check_ready_and_start, 1000);
// }
// };
onDestroy(async () => {
if (mobile) {
if (nativeScanner) await nativeScanner.cancel();
} else {
if (webScanner) webScanner.clear();
if (webScanner) webScanner.stop();
}
});
</script>
@ -86,10 +123,18 @@
<div>
<h2 class="text-xl mb-6">{$t("pages.scan_qr.scanning")}</h2>
</div>
{#if !error}<Spinner />{/if}
<!-- Web Scanner -->
<div id="scanner-div"><Spinner /></div>
<div id="scanner-div"></div>
{#if error}
<div class="mx-6 max-w-6xl lg:px-8 mx-auto px-4 text-red-800">
<ExclamationTriangle class="animate-bounce mt-10 h-16 w-16 mx-auto" />
{@html $t("errors.camera_unavailable")}
</div>
{/if}
<div class="mx-auto max-w-xs">
<button
on:click={() => window.history.go(-1)}

@ -1539,7 +1539,27 @@
{#if !ready}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-primary-700">
{$t("pages.wallet_create.creating")}
<Spinner className="mt-10 h-6 w-6 mx-auto" />
<svg
class="animate-spin mt-10 h-6 w-6 mx-auto"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
{:else}
<div class="text-left mx-4">

@ -41,6 +41,7 @@
display_error,
online,
scanned_qr_code,
check_has_camera,
} from "../store";
import { default as ng } from "../api";
@ -63,6 +64,8 @@
let scanner_state: "before_start" | "scanned" | "success" = "before_start";
let has_camera = false;
async function scrollToTop() {
await tick();
container.scrollIntoView();
@ -78,6 +81,7 @@
scanned_qr_code.set("");
}
await scrollToTop();
has_camera = await check_has_camera();
});
function open_scan_menu() {
@ -98,7 +102,7 @@
generation_state = "loading";
generated_qr = await ng.wallet_export_get_qrcode(
$active_session.session_id,
Math.ceil(container.clientWidth * 0.9)
container.clientWidth
);
generation_state = "generated";
}
@ -184,13 +188,13 @@
</script>
<CenteredLayout>
<div class="container3" bind:this={container}>
<div class="row mb-10">
<Sidebar {nonActiveClass}>
<SidebarWrapper
divClass="bg-gray-60 overflow-y-auto py-4 px-3 rounded dark:bg-gray-800"
>
{#if sub_menu === null}
<div class="container3 mb-20" bind:this={container}>
{#if sub_menu === null}
<Sidebar {nonActiveClass}>
<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>
@ -211,7 +215,7 @@
<span class="ml-3">{$t("buttons.back")}</span>
</li>
<!-- Scan QR Code to log in with another device -->
<!-- Scan QR Code to export wallet to another device -->
<li
tabindex="0"
role="menuitem"
@ -228,7 +232,7 @@
<span class="ml-3">{$t("pages.wallet_info.scan_qr")}</span>
</li>
<!-- Generate QR Code to log in with another device -->
<!-- Generate QR Code export wallet to another device -->
<li
tabindex="0"
role="menuitem"
@ -390,8 +394,14 @@
</div>
</Modal>
</SidebarGroup>
{:else if sub_menu === "scan_qr"}
<SidebarGroup ulClass="space-y-2" role="menu">
</SidebarWrapper>
</Sidebar>
{:else if sub_menu === "scan_qr"}
<Sidebar {nonActiveClass}>
<SidebarWrapper
divClass="bg-gray-60 overflow-y-auto py-4 px-3 rounded dark:bg-gray-800"
>
<SidebarGroup ulClass="space-y-6" role="menu">
<li>
<h2 class="text-xl mb-6">
{$t("pages.wallet_info.scan_qr.title")}
@ -411,170 +421,220 @@
/>
<span class="ml-3">{$t("buttons.back")}</span>
</li>
{#if scanner_state === "before_start"}
<!-- NOTES ABOUT QR-->
{#if !has_camera}
<li class="text-left">
{@html $t("pages.wallet_info.scan_qr.notes")}
<br />
{@html $t("wallet_sync.server_transfer_notice")}
<Alert color="red">
{@html $t("wallet_sync.no_camera")}
</Alert>
<Alert color="blue" class="mt-4">
{@html $t("pages.wallet_info.scan_qr.other_has_camera")}
</Alert>
<Alert color="blue" class="mt-4">
{@html $t("pages.wallet_info.scan_qr.no_camera")}
{@html $t("wallet_sync.no_camera_alternatives")}
</Alert>
</li>
<!-- Warning if offline -->
{#if !$online}
{:else}
{#if scanner_state === "before_start"}
<!-- NOTES ABOUT QR-->
<li class="text-left">
<Alert color="red">
{@html $t("wallet_sync.offline_warning")}
</Alert>
{@html $t("pages.wallet_info.scan_qr.notes")}
<br /><br />
{@html $t("wallet_sync.server_transfer_notice")}
</li>
{/if}
<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"
>
{$t("pages.wallet_info.scan_qr.scan_btn")}
</Button>
{:else if scanner_state === "scanned"}
<li class="">
<Spinner class="mt-4 mb-2" />
<div>
{@html $t("pages.wallet_info.scan_qr.syncing")}...
<br />
<br />
{scanned_qr_code}
</div>
</li>
{:else if scanner_state === "success"}
<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>
<!-- Warning if offline -->
{#if !$online}
<li class="text-left">
<Alert color="red">
{@html $t("wallet_sync.offline_warning")}
</Alert>
</li>
{/if}
<li class="">
<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"
>
{$t("pages.wallet_info.scan_qr.scan_btn")}
</Button>
</li>
{:else if scanner_state === "scanned"}
<li class="">
<Spinner class="mt-4 mb-2" />
<div>
{@html $t("pages.wallet_info.scan_qr.syncing")}...
<br />
<br />
{scanned_qr_code}
</div>
</li>
{:else if scanner_state === "success"}
<li class="text-green-800 flex flex-col items-center">
<div class="mt-4">
<CheckBadge color="green" size="3em" />
</div>
<div class="mt-4 mb-4">
{@html $t("pages.wallet_info.scan_qr.scan_successful")}
</div>
<Button
on:click={to_main_menu}
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"
>
{$t("buttons.go_back")}
</Button>
</li>
{/if}
{/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}
>
<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 generated QR -->
<li class="text-left">
{@html $t("pages.wallet_info.gen_qr.notes")}
<br />
{@html $t("wallet_sync.server_transfer_notice")}
</li>
<!-- Warning if offline -->
{#if !$online}
<li class="text-left">
<Alert color="red">
{@html $t("wallet_sync.offline_warning")}
</Alert>
</li>
{/if}
{#if generation_state === "before_start"}
<Button
on:click={generate_qr_code}
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"
>
{$t("pages.wallet_info.gen_qr.gen_button")}
</Button>
{:else if generation_state === "loading"}
<Spinner class="mx-auto" size="6" />
{:else}
<!-- QR Code -->
<div class="w-full">
{@html generated_qr}
</SidebarWrapper>
</Sidebar>
<!-- Generate QR-Code screen -->
{:else if sub_menu === "generate_qr"}
{#if generation_state !== "generated"}
<div
class="flex flex-col justify-center max-w-md mb-10 bg-gray-60 overflow-y-auto py-4 dark:bg-gray-800"
>
<div class="mx-6">
<h2 class="text-xl mb-6">
{$t("pages.wallet_info.gen_qr.title")}
</h2>
</div>
{/if}
</SidebarGroup>
{:else if sub_menu === "text_code"}
<SidebarGroup ulClass="space-y-2" role="menu">
<li>
<h2 class="text-xl mb-6">
{$t("pages.wallet_info.gen_text_code.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}
>
<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>
<!-- Warning to prefer QR codes or wallet downloads -->
<div class="text-left my-4">
<Alert color="yellow">
{@html $t("wallet_sync.textcode.usage_warning")}
</Alert>
</div>
<!-- Go Back -->
<!-- Go Back -->
<!-- Notes about generated QR -->
<div class="mx-6 text-left">
{@html $t("pages.wallet_info.gen_qr.notes")}
<br /><br />
{@html $t("pages.wallet_info.gen_qr.no_camera")}
{@html $t("wallet_sync.no_camera_alternatives")}
<br /><br />
{@html $t("wallet_sync.server_transfer_notice")}
</div>
<!-- Warning if offline -->
{#if !$online}
<li class="text-left my-4">
<Alert color="red">
{@html $t("wallet_sync.offline_warning")}
</Alert>
</li>
{/if}
<!-- Warning if offline -->
{#if !$online}
<div class="mx-6 text-left">
<Alert color="red">
{@html $t("wallet_sync.offline_warning")}
</Alert>
</div>
{/if}
<div class="mt-4">
{#if generation_state === "before_start"}
<div class="mx-6">
<div class="mx-auto">
<div class="my-4 mx-1">
<Button
on:click={generate_text_code}
on:click={generate_qr_code}
disabled={!$online}
class="my-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"
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"
>
{$t("pages.wallet_info.gen_text_code.gen_btn")}
</Button>
{:else if generation_state == "loading"}
{$t("pages.wallet_info.gen_qr.gen_button")}
</Button></div></div></div>
{:else if generation_state === "loading"}
<Spinner class="mx-auto" size="6" />
{:else}
<!-- TextCode Code -->
<label>{$t("pages.wallet_info.gen_text_code.label")}</label>
<div>
<CopyToClipboard rows={8} value={generated_text_code} />
</div>
{/if}
</div>
</SidebarGroup>
<button
on:click={to_main_menu}
class="mt-4 mx-6 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>
{:else}
<div
class="flex flex-col justify-center max-w-md mb-20 bg-gray-60 overflow-y-auto py-4 dark:bg-gray-800"
>
<h2 class="text-xl mb-6">
{$t("pages.wallet_info.gen_qr.title")}
</h2>
<div class="text-center mb-2 mx-6">
{@html $t("pages.wallet_login_qr.gen.generated")}
</div>
<!-- Generated QR Code -->
<div class="my-4 mx-auto">
{@html generated_qr}
</div>
<button
on:click={to_main_menu}
class="mt-8 mx-6 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>
{/if}
{:else if sub_menu === "text_code"}
<div
class="flex flex-col justify-center max-w-md mx-6 mb-20 bg-gray-60 overflow-y-auto py-4 dark:bg-gray-800"
>
<div>
<h2 class="text-xl mb-6">
{$t("pages.wallet_info.gen_text_code.title")}
</h2>
</div>
<!-- Go Back -->
<button
on:click={to_main_menu}
class="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
>
<!-- Warning to prefer QR codes or wallet downloads -->
{#if generation_state === "before_start"}
<div class="text-left my-4">
<Alert color="yellow">
{@html $t("wallet_sync.textcode.usage_warning")}
</Alert>
</div>
{/if}
</SidebarWrapper>
</Sidebar>
<!-- Warning if offline -->
<div class="text-left my-4">
{#if !$online}
<Alert color="red">
{@html $t("wallet_sync.offline_warning")}
</Alert>
{:else}
{@html $t("wallet_sync.expiry")}
{/if}
</div>
<div class="mt-4">
{#if generation_state === "before_start"}
<Button
on:click={generate_text_code}
disabled={!$online}
class="my-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"
>
{$t("pages.wallet_info.gen_text_code.gen_btn")}
</Button>
{:else if generation_state == "loading"}
<Spinner class="mx-auto" size="6" />
{:else}
<!-- TextCode Code -->
<span>{$t("pages.wallet_info.gen_text_code.label")}</span>
<div>
<CopyToClipboard rows={8} value={generated_text_code} />
</div>
{/if}
</div>
</div>
{/if}
</div>
{#if error}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-red-800">
@ -587,7 +647,7 @@
</p>
</div>
{/if}
</div>
</CenteredLayout>
<style>

@ -101,10 +101,8 @@
if ($wallet_from_import) {
wallet = $wallet_from_import;
importing = true;
// TODO: Show component: "We got wallet from other device. Please log in to your wallet, to import the device."
}
// Sample textcode AABAOAAAAHNb4y7hdWADqFWDgER3J0xvD3K5D9pZ1wd7Bja4c9cWAOFNpmUIZOFRro0UIpZWr5Ah8U7PlRFe1GFZSKuIextFAA8A45zZUJmUPhfdBrcho1vYPfgda0BAgIT1qjzgEkBQAA"
});
function loggedin() {
@ -125,7 +123,7 @@
wallet_from_import.set(null);
});
async function gotError(event) {
importing = false;
//importing = false;
console.error(event.detail);
}
async function gotWallet(event) {
@ -191,6 +189,7 @@
} else {
wallet = $wallets[selected]?.wallet;
}
importing = false;
}
function handleWalletUpload(event) {
const files = event.target.files;
@ -279,7 +278,7 @@
<div>
<button
class="mt-4 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-white bg-primary-700 hover:bg-primary-700/90 focus:ring-4 focus:ring-primary-100/50 font-medium rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55 mb-2"
on:click={start_login_from_import}
>
{$t("buttons.login")}
@ -364,7 +363,7 @@
</button>
<a href="/wallet/login-qr" use:link>
<button
style="min-width: 250px;justify-content: left;"
style="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"
>
<QrCode class="w-8 h-8 mr-2 -ml-1" />
@ -373,7 +372,7 @@
</a>
<a href="/wallet/login-text-code" use:link>
<button
style="min-width: 250px;justify-content: left;"
style="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"
>
<svg
@ -434,7 +433,7 @@
cursor: pointer;
}
.wallet-box button {
min-width: 250px;
min-width: 262px;
}
.securitytxt {
z-index: 100;

@ -1,6 +1,6 @@
<script lang="ts">
import { t } from "svelte-i18n";
import { Alert, Modal, Spinner } from "flowbite-svelte";
import { Alert, Modal, Spinner, Button } from "flowbite-svelte";
import {
ArrowLeft,
Camera,
@ -10,21 +10,21 @@
import { onDestroy, 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 { wallet_from_import, scanned_qr_code, display_error, check_has_camera } from "../store";
import ng from "../api";
// <a href="/wallet/scanqr" use:link>
let top: HTMLElement;
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";
const set_online = () => { connected = true; };
const set_offline = () => { connected = false; };
let login_method: "scan" | "gen" | undefined = undefined;
let error;
let connected = true;
let scan_state: "before_start" | "importing" = "before_start";
@ -36,25 +36,7 @@
push("#/wallet/scanqr");
};
const check_has_camera = async () => {
if (!use_native_cam) {
// 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";
} else {
// 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;
}
};
async function on_qr_scanned(code) {
login_method = "scan";
@ -73,7 +55,7 @@
gen_state = "generating";
try {
const [qr_code_el, code] = await ng.wallet_import_rendezvous(
Math.ceil(top.clientWidth * 0.9)
top.clientWidth
);
rendezvous_code = code;
qr_code_html = qr_code_el;
@ -91,18 +73,23 @@
top.scrollIntoView();
}
onMount(() => {
onMount(async () => {
connected = window.navigator.onLine;
window.addEventListener("offline", set_offline);
window.addEventListener("online", set_online);
// 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();
login_method = await check_has_camera() ? "scan" : "gen";
}
scrollToTop();
});
onDestroy(() => {
window.removeEventListener("offline", set_offline);
window.removeEventListener("online", set_online);
if (rendezvous_code) {
// TODO: Destroy
}
@ -112,10 +99,10 @@
<CenteredLayout>
<div class="container3" bind:this={top}>
<div
class="flex flex-col justify-center max-w-md mx-6 mb-20 bg-gray-60 overflow-y-auto py-4 dark:bg-gray-800"
class="flex flex-col justify-center max-w-md mb-20 bg-gray-60 overflow-y-auto py-4 dark:bg-gray-800"
>
<!-- Title -->
<div>
<div class="mx-6">
<h2 class="text-xl mb-6">{$t("pages.wallet_login_qr.title")}</h2>
</div>
@ -124,15 +111,16 @@
<div><Spinner /></div>
{: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">
<div class="text-left mx-6">
<Alert color="red">
{@html $t("wallet_sync.offline_warning")}
</Alert>
<Alert color="blue" class="mt-4">
{@html $t("pages.wallet_login_qr.offline_advice")}
</Alert>
</div>
{:else if error}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-red-800">
<div class="mx-6 max-w-6xl lg:px-8 mx-auto px-4 text-red-800">
<ExclamationTriangle class="animate-bounce mt-10 h-16 w-16 mx-auto" />
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
@ -142,28 +130,33 @@
</p>
</div>
{:else if login_method === "scan"}
{#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 === "importing"}
<div class="mb-4 w-full">
{@html $t("wallet_sync.importing")}
</div>
<div class="w-full"><Spinner /></div>
{/if}
<div class="mx-6">
{#if scan_state === "before_start"}
<!-- Scan Mode -->
<!-- Notes about QR -->
<div class="text-left">
{@html $t("pages.wallet_login_qr.scan.description")}
<br />
{@html $t("wallet_sync.server_transfer_notice")}
</div>
{:else if scan_state === "importing"}
<div class="mb-4 w-full">
{@html $t("wallet_sync.importing")}
</div>
<div class="w-full"><Spinner /></div>
{/if}
</div>
{: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">
<div class="text-left mx-6">
{@html $t("pages.wallet_login_qr.gen.description")}
<br />
{@html $t("wallet_sync.no_camera_alternatives")}
<br /><br />
{@html $t("pages.wallet_login_qr.gen.letsgo")}
<br /><br />
{@html $t("wallet_sync.server_transfer_notice")}
</div>
{:else if gen_state === "generating"}
@ -172,54 +165,56 @@
</div>
{:else if gen_state === "generated"}
<!-- Notes about generated QR -->
<div class="text-left text-sm">
<div class="text-center mb-2 mx-6">
{@html $t("pages.wallet_login_qr.gen.generated")}
</div>
<!-- Generated QR Code -->
<div class="my-4 my-auto">
<div class="my-4 mx-auto">
{@html qr_code_html}
</div>
{/if}
{/if}
<div class="mx-auto">
<div class="my-4 mx-1">
{#if login_method === "scan" && scan_state === "before_start"}
<!-- Open Scanner Button-->
<div class="mx-6">
<div class="mx-auto">
<div class="my-4 mx-1">
{#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
disabled={!connected}
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>
{/if}
<!-- Go Back -->
<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
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("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"
/>{$t("buttons.back")}</button
>
<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>
{/if}
<!-- Go Back -->
<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>

@ -1,5 +1,6 @@
<script lang="ts">
import { t } from "svelte-i18n";
import { onMount } from "svelte";
import { Alert, Modal, Spinner } from "flowbite-svelte";
import {
ArrowLeft,
@ -32,6 +33,11 @@
error = e;
}
};
function scrollToTop() {
top.scrollIntoView();
}
onMount(() => scrollToTop());
</script>
<CenteredLayout>
@ -65,6 +71,8 @@
<br />
{@html $t("wallet_sync.server_transfer_notice")}
</div>
<p><br/>{@html $t("pages.wallet_login_textcode.enter_here")}</p>
<!-- TextCode Input -->
<textarea
@ -77,7 +85,6 @@
{#if error}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-red-800">
<ExclamationTriangle class="animate-bounce mt-10 h-16 w-16 mx-auto" />
<p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.error_occurred", {
values: { message: display_error(error) },
@ -92,7 +99,7 @@
<!-- 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"
class="mt-4 mb-8 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}
class:hidden={state === "importing" || error}
@ -107,7 +114,7 @@
<!-- 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"
class="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"

@ -44,9 +44,9 @@ init({
});
export const display_error = (error: string) => {
// Check, if error tranlsation does not exist
console.log(error);
// TODO: Check, if error tranlsation does not exist
const parts = error.split(":");
let res = get(format)("errors." + parts[0]);
if (parts[1]) {
res += " " + get(format)("errors." + parts[1]);
@ -177,6 +177,37 @@ export const connection_status: Writable<"disconnected" | "connected" | "connect
let next_reconnect: NodeJS.Timeout | null = null;
export const check_has_camera = async () => {
const tauri_platform: string | undefined = import.meta.env.TAURI_PLATFORM;
const use_native_cam =
tauri_platform === "ios" || tauri_platform === "android";
let has_camera: boolean | "checking" = "checking";
if (!use_native_cam) {
if (tauri_platform) {
has_camera = false;
}
else {
// 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;
}
}
} else {
// 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;
}
return has_camera;
};
const updateConnectionStatus = ($connections: Record<string, any>) => {
// Reset error state for PeerAlreadyConnected errors.
Object.entries($connections).forEach(([cnx, connection]) => {

@ -11,120 +11,122 @@
/** To format paths, like Settings > Wallet > Generate Wallet QR */
.path {
font-family: monospace;
background-color: rgba(73, 114, 165, 0.1);
font-family: monospace;
background-color: rgba(73, 114, 165, 0.1);
}
/* TODO: hide qr scanner and info button */
/*
#scanner-div {
border: none !important;
}
#scanner-div * {
display: none;
border: none !important;
}
/* #scanner-div * {
display: none;
}
#scanner-div__scan_region {
display: block;
display: block;
}
#scanner-div__scan_region * {
display: block;
display: block;
}
#scanner-div__dashboard_section_csr {
display: none !important;
#html5-qrcode-button-camera-permission {
display: none !important;
}
/* #scanner-div__dashboard_section_csr {
display: none !important;
}
#html5-qrcode-anchor-scan-type-change {
display: none !important;
display: none !important;
}
[alt="Camera based scan"] {
display: none !important;
display: none !important;
}
[alt="Info icon"] {
display: none !important;
}
display: none !important;
}*/
.logo {
padding: 1.5em;
will-change: filter;
transition: 0.75s;
padding-bottom: 1em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
padding-bottom: 1em;
}
@keyframes pulse-logo-color {
0%,
100% {
fill: rgb(73, 114, 165);
stroke: rgb(73, 114, 165);
}
50% {
/* Mid-transition color */
stroke: #bbb;
fill: #bbb;
}
0%,
100% {
fill: rgb(73, 114, 165);
stroke: rgb(73, 114, 165);
}
50% {
/* Mid-transition color */
stroke: #bbb;
fill: #bbb;
}
}
.logo-pulse path {
animation: pulse-logo-color 2s infinite;
animation-timing-function: cubic-bezier(0.65, 0.01, 0.59, 0.83);
animation: pulse-logo-color 2s infinite;
animation-timing-function: cubic-bezier(0.65, 0.01, 0.59, 0.83);
}
.logo-gray path {
fill: #bbb;
stroke: #bbb;
fill: #bbb;
stroke: #bbb;
}
.logo-blue path {
fill: rgb(73, 114, 165);
stroke: rgb(73, 114, 165);
fill: rgb(73, 114, 165);
stroke: rgb(73, 114, 165);
}
.container3 {
margin: 0;
min-width: 280px;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
margin: 0;
min-width: 280px;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
div[role="alert"] div {
display: block;
display: block;
}
.choice-button {
min-width: 320px;
min-width: 320px;
}
.row {
display: flex;
justify-content: center;
display: flex;
justify-content: center;
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 305px;
min-height: 100vh;
margin: 0;
display: flex;
place-items: center;
min-width: 305px;
min-height: 100vh;
}
#app {
width: 100%;
width: 100%;
}
/* #app {
/*max-width: 1280px;
@ -138,45 +140,45 @@ body {
} */
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
color: #535bf2;
}
h1 {
text-align: center;
font-size: 3.2em;
line-height: 1.1;
text-align: center;
font-size: 3.2em;
line-height: 1.1;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
cursor: pointer;
}
button:hover {
border-color: #396cd8;
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
border-color: #396cd8;
background-color: #e8e8e8;
}
/* input,
@ -186,25 +188,25 @@ button {
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}

@ -1,2 +1 @@
pub mod wallet_get_export;
pub use wallet_get_export::*;

@ -1258,13 +1258,20 @@ pub fn display_pazzle(pazzle: &Vec<u8>) -> Vec<(&'static str, &'static str)> {
for emoji in pazzle {
let cat = (emoji & 240) >> 4;
let idx = emoji & 15;
res.push((
EMOJI_CAT[cat as usize],
EMOJIS.get(&EMOJI_CAT[cat as usize]).unwrap()[idx as usize].code,
));
let cat_str = EMOJI_CAT[cat as usize];
res.push((cat_str, EMOJIS.get(cat_str).unwrap()[idx as usize].code));
}
res
}
pub fn display_pazzle_one(pazzle: &Vec<u8>) -> Vec<String> {
let res: Vec<String> = display_pazzle(pazzle)
.into_iter()
.map(|(cat, emoji)| String::from(format!("{cat}:{emoji}")))
.collect();
res
}
//use ng_repo::log::*;
/// taking a list of pazzle words, returns a list of u8 codes

@ -408,25 +408,6 @@ pub fn display_mnemonic(mnemonic: &[u16; 12]) -> Vec<String> {
res
}
use crate::emojis::{EMOJIS, EMOJI_CAT};
pub fn display_pazzle(pazzle: &Vec<u8>) -> Vec<String> {
let res: Vec<String> = pazzle
.into_iter()
.map(|i| {
let cat = i >> 4;
let idx = i & 15;
let cat_str = EMOJI_CAT[cat as usize];
String::from(format!(
"{}:{}",
cat_str,
EMOJIS.get(cat_str).unwrap()[idx as usize].code
))
})
.collect();
res
}
pub fn gen_shuffle_for_pazzle_opening(pazzle_length: u8) -> ShuffledPazzle {
let mut rng = rand::thread_rng();
let mut category_indices: Vec<u8> = (0..pazzle_length).collect();
@ -870,7 +851,7 @@ mod test {
let _ = file.write_all(&ser_wallet);
log_debug!("wallet id: {}", res.wallet.id());
log_debug!("pazzle {:?}", display_pazzle(&res.pazzle));
log_debug!("pazzle {:?}", display_pazzle_one(&res.pazzle));
log_debug!("mnemonic {:?}", display_mnemonic(&res.mnemonic));
log_debug!("pin {:?}", pin);

Loading…
Cancel
Save