Import wallet with QR- and TextCode #32

Closed
laurin wants to merge 13 commits from feat/ng-app/sync-wallets into master
  1. 73
      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. 87
      ng-app/src/routes/ScanQR.svelte
  10. 22
      ng-app/src/routes/WalletCreate.svelte
  11. 176
      ng-app/src/routes/WalletInfo.svelte
  12. 13
      ng-app/src/routes/WalletLogin.svelte
  13. 81
      ng-app/src/routes/WalletLoginQr.svelte
  14. 13
      ng-app/src/routes/WalletLoginTextCode.svelte
  15. 35
      ng-app/src/store.ts
  16. 12
      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_verifier::verifier::Verifier;
use ng_wallet::bip39::encode_mnemonic; use ng_wallet::bip39::encode_mnemonic;
use ng_wallet::emojis::encode_pazzle; use ng_wallet::emojis::{display_pazzle, encode_pazzle};
use ng_wallet::{create_wallet_first_step_v0, create_wallet_second_step_v0, types::*}; use ng_wallet::{
create_wallet_first_step_v0, create_wallet_second_step_v0, display_mnemonic, types::*,
};
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
use ng_client_ws::remote_ws::ConnectionWebSocket; 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. /// Generates the Recovery PDF containing the Wallet, PIN, Pazzle and Mnemonic.
pub async fn wallet_recovery_pdf( pub async fn wallet_recovery_pdf(
content: NgQRCodeWalletRecoveryV0, recovery: NgQRCodeWalletRecoveryV0,
size: u32, size: u32,
) -> Result<Vec<u8>, NgError> { ) -> Result<Vec<u8>, NgError> {
let ser = serde_bare::to_vec(&content)?; let ser = serde_bare::to_vec(&recovery)?;
if ser.len() > 2_953 { if ser.len() > 2_953 {
return Err(NgError::InvalidPayload); return Err(NgError::InvalidPayload);
} }
@ -1607,20 +1609,7 @@ pub async fn wallet_recovery_pdf(
let tree = svg2pdf::usvg::Tree::from_str(&wallet_svg, &options) let tree = svg2pdf::usvg::Tree::from_str(&wallet_svg, &options)
.map_err(|e| NgError::WalletError(e.to_string()))?; .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()); 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()); //let pdf_buf = svg2pdf::to_pdf(&tree, ConversionOptions::default(), PageOptions::default());
// Define some indirect reference ids we'll use. // 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 font_name = Name(b"F1");
let qrcode_name = Name(b"Im1"); 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(); let mut content = Content::new();
for (line, string) in chunks.iter().enumerate() {
content.begin_text(); content.begin_text();
content.set_font(font_name, 14.0); content.set_font(font_name, 10.0);
content.next_line(108.0, 734.0); content.next_line(20.0, 810.0 - line as f32 * 15.0);
content.show(Str(b"Hello World from Rust!")); content.show(Str(*string));
content.end_text(); 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.begin_text();
content.set_font(font_name, 14.0); content.set_font(font_name, 10.0);
content.next_line(15.0, 810.0); content.next_line(20.0, 630.0 - line as f32 * 15.0);
content.show(Str(recovery_str.as_bytes())); content.show(Str(*string));
content.end_text(); content.end_text();
}
content.save_state(); content.save_state();
content.transform([595.0, 0.0, 0.0, 595.0, 0.0, 0.0]); content.transform([595.0, 0.0, 0.0, 595.0, 0.0, 0.0]);
content.x_object(qrcode_name); content.x_object(qrcode_name);
@ -1704,7 +1728,7 @@ lazy_static! {
/// with the help of the function [wallet_open_with_pazzle_words] /// with the help of the function [wallet_open_with_pazzle_words]
/// followed by [wallet_import] /// followed by [wallet_import]
pub async fn wallet_import_from_code(code: String) -> Result<Wallet, NgError> { 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 { match qr {
NgQRCode::WalletTransferV0(NgQRCodeWalletTransferV0 { NgQRCode::WalletTransferV0(NgQRCodeWalletTransferV0 {
broker, broker,
@ -2924,7 +2948,6 @@ mod test {
let mnemonic = encode_mnemonic(&mnemonic_words).expect("encode_mnemonic"); let mnemonic = encode_mnemonic(&mnemonic_words).expect("encode_mnemonic");
let wallet_recovery = wallet_to_wallet_recovery(&wallet, pazzle, mnemonic, pin); let wallet_recovery = wallet_to_wallet_recovery(&wallet, pazzle, mnemonic, pin);
let pdf_buffer = wallet_recovery_pdf(wallet_recovery, 600) let pdf_buffer = wallet_recovery_pdf(wallet_recovery, 600)
.await .await
.expect("wallet_recovery_pdf"); .expect("wallet_recovery_pdf");

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

@ -72,6 +72,7 @@ const handler = {
} }
} else { } else {
let tauri = await import("@tauri-apps/api/tauri"); let tauri = await import("@tauri-apps/api/tauri");
try {
if (path[0] === "client_info") { if (path[0] === "client_info") {
let from_rust = await tauri.invoke("client_info_rust",{}); let from_rust = await tauri.invoke("client_info_rust",{});
@ -105,6 +106,11 @@ const handler = {
}; };
//console.log(info,res); //console.log(info,res);
return 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") { } else if (path[0] === "locales") {
let from_rust = await tauri.invoke("locales",{}); let from_rust = await tauri.invoke("locales",{});
let from_js = window.navigator.languages; let from_js = window.navigator.languages;
@ -166,6 +172,15 @@ const handler = {
} }
return res || {}; 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") { } else if (path[0] === "upload_chunk") {
let session_id = args[0]; let session_id = args[0];
let upload_id = args[1]; let upload_id = args[1];
@ -202,7 +217,16 @@ const handler = {
} else { } else {
let arg = {}; let arg = {};
args.map((el,ix) => arg[mapping[path[0]][ix]]=el) 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", // "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" // "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" // "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 // application/vnd.api+json

@ -89,7 +89,7 @@
unlockWith = "pazzle"; unlockWith = "pazzle";
scrollToTop(); scrollToTop();
} }
function start_with_mnemonic() { async function start_with_mnemonic() {
loaded = false; loaded = false;
step = "mnemonic"; step = "mnemonic";
unlockWith = "mnemonic"; unlockWith = "mnemonic";
@ -169,7 +169,7 @@
pazzle.push((emoji.cat << 4) + emoji.index); pazzle.push((emoji.cat << 4) + emoji.index);
} }
const mnemonic_words = mnemonic.split(" "); const mnemonic_words = mnemonic.split(" ").filter((t) => t !== "");
// open the wallet // open the wallet
try { 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" 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 <svg
class="w-8 h-8 -ml-1" class="w-8 h-8 -ml-1 mr-2"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="1.5" stroke-width="1.5"

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

@ -28,18 +28,21 @@
"remove_wallet_modal.title": "Remove wallet?", "remove_wallet_modal.title": "Remove wallet?",
"remove_wallet_modal.confirm": "Are you sure you want to remove this wallet from your device?", "remove_wallet_modal.confirm": "Are you sure you want to remove this wallet from your device?",
"create_text_code": "Generate TextCode to export", "create_text_code": "Generate TextCode to export",
"scan_qr.title": "Export by scanning QR-Code", "scan_qr.title": "Export by scanning a QR-Code",
"scan_qr.notes": "Scan the QR-Code on the device that you want to transfer your wallet to.", "scan_qr.no_camera": "If to the contrary, the other device does not have a camera, ",
"scan_qr.scan_btn": "Scan QR Code ", "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.scanner.title": "Scan your QR Code", "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.scanner.loading": "Loading scanner",
"scan_qr.syncing": "Synchronizing wallet", "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.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.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.img_title": "Your Export QR Code to Scan", "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_alt": "Your Export QR Code to Scan", "gen_qr.img_title": "Your Export QR-Code to Scan",
"gen_qr.gen_button": "Show QR Code", "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.title": "Export with TextCode",
"gen_text_code.gen_btn": "Generate TextCode", "gen_text_code.gen_btn": "Generate TextCode",
"gen_text_code.label": "Your TextCode:" "gen_text_code.label": "Your TextCode:"
@ -242,7 +245,7 @@
"wallet_login": { "wallet_login": {
"select_wallet": "Select a wallet to login with", "select_wallet": "Select a wallet to login with",
"from_import.title": "Your wallet has been transferred", "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.", "from_import.instruction": "To finish the import, please log in.",
"with_another_wallet": "Log in with another wallet", "with_another_wallet": "Log in with another wallet",
"import_wallet": "Import your wallet", "import_wallet": "Import your wallet",
@ -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.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.button": "Scan QR-Code",
"scan.modal.title": "Scan Wallet QR-Code", "scan.modal.title": "Scan Wallet QR-Code",
"gen.button": "Generate", "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. On the other device, go to<br /><span class=\"path\">User Panel > Wallet > Scan QR</span> to export.", "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.",
"gen.generated": "Scan this QR-Code from the the other device.", "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" "success_btn": "Continue to Login"
}, },
"wallet_login_textcode": { "wallet_login_textcode": {
"title": "Import Wallet from 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.", "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" "login_btn": "Import with TextCode",
"enter_here": "Enter your TextCode here below and click on Import button"
}, },
"scan_qr": { "scan_qr": {
"scanning": "Scan the QR-Code" "scanning": "Scanning the QR-Code"
} }
}, },
"buttons": { "buttons": {
@ -291,7 +297,7 @@
"AlreadyExists": "The user is already registered with the selected broker.<br />Try logging in instead.", "AlreadyExists": "The user is already registered with the selected broker.<br />Try logging in instead.",
"InvalidSignature": "The signature is invalid.", "InvalidSignature": "The signature is invalid.",
"IncompleteSignature": "The signature is incomplete.", "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.", "EncryptionError": "Your wallet could not be opened. You probably did a mistake.",
"DecryptionError": "Error with decryption.", "DecryptionError": "Error with decryption.",
"InvalidValue": "The value is invalid.", "InvalidValue": "The value is invalid.",
@ -352,7 +358,8 @@
"ExportWalletTimeOut": "Export of wallet has expired.", "ExportWalletTimeOut": "Export of wallet has expired.",
"ConnectionError": "Could not connect to the server.", "ConnectionError": "Could not connect to the server.",
"IncompatibleQrCode": "You scanned a NextGraph QR-Code that is of the wrong type.", "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": { "connectivity": {
"stopped": "Stopped", "stopped": "Stopped",
@ -372,11 +379,14 @@
"donate_nextgraph": "Donate to NextGraph" "donate_nextgraph": "Donate to NextGraph"
}, },
"wallet_sync": { "wallet_sync": {
"offline_warning": "You cannot transfer your wallet when offline.<br />Please make sure, you are connected first.", "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 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.", "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 import, your wallet will be temporarily and securely stored on our servers for up to 5 minutes, using two levels of encryption.", "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", "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": { "emojis": {
"category": { "category": {

@ -18,14 +18,14 @@
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import { t } from "svelte-i18n"; import { t } from "svelte-i18n";
import { scanned_qr_code } from "../store"; import { scanned_qr_code } from "../store";
import { ArrowLeft } from "svelte-heros-v2"; import { ArrowLeft, ExclamationTriangle } from "svelte-heros-v2";
import { Spinner } from "flowbite-svelte"; import { Spinner } from "flowbite-svelte";
let tauri_platform = import.meta.env.TAURI_PLATFORM; let tauri_platform = import.meta.env.TAURI_PLATFORM;
let mobile = tauri_platform == "android" || tauri_platform == "ios"; let mobile = tauri_platform == "android" || tauri_platform == "ios";
let webScanner; let webScanner;
let nativeScanner; let nativeScanner;
let error = false;
function on_qr_scanned(content) { function on_qr_scanned(content) {
scanned_qr_code.set(content); scanned_qr_code.set(content);
@ -49,35 +49,72 @@
on_qr_scanned(result.content); on_qr_scanned(result.content);
} else { } else {
// Load Web Scanner // Load Web Scanner
const { Html5QrcodeScanner } = await import("html5-qrcode"); const { Html5QrcodeScanner, Html5Qrcode } = await import("html5-qrcode");
// Init scanner object // Init scanner object
webScanner = new Html5QrcodeScanner( // webScanner = new Html5QrcodeScanner(
"scanner-div", // "scanner-div",
{ fps: 10, qrbox: { width: 300, height: 300 }, formatsToSupport: [0] }, // { fps: 10, qrbox: { width: 300, height: 300 }, formatsToSupport: [0] },
false // false
); // );
try {
// Add scanner to Screen. webScanner = new Html5Qrcode ("scanner-div");
webScanner.render((decoded_text, decoded_result) => { 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 // Handle scan result
on_qr_scanned(decoded_text); on_qr_scanned(decoded_text);
}, undefined); });
} catch (e) {
webScanner = null;
error = true;
}
}
// // 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...) // Auto-Request camera permissions (there's no native way, unfortunately...)
setTimeout(() => { // setTimeout(() => {
// Auto-start by clicking button // // Auto-start by clicking button
document // document
.getElementById("html5-qrcode-button-camera-permission") // .getElementById("html5-qrcode-button-camera-permission")
?.click(); // ?.click();
}, 100); // }, 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 () => { onDestroy(async () => {
if (mobile) { if (mobile) {
if (nativeScanner) await nativeScanner.cancel(); if (nativeScanner) await nativeScanner.cancel();
} else { } else {
if (webScanner) webScanner.clear(); if (webScanner) webScanner.stop();
} }
}); });
</script> </script>
@ -86,10 +123,18 @@
<div> <div>
<h2 class="text-xl mb-6">{$t("pages.scan_qr.scanning")}</h2> <h2 class="text-xl mb-6">{$t("pages.scan_qr.scanning")}</h2>
</div> </div>
{#if !error}<Spinner />{/if}
<!-- Web Scanner --> <!-- 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"> <div class="mx-auto max-w-xs">
<button <button
on:click={() => window.history.go(-1)} on:click={() => window.history.go(-1)}

@ -1539,7 +1539,27 @@
{#if !ready} {#if !ready}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-primary-700"> <div class=" max-w-6xl lg:px-8 mx-auto px-4 text-primary-700">
{$t("pages.wallet_create.creating")} {$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> </div>
{:else} {:else}
<div class="text-left mx-4"> <div class="text-left mx-4">

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

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

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { t } from "svelte-i18n"; import { t } from "svelte-i18n";
import { Alert, Modal, Spinner } from "flowbite-svelte"; import { Alert, Modal, Spinner, Button } from "flowbite-svelte";
import { import {
ArrowLeft, ArrowLeft,
Camera, Camera,
@ -10,21 +10,21 @@
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import { push } from "svelte-spa-router"; import { push } from "svelte-spa-router";
import CenteredLayout from "../lib/CenteredLayout.svelte"; 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"; import ng from "../api";
// <a href="/wallet/scanqr" use:link> // <a href="/wallet/scanqr" use:link>
let top: HTMLElement; let top: HTMLElement;
const tauri_platform: string | undefined = import.meta.env.TAURI_PLATFORM;
const use_native_cam = const set_online = () => { connected = true; };
tauri_platform === "ios" || tauri_platform === "android"; const set_offline = () => { connected = false; };
// TODO: Check connectivity to sync service.
let connected = true;
let has_camera: boolean | "checking" = "checking";
let login_method: "scan" | "gen" | undefined = undefined; let login_method: "scan" | "gen" | undefined = undefined;
let error; let error;
let connected = true;
let scan_state: "before_start" | "importing" = "before_start"; let scan_state: "before_start" | "importing" = "before_start";
@ -36,25 +36,7 @@
push("#/wallet/scanqr"); 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) { async function on_qr_scanned(code) {
login_method = "scan"; login_method = "scan";
@ -73,7 +55,7 @@
gen_state = "generating"; gen_state = "generating";
try { try {
const [qr_code_el, code] = await ng.wallet_import_rendezvous( const [qr_code_el, code] = await ng.wallet_import_rendezvous(
Math.ceil(top.clientWidth * 0.9) top.clientWidth
); );
rendezvous_code = code; rendezvous_code = code;
qr_code_html = qr_code_el; qr_code_html = qr_code_el;
@ -91,18 +73,23 @@
top.scrollIntoView(); top.scrollIntoView();
} }
onMount(() => { onMount(async () => {
connected = window.navigator.onLine;
window.addEventListener("offline", set_offline);
window.addEventListener("online", set_online);
// Handle return from QR scanner. // Handle return from QR scanner.
if ($scanned_qr_code) { if ($scanned_qr_code) {
on_qr_scanned($scanned_qr_code); on_qr_scanned($scanned_qr_code);
scanned_qr_code.set(""); scanned_qr_code.set("");
} else { } else {
// Or check, if a camera exists and offer scanner or QR generator, respectively. // 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(); scrollToTop();
}); });
onDestroy(() => { onDestroy(() => {
window.removeEventListener("offline", set_offline);
window.removeEventListener("online", set_online);
if (rendezvous_code) { if (rendezvous_code) {
// TODO: Destroy // TODO: Destroy
} }
@ -112,10 +99,10 @@
<CenteredLayout> <CenteredLayout>
<div class="container3" bind:this={top}> <div class="container3" bind:this={top}>
<div <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 --> <!-- Title -->
<div> <div class="mx-6">
<h2 class="text-xl mb-6">{$t("pages.wallet_login_qr.title")}</h2> <h2 class="text-xl mb-6">{$t("pages.wallet_login_qr.title")}</h2>
</div> </div>
@ -124,15 +111,16 @@
<div><Spinner /></div> <div><Spinner /></div>
{:else if !connected} {:else if !connected}
<!-- Warning, if offline --> <!-- Warning, if offline -->
<!-- TODO: just use $online from store to know if it is online --> <div class="text-left mx-6">
<!-- @Niko isnt online only true, when logged in and connected to a broker? -->
<div class="text-left">
<Alert color="red"> <Alert color="red">
{@html $t("wallet_sync.offline_warning")} {@html $t("wallet_sync.offline_warning")}
</Alert> </Alert>
<Alert color="blue" class="mt-4">
{@html $t("pages.wallet_login_qr.offline_advice")}
</Alert>
</div> </div>
{:else if error} {: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" /> <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"> <p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
@ -142,10 +130,11 @@
</p> </p>
</div> </div>
{:else if login_method === "scan"} {:else if login_method === "scan"}
<div class="mx-6">
{#if scan_state === "before_start"} {#if scan_state === "before_start"}
<!-- Scan Mode --> <!-- Scan Mode -->
<!-- Notes about QR --> <!-- Notes about QR -->
<div class="text-left text-sm"> <div class="text-left">
{@html $t("pages.wallet_login_qr.scan.description")} {@html $t("pages.wallet_login_qr.scan.description")}
<br /> <br />
{@html $t("wallet_sync.server_transfer_notice")} {@html $t("wallet_sync.server_transfer_notice")}
@ -157,13 +146,17 @@
<div class="w-full"><Spinner /></div> <div class="w-full"><Spinner /></div>
{/if} {/if}
</div>
{:else if login_method === "gen"} {:else if login_method === "gen"}
<!-- Generate QR Code to log in with another device --> <!-- Generate QR Code to log in with another device -->
{#if gen_state == "before_start"} {#if gen_state == "before_start"}
<!-- Notes about QR Generation --> <!-- Notes about QR Generation -->
<div class="text-left text-sm"> <div class="text-left mx-6">
{@html $t("pages.wallet_login_qr.gen.description")} {@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")} {@html $t("wallet_sync.server_transfer_notice")}
</div> </div>
{:else if gen_state === "generating"} {:else if gen_state === "generating"}
@ -172,17 +165,17 @@
</div> </div>
{:else if gen_state === "generated"} {:else if gen_state === "generated"}
<!-- Notes about generated QR --> <!-- 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")} {@html $t("pages.wallet_login_qr.gen.generated")}
</div> </div>
<!-- Generated QR Code --> <!-- Generated QR Code -->
<div class="my-4 my-auto"> <div class="my-4 mx-auto">
{@html qr_code_html} {@html qr_code_html}
</div> </div>
{/if} {/if}
{/if} {/if}
<div class="mx-6">
<div class="mx-auto"> <div class="mx-auto">
<div class="my-4 mx-1"> <div class="my-4 mx-1">
{#if login_method === "scan" && scan_state === "before_start"} {#if login_method === "scan" && scan_state === "before_start"}
@ -199,7 +192,8 @@
</button> </button>
{:else if login_method === "gen" && gen_state === "before_start"} {:else if login_method === "gen" && gen_state === "before_start"}
<!-- Generate QR Button --> <!-- Generate QR Button -->
<button <Button
disabled={!connected}
on:click={generate_qr} 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" 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"
> >
@ -208,7 +202,7 @@
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white" class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"
/> />
{$t("pages.wallet_login_qr.gen.button")} {$t("pages.wallet_login_qr.gen.button")}
</button> </Button>
{/if} {/if}
<!-- Go Back --> <!-- Go Back -->
@ -224,4 +218,5 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</CenteredLayout> </CenteredLayout>

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { t } from "svelte-i18n"; import { t } from "svelte-i18n";
import { onMount } from "svelte";
import { Alert, Modal, Spinner } from "flowbite-svelte"; import { Alert, Modal, Spinner } from "flowbite-svelte";
import { import {
ArrowLeft, ArrowLeft,
@ -32,6 +33,11 @@
error = e; error = e;
} }
}; };
function scrollToTop() {
top.scrollIntoView();
}
onMount(() => scrollToTop());
</script> </script>
<CenteredLayout> <CenteredLayout>
@ -66,6 +72,8 @@
{@html $t("wallet_sync.server_transfer_notice")} {@html $t("wallet_sync.server_transfer_notice")}
</div> </div>
<p><br/>{@html $t("pages.wallet_login_textcode.enter_here")}</p>
<!-- TextCode Input --> <!-- TextCode Input -->
<textarea <textarea
rows="6" rows="6"
@ -77,7 +85,6 @@
{#if error} {#if error}
<div class=" max-w-6xl lg:px-8 mx-auto px-4 text-red-800"> <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" /> <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"> <p class="max-w-xl md:mx-auto lg:max-w-2xl mb-5">
{@html $t("errors.error_occurred", { {@html $t("errors.error_occurred", {
values: { message: display_error(error) }, values: { message: display_error(error) },
@ -92,7 +99,7 @@
<!-- Submit Button--> <!-- Submit Button-->
<div class="my-4 mx-1"> <div class="my-4 mx-1">
<button <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} on:click={textcode_submit}
disabled={!connected || !textcode} disabled={!connected || !textcode}
class:hidden={state === "importing" || error} class:hidden={state === "importing" || error}
@ -107,7 +114,7 @@
<!-- Back Button --> <!-- Back Button -->
<button <button
on:click={() => window.history.go(-1)} on:click={() => window.history.go(-1)}
class="mt-8 w-full text-gray-500 dark:text-gray-400 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55" class="w-full text-gray-500 dark:text-gray-400 focus:ring-4 focus:ring-primary-100/50 rounded-lg text-lg px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-primary-700/55"
><ArrowLeft ><ArrowLeft
tabindex="-1" tabindex="-1"
class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white" class="w-8 h-8 mr-2 -ml-1 transition duration-75 focus:outline-none group-hover:text-gray-900 dark:group-hover:text-white"

@ -44,9 +44,9 @@ init({
}); });
export const display_error = (error: string) => { 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(":"); const parts = error.split(":");
let res = get(format)("errors." + parts[0]); let res = get(format)("errors." + parts[0]);
if (parts[1]) { if (parts[1]) {
res += " " + get(format)("errors." + parts[1]); res += " " + get(format)("errors." + parts[1]);
@ -177,6 +177,37 @@ export const connection_status: Writable<"disconnected" | "connected" | "connect
let next_reconnect: NodeJS.Timeout | null = null; 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>) => { const updateConnectionStatus = ($connections: Record<string, any>) => {
// Reset error state for PeerAlreadyConnected errors. // Reset error state for PeerAlreadyConnected errors.
Object.entries($connections).forEach(([cnx, connection]) => { Object.entries($connections).forEach(([cnx, connection]) => {

@ -15,11 +15,11 @@
background-color: rgba(73, 114, 165, 0.1); background-color: rgba(73, 114, 165, 0.1);
} }
/* TODO: hide qr scanner and info button */ /*
#scanner-div { #scanner-div {
border: none !important; border: none !important;
} }
#scanner-div * { /* #scanner-div * {
display: none; display: none;
} }
#scanner-div__scan_region { #scanner-div__scan_region {
@ -28,7 +28,10 @@
#scanner-div__scan_region * { #scanner-div__scan_region * {
display: block; display: block;
} }
#scanner-div__dashboard_section_csr { #html5-qrcode-button-camera-permission {
display: none !important;
}
/* #scanner-div__dashboard_section_csr {
display: none !important; display: none !important;
} }
#html5-qrcode-anchor-scan-type-change { #html5-qrcode-anchor-scan-type-change {
@ -39,8 +42,7 @@
} }
[alt="Info icon"] { [alt="Info icon"] {
display: none !important; display: none !important;
} }*/
.logo { .logo {
padding: 1.5em; padding: 1.5em;

@ -1,2 +1 @@
pub mod wallet_get_export; 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 { for emoji in pazzle {
let cat = (emoji & 240) >> 4; let cat = (emoji & 240) >> 4;
let idx = emoji & 15; let idx = emoji & 15;
res.push(( let cat_str = EMOJI_CAT[cat as usize];
EMOJI_CAT[cat as usize], res.push((cat_str, EMOJIS.get(cat_str).unwrap()[idx as usize].code));
EMOJIS.get(&EMOJI_CAT[cat as usize]).unwrap()[idx as usize].code,
));
} }
res 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::*; //use ng_repo::log::*;
/// taking a list of pazzle words, returns a list of u8 codes /// 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 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 { pub fn gen_shuffle_for_pazzle_opening(pazzle_length: u8) -> ShuffledPazzle {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let mut category_indices: Vec<u8> = (0..pazzle_length).collect(); let mut category_indices: Vec<u8> = (0..pazzle_length).collect();
@ -870,7 +851,7 @@ mod test {
let _ = file.write_all(&ser_wallet); let _ = file.write_all(&ser_wallet);
log_debug!("wallet id: {}", res.wallet.id()); 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!("mnemonic {:?}", display_mnemonic(&res.mnemonic));
log_debug!("pin {:?}", pin); log_debug!("pin {:?}", pin);

Loading…
Cancel
Save