from typing import Callable, TYPE_CHECKING, Optional, Union from kivy.app import App from kivy.factory import Factory from kivy.properties import ObjectProperty from kivy.lang import Builder from decimal import Decimal from kivy.clock import Clock from electrum.util import InvalidPassword from electrum.gui.kivy.i18n import _ if TYPE_CHECKING: from ...main_window import ElectrumWindow from electrum.wallet import Abstract_Wallet from electrum.storage import WalletStorage Builder.load_string(''' id: popup title: 'Electrum' message: '' basename:'' is_change: False BoxLayout: size_hint: 1, 1 orientation: 'vertical' spacing: '12dp' padding: '12dp' BoxLayout: size_hint: 1, None orientation: 'horizontal' height: '40dp' Label: size_hint: 0.85, None height: '40dp' font_size: '20dp' text: _('Wallet') + ': ' + root.basename text_size: self.width, None IconButton: size_hint: 0.15, None height: '40dp' icon: 'atlas://electrum/gui/kivy/theming/light/btn_create_account' on_release: root.select_file() disabled: root.is_change opacity: 0 if root.is_change else 1 Widget: size_hint: 1, 0.05 Label: size_hint: 0.70, None font_size: '20dp' text: root.message text_size: self.width, None Widget: size_hint: 1, 0.05 BoxLayout: orientation: 'horizontal' id: box_generic_password size_hint_y: 0.05 height: '40dp' TextInput: height: '40dp' id: textinput_generic_password valign: 'center' multiline: False on_text_validate: popup.on_password(self.text) password: True size_hint: 0.85, None unfocus_on_touch: False focus: True IconButton: height: '40dp' size_hint: 0.15, None icon: 'atlas://electrum/gui/kivy/theming/light/eye1' icon_size: '40dp' on_release: textinput_generic_password.password = False if textinput_generic_password.password else True Widget: size_hint: 1, 1 id: popup title: 'Electrum' message: '' basename:'' BoxLayout: size_hint: 1, 1 orientation: 'vertical' Widget: size_hint: 1, 0.05 Label: size_hint: 0.70, None font_size: '20dp' text: root.message text_size: self.width, None Widget: size_hint: 1, 0.05 Label: id: label_pin size_hint_y: 0.05 font_size: '50dp' text: '*'*len(kb.password) + '-'*(6-len(kb.password)) size: self.texture_size Widget: size_hint: 1, 0.05 GridLayout: id: kb size_hint: 1, None height: self.minimum_height update_amount: popup.update_password password: '' on_password: popup.on_password(self.password) spacing: '2dp' cols: 3 KButton: text: '1' KButton: text: '2' KButton: text: '3' KButton: text: '4' KButton: text: '5' KButton: text: '6' KButton: text: '7' KButton: text: '8' KButton: text: '9' KButton: text: 'Clear' KButton: text: '0' KButton: text: '<' ''') class AbstractPasswordDialog: def init(self, app: 'ElectrumWindow', *, check_password = None, on_success: Callable = None, on_failure: Callable = None, is_change: bool = False, is_password: bool = True, # whether this is for a generic password or for a numeric PIN has_password: bool = False, message: str = '', basename:str=''): self.app = app self.pw_check = check_password self.message = message self.on_success = on_success self.on_failure = on_failure self.success = False self.is_change = is_change self.pw = None self.new_password = None self.title = 'Electrum' self.level = 1 if is_change and not has_password else 0 self.basename = basename self.update_screen() def update_screen(self): self.clear_password() if self.level == 0 and self.message == '': self.message = self.enter_pw_message elif self.level == 1: self.message = self.enter_new_pw_message elif self.level == 2: self.message = self.confirm_new_pw_message def check_password(self, password): if self.level > 0: return True try: self.pw_check(password) return True except InvalidPassword as e: return False def on_dismiss(self): if self.level == 1 and self.allow_disable and self.on_success: self.on_success(self.pw, None) return False if not self.success: if self.on_failure: self.on_failure() else: # keep dialog open return True else: if self.on_success: args = (self.pw, self.new_password) if self.is_change else (self.pw,) Clock.schedule_once(lambda dt: self.on_success(*args), 0.1) def update_password(self, c): kb = self.ids.kb text = kb.password if c == '<': text = text[:-1] elif c == 'Clear': text = '' else: text += c kb.password = text def do_check(self, pw): if self.check_password(pw): if self.is_change is False: self.success = True self.pw = pw self.message = _('Please wait...') self.dismiss() elif self.level == 0: self.level = 1 self.pw = pw self.update_screen() elif self.level == 1: self.level = 2 self.new_password = pw self.update_screen() elif self.level == 2: self.success = pw == self.new_password self.dismiss() else: self.app.show_error(self.wrong_password_message) self.clear_password() class PasswordDialog(AbstractPasswordDialog, Factory.Popup): enter_pw_message = _('Enter your password') enter_new_pw_message = _('Enter new password') confirm_new_pw_message = _('Confirm new password') wrong_password_message = _('Wrong password') allow_disable = False def clear_password(self): self.ids.textinput_generic_password.text = '' def on_password(self, pw: str): # if setting new generic password, enforce min length if self.level > 0: if len(pw) < 6: self.app.show_error(_('Password is too short (min {} characters)').format(6)) return # don't enforce minimum length on existing self.do_check(pw) def select_file(self): self.app.wallets_dialog() class PincodeDialog(AbstractPasswordDialog, Factory.Popup): enter_pw_message = _('Enter your PIN') enter_new_pw_message = _('Enter new PIN') confirm_new_pw_message = _('Confirm new PIN') wrong_password_message = _('Wrong PIN') allow_disable = True def clear_password(self): self.ids.kb.password = '' def on_password(self, pw: str): # PIN codes are exactly 6 chars if len(pw) >= 6: self.do_check(pw)