diff --git a/locales/en.yml b/locales/en.yml index e960bf8..933cca9 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -117,6 +117,7 @@ wallets: change_server_confirmation: To apply change of connection settings, you need to re-open your wallet. Reopen it now? tx_send_cancel_conf: 'Are you sure you want to cancel sending of %{amount} ツ?' tx_receive_cancel_conf: 'Are you sure you want to cancel receiving of %{amount} ツ?' + rec_phrase_not_found: Recovery phrase not found. transport: desc: 'Use transport to receive or send messages synchronously:' tor_network: Tor network diff --git a/locales/ru.yml b/locales/ru.yml index 7bb5cfa..6081ad7 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -117,6 +117,7 @@ wallets: change_server_confirmation: Для применения изменения настроек соединения необходимо переоткрыть кошелёк. Переоткрыть его сейчас? tx_send_cancel_conf: 'Вы действительно хотите отменить отправку %{amount} ツ?' tx_receive_cancel_conf: 'Вы действительно хотите отменить получение %{amount} ツ?' + rec_phrase_not_found: Фраза восстановления не найдена. transport: desc: 'Используйте транспорт для синхронных получения или отправки сообщений:' tor_network: Сеть Tor diff --git a/src/gui/views/camera.rs b/src/gui/views/camera.rs index 2164a7a..663fb21 100644 --- a/src/gui/views/camera.rs +++ b/src/gui/views/camera.rs @@ -17,6 +17,7 @@ use std::thread; use eframe::emath::Align; use egui::load::SizedTexture; use egui::{Layout, Pos2, Rect, TextureOptions, Widget}; +use grin_util::ZeroingString; use grin_wallet_libwallet::SlatepackAddress; use image::{DynamicImage, EncodableLayout, ImageFormat}; use crate::gui::Colors; @@ -181,16 +182,16 @@ impl CameraContent { // Check if string starts with Grin address prefix. if text.starts_with("tgrin") || text.starts_with("grin") { if SlatepackAddress::try_from(text).is_ok() { - return QrScanResult::Address(text.to_string()) + return QrScanResult::Address(ZeroingString::from(text)) } } // Check if string contains Slatepack message prefix and postfix. if text.starts_with("BEGINSLATEPACK.") && text.ends_with("ENDSLATEPACK.") { - return QrScanResult::Slatepack(text.to_string()) + return QrScanResult::Slatepack(ZeroingString::from(text)) } - QrScanResult::Text(text.to_string()) + QrScanResult::Text(ZeroingString::from(text)) } /// Get QR code scan result. diff --git a/src/gui/views/types.rs b/src/gui/views/types.rs index b356a8d..67d2d88 100644 --- a/src/gui/views/types.rs +++ b/src/gui/views/types.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use grin_util::ZeroingString; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::Modal; @@ -144,11 +145,11 @@ impl TextEditOptions { #[derive(Clone)] pub enum QrScanResult { /// Slatepack message. - Slatepack(String), + Slatepack(ZeroingString), /// Slatepack address. - Address(String), + Address(ZeroingString), /// Parsed text. - Text(String) + Text(ZeroingString) } /// QR code scan state. diff --git a/src/gui/views/wallets/creation/creation.rs b/src/gui/views/wallets/creation/creation.rs index 7d2af4b..a4d8b64 100644 --- a/src/gui/views/wallets/creation/creation.rs +++ b/src/gui/views/wallets/creation/creation.rs @@ -14,10 +14,11 @@ use egui::{Id, Margin, RichText, ScrollArea, TextStyle, vec2, Widget}; use egui_extras::{RetainedImage, Size, StripBuilder}; +use grin_util::ZeroingString; use crate::built_info; use crate::gui::Colors; -use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, FOLDER_PLUS, SHARE_FAT}; +use crate::gui::icons::{CHECK, CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, FOLDER_PLUS, SCAN, SHARE_FAT}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, Root, View}; use crate::gui::views::types::{ModalPosition, TextEditOptions}; @@ -178,28 +179,30 @@ impl WalletCreation { } if step == Step::EnterMnemonic { ui.add_space(4.0); - if !step_available { - self.copy_or_paste_button_ui(ui, cb); - ui.add_space(4.0); - } else { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - ui.columns(2, |columns| { - // Show copy or paste button for mnemonic phrase step. - columns[0].vertical_centered_justified(|ui| { - self.copy_or_paste_button_ui(ui, cb); - }); + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - // Show next step button if there are no empty words. + ui.columns(2, |columns| { + // Show copy or paste button for mnemonic phrase step. + columns[0].vertical_centered_justified(|ui| { + self.copy_or_paste_button_ui(ui, cb); + }); + + columns[1].vertical_centered_justified(|ui| { if step_available { - columns[1].vertical_centered_justified(|ui| { - self.next_step_button_ui(ui, step, on_create); + // Show next step button if there are no empty words. + self.next_step_button_ui(ui, step, on_create); + } else { + // Show QR code scan button. + let scan_text = format!("{} {}", SCAN, t!("scan").to_uppercase()); + View::button(ui, scan_text, Colors::WHITE, || { + self.mnemonic_setup.show_qr_scan_modal(cb); }); } }); - ui.add_space(4.0); - } + }); + ui.add_space(4.0); } else { if step_available { ui.add_space(4.0); @@ -225,7 +228,8 @@ impl WalletCreation { // Show paste button. let p_t = format!("{} {}", CLIPBOARD_TEXT, t!("paste").to_uppercase()); View::button(ui, p_t, Colors::WHITE, || { - self.mnemonic_setup.mnemonic.import_text(cb.get_string_from_buffer()); + let data = ZeroingString::from(cb.get_string_from_buffer()); + self.mnemonic_setup.mnemonic.import_text(&data); }); } } diff --git a/src/gui/views/wallets/creation/mnemonic.rs b/src/gui/views/wallets/creation/mnemonic.rs index d532398..d2dff66 100644 --- a/src/gui/views/wallets/creation/mnemonic.rs +++ b/src/gui/views/wallets/creation/mnemonic.rs @@ -17,8 +17,8 @@ use egui::{Id, RichText}; use crate::gui::Colors; use crate::gui::icons::PENCIL; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{Modal, Root, View}; -use crate::gui::views::types::{ModalContainer, ModalPosition, TextEditOptions}; +use crate::gui::views::{CameraContent, Modal, Root, View}; +use crate::gui::views::types::{ModalContainer, ModalPosition, QrScanResult, TextEditOptions}; use crate::wallet::Mnemonic; use crate::wallet::types::{PhraseMode, PhraseSize}; @@ -37,6 +37,11 @@ pub struct MnemonicSetup { /// Flag to check if entered word is valid. valid_word_edit: bool, + /// Camera content for QR scan [`Modal`]. + camera_content: CameraContent, + /// Flag to check if recovery phrase was found at QR code scanning [`Modal`]. + scan_phrase_not_found: Option, + /// [`Modal`] identifiers allowed at this ui container. modal_ids: Vec<&'static str> } @@ -44,6 +49,9 @@ pub struct MnemonicSetup { /// Identifier for word input [`Modal`]. pub const WORD_INPUT_MODAL: &'static str = "word_input_modal"; +/// Identifier for QR code recovery phrase scan [`Modal`]. +const QR_CODE_PHRASE_SCAN_MODAL: &'static str = "qr_code_rec_phrase_scan_modal"; + impl Default for MnemonicSetup { fn default() -> Self { Self { @@ -52,8 +60,11 @@ impl Default for MnemonicSetup { word_num_edit: 0, word_edit: String::from(""), valid_word_edit: true, + camera_content: CameraContent::default(), + scan_phrase_not_found: None, modal_ids: vec![ - WORD_INPUT_MODAL + WORD_INPUT_MODAL, + QR_CODE_PHRASE_SCAN_MODAL ] } } @@ -71,6 +82,7 @@ impl ModalContainer for MnemonicSetup { cb: &dyn PlatformCallbacks) { match modal.id { WORD_INPUT_MODAL => self.word_modal_ui(ui, modal, cb), + QR_CODE_PHRASE_SCAN_MODAL => self.scan_qr_modal_ui(ui, modal, cb), _ => {} } } @@ -338,6 +350,85 @@ impl MnemonicSetup { ui.add_space(6.0); }); } + + /// Show QR code recovery phrase scanner [`Modal`]. + pub fn show_qr_scan_modal(&mut self, cb: &dyn PlatformCallbacks) { + self.scan_phrase_not_found = None; + // Show QR code scan modal. + Modal::new(QR_CODE_PHRASE_SCAN_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("scan_qr")) + .closeable(false) + .show(); + cb.start_camera(); + } + + /// Draw QR code scan [`Modal`] content. + fn scan_qr_modal_ui(&mut self, + ui: &mut egui::Ui, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + // Show scan result if exists or show camera content while scanning. + if let Some(_) = &self.scan_phrase_not_found { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("wallets.rec_phrase_not_found")) + .size(17.0) + .color(Colors::RED)); + }); + ui.add_space(6.0); + } else if let Some(result) = self.camera_content.qr_scan_result() { + cb.stop_camera(); + self.camera_content.clear_state(); + match &result { + QrScanResult::Text(text) => { + self.mnemonic.import_text(text); + if self.mnemonic.is_valid_phrase() { + modal.close(); + return; + } + } + _ => {} + } + + // Set an error when found phrase was not valid. + self.scan_phrase_not_found = Some(true); + Modal::set_title(t!("scan_result")); + } else { + ui.add_space(6.0); + self.camera_content.ui(ui, cb); + ui.add_space(6.0); + } + + if self.scan_phrase_not_found.is_some() { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::WHITE, || { + self.scan_phrase_not_found = None; + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("repeat"), Colors::WHITE, || { + Modal::set_title(t!("scan_qr")); + self.scan_phrase_not_found = None; + cb.start_camera(); + }); + }); + }); + } else { + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::WHITE, || { + cb.stop_camera(); + modal.close(); + }); + }); + } + ui.add_space(6.0); + } } /// Calculate word list columns count based on available ui width. diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index a77378c..49dcf44 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -192,9 +192,8 @@ impl WalletContent { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw button to scan QR code. View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || { - // Load accounts. self.qr_scan_result = None; - // Show account list modal. + // Show QR code scan modal. Modal::new(QR_CODE_SCAN_MODAL) .position(ModalPosition::CenterTop) .title(t!("scan_qr")) @@ -359,7 +358,7 @@ impl WalletContent { } } - /// Draw QR scanner [`Modal`] content. + /// Draw QR code scan [`Modal`] content. fn scan_qr_modal_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, @@ -371,7 +370,7 @@ impl WalletContent { QrScanResult::Slatepack(t) => t, QrScanResult::Address(t) => t, QrScanResult::Text(t) => t - }.clone(); + }.to_string(); View::horizontal_line(ui, Colors::ITEM_STROKE); ui.add_space(3.0); ScrollArea::vertical() @@ -396,7 +395,7 @@ impl WalletContent { ui.vertical_centered(|ui| { let copy_text = format!("{} {}", COPY, t!("copy")); View::button(ui, copy_text, Colors::BUTTON, || { - cb.copy_string_to_buffer(result_text); + cb.copy_string_to_buffer(result_text.to_string()); self.qr_scan_result = None; modal.close(); }); @@ -409,7 +408,7 @@ impl WalletContent { QrScanResult::Slatepack(message) => { // Redirect to messages to handle parsed message. let mut messages = - WalletMessages::new(wallet.can_use_dandelion(), Some(message.clone())); + WalletMessages::new(wallet.can_use_dandelion(), Some(message.to_string())); messages.parse_message(wallet); modal.close(); self.current_tab = Box::new(messages); @@ -420,7 +419,7 @@ impl WalletContent { // Redirect to send amount with Tor. let addr = wallet.slatepack_address().unwrap(); let mut transport = WalletTransport::new(addr.clone()); - transport.show_send_tor_modal(cb, Some(receiver.clone())); + transport.show_send_tor_modal(cb, Some(receiver.to_string())); modal.close(); self.current_tab = Box::new(transport); return; diff --git a/src/wallet/mnemonic.rs b/src/wallet/mnemonic.rs index 537cd8c..54da957 100644 --- a/src/wallet/mnemonic.rs +++ b/src/wallet/mnemonic.rs @@ -13,6 +13,7 @@ // limitations under the License. use grin_keychain::mnemonic::{from_entropy, search, to_entropy}; +use grin_util::ZeroingString; use rand::{Rng, thread_rng}; use crate::wallet::types::{PhraseMode, PhraseSize}; @@ -105,7 +106,7 @@ impl Mnemonic { } /// Set words from provided text if possible. - pub fn import_text(&mut self, text: String) { + pub fn import_text(&mut self, text: &ZeroingString) { if self.mode != PhraseMode::Import { return; }