diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py index d5b84735f..7ffa41931 100644 --- a/electrum/plugins/trustedcoin/qt.py +++ b/electrum/plugins/trustedcoin/qt.py @@ -25,26 +25,22 @@ from functools import partial import threading -import sys import os from typing import TYPE_CHECKING -from PyQt5.QtGui import QPixmap -from PyQt5.QtCore import QObject, pyqtSignal, QTimer +from PyQt5.QtGui import QPixmap, QMovie, QColor +from PyQt5.QtCore import QObject, pyqtSignal, QSize, Qt from PyQt5.QtWidgets import (QTextEdit, QVBoxLayout, QLabel, QGridLayout, QHBoxLayout, QRadioButton, QCheckBox, QLineEdit, QPushButton, QWidget) from electrum.i18n import _ -from electrum import keystore -from electrum.bip32 import xpub_type from electrum.plugin import hook from electrum.util import is_valid_email from electrum.logging import Logger, get_logger from electrum.base_wizard import GoBack, UserCancelled -from .qt_common import QSignalObject from electrum.gui.qt.util import (read_QIcon, WindowModalDialog, WaitingDialog, OkButton, - CancelButton, Buttons, icon_path, WWLabel, CloseButton, ChoicesLayout) + CancelButton, Buttons, icon_path, WWLabel, CloseButton, ChoicesLayout, ColorScheme) from electrum.gui.qt.qrcodewidget import QRCodeWidget from electrum.gui.qt.amountedit import AmountEdit from electrum.gui.qt.main_window import StatusBarButton @@ -52,10 +48,8 @@ from electrum.gui.qt.installwizard import InstallWizard from electrum.gui.qt.wizard.wallet import WCCreateSeed, WCConfirmSeed, WCHaveSeed, WCEnterExt, WCConfirmExt from electrum.gui.qt.wizard.wizard import WizardComponent -# from .trustedcoin import TrustedCoinPlugin, server, DISCLAIMER, make_xpub, get_signing_xpub, get_user_id -from .trustedcoin import (TrustedCoinPlugin, server, ErrorConnectingServer, - DISCLAIMER, get_user_id, get_signing_xpub, - TrustedCoinException, make_xpub) +from .qt_common import QSignalObject +from .trustedcoin import TrustedCoinPlugin, server, DISCLAIMER if TYPE_CHECKING: @@ -505,6 +499,7 @@ class WCShowConfirmOTP(WizardComponent): def __init__(self, parent, wizard): WizardComponent.__init__(self, parent, wizard, title=_('Authenticator secret')) self.plugin = wizard.plugins.get_plugin('trustedcoin') + self._otp_verified = False self.new_otp = QWidget() new_otp_layout = QVBoxLayout() @@ -527,6 +522,18 @@ class WCShowConfirmOTP(WizardComponent): self.authlabelnew = WWLabel(_('Then, enter your Google Authenticator code:')) self.authlabelexist = WWLabel(_('Google Authenticator code:')) + self.spinner = QMovie(icon_path('spinner.gif')) + self.spinner.setScaledSize(QSize(24, 24)) + self.spinner.setBackgroundColor(QColor('black')) + self.spinner_l = QLabel() + self.spinner_l.setMargin(5) + self.spinner_l.setVisible(False) + self.spinner_l.setMovie(self.spinner) + + self.otp_status_l = QLabel() + self.otp_status_l.setAlignment(Qt.AlignHCenter) + self.otp_status_l.setVisible(False) + self.resetlabel = WWLabel(_('If you have lost your OTP secret, click the button below to request a new secret from the server.')) self.button = QPushButton('Request OTP secret') self.button.clicked.connect(self.on_request_otp) @@ -534,15 +541,18 @@ class WCShowConfirmOTP(WizardComponent): hbox = QHBoxLayout() hbox.addWidget(self.authlabelnew) hbox.addWidget(self.authlabelexist) - pw = AmountEdit(None, is_int = True) - pw.setFocus(True) - pw.setMaximumWidth(150) - hbox.addWidget(pw) - # hbox.addStretch(1) + hbox.addStretch(1) + hbox.addWidget(self.spinner_l) + self.otp_e = AmountEdit(None, is_int=True) + self.otp_e.setFocus(True) + self.otp_e.setMaximumWidth(150) + self.otp_e.textEdited.connect(self.on_otp_edited) + hbox.addWidget(self.otp_e) self.layout().addWidget(self.new_otp) self.layout().addWidget(self.exist_otp) self.layout().addLayout(hbox) + self.layout().addWidget(self.otp_status_l) self.layout().addWidget(self.resetlabel) self.layout().addWidget(self.button) self.layout().addStretch(1) @@ -550,6 +560,10 @@ class WCShowConfirmOTP(WizardComponent): def on_ready(self): self.plugin.so.busyChanged.connect(self.on_busy_changed) self.plugin.so.remoteKeyError.connect(self.on_remote_key_error) + self.plugin.so.otpSuccess.connect(self.on_otp_success) + self.plugin.so.otpError.connect(self.on_otp_error) + self.plugin.so.remoteKeyError.connect(self.on_remote_key_error) + self.plugin.so.createKeystore(self.wizard_data['2fa_email']) def update(self): @@ -558,8 +572,8 @@ class WCShowConfirmOTP(WizardComponent): self.exist_otp.setVisible(not is_new) self.authlabelnew.setVisible(is_new) self.authlabelexist.setVisible(not is_new) - self.resetlabel.setVisible(not is_new) - self.button.setVisible(not is_new) + self.resetlabel.setVisible(not is_new and not self._otp_verified) + self.button.setVisible(not is_new and not self._otp_verified) if self.plugin.so.otpSecret: self.secretlabel.setText(self.plugin.so.otpSecret) @@ -568,18 +582,50 @@ class WCShowConfirmOTP(WizardComponent): self.qr.setData(uri) def on_busy_changed(self): - self.busy = self.plugin.so.busy - if not self.busy: - self.update() + if not self.plugin.so._verifyingOtp: + self.busy = self.plugin.so.busy + if not self.busy: + self.update() def on_remote_key_error(self, text): self._logger.error(text) self.error = text def on_request_otp(self): + self.otp_status_l.setVisible(False) self.plugin.so.resetOtpSecret() self.update() + def on_otp_success(self): + self._otp_verified = True + self.otp_status_l.setText('Valid!') + self.otp_status_l.setVisible(True) + self.otp_status_l.setStyleSheet(ColorScheme.GREEN.as_stylesheet(False)) + self.setEnabled(True) + self.spinner_l.setVisible(False) + self.spinner.stop() + + self.valid = True + + def on_otp_error(self, message): + self.otp_status_l.setText(message) + self.otp_status_l.setVisible(True) + self.otp_status_l.setStyleSheet(ColorScheme.RED.as_stylesheet(False)) + self.setEnabled(True) + self.spinner_l.setVisible(False) + self.spinner.stop() + + def on_otp_edited(self): + self.otp_status_l.setVisible(False) + text = self.otp_e.text() + if len(text) == 6: + # verify otp + self.plugin.so.checkOtp(self.plugin.so.shortId, int(text)) + self.setEnabled(False) + self.spinner_l.setVisible(True) + self.spinner.start() + self.otp_e.setText('') + def apply(self): pass diff --git a/electrum/plugins/trustedcoin/qt_common.py b/electrum/plugins/trustedcoin/qt_common.py index 61aec8bcd..d8186af30 100644 --- a/electrum/plugins/trustedcoin/qt_common.py +++ b/electrum/plugins/trustedcoin/qt_common.py @@ -13,7 +13,6 @@ from electrum.gui.qt_common.plugins import PluginQObject class QSignalObject(PluginQObject): canSignWithoutServerChanged = pyqtSignal() - _canSignWithoutServer = False termsAndConditionsRetrieved = pyqtSignal([str], arguments=['message']) termsAndConditionsError = pyqtSignal([str], arguments=['message']) otpError = pyqtSignal([str], arguments=['message']) @@ -21,13 +20,9 @@ class QSignalObject(PluginQObject): disclaimerChanged = pyqtSignal() keystoreChanged = pyqtSignal() otpSecretChanged = pyqtSignal() - _otpSecret = '' shortIdChanged = pyqtSignal() - _shortId = '' billingModelChanged = pyqtSignal() - _billingModel = [] - _remoteKeyState = '' remoteKeyStateChanged = pyqtSignal() remoteKeyError = pyqtSignal([str], arguments=['message']) @@ -36,6 +31,12 @@ class QSignalObject(PluginQObject): def __init__(self, plugin, wizard, parent): super().__init__(plugin, parent) self.wizard = wizard + self._canSignWithoutServer = False + self._otpSecret = '' + self._shortId = '' + self._billingModel = [] + self._remoteKeyState = '' + self._verifyingOtp = False @pyqtProperty(str, notify=disclaimerChanged) def disclaimer(self): @@ -241,7 +242,9 @@ class QSignalObject(PluginQObject): finally: self._busy = False self.busyChanged.emit() + self._verifyingOtp = False + self._verifyingOtp = True self._busy = True self.busyChanged.emit() t = threading.Thread(target=check_otp_task, daemon=True)