diff --git a/electrum/gui/qml/auth.py b/electrum/gui/qml/auth.py index e55a0c036..a7e141204 100644 --- a/electrum/gui/qml/auth.py +++ b/electrum/gui/qml/auth.py @@ -4,9 +4,9 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot from electrum.logging import get_logger -def auth_protect(func=None, reject=None): +def auth_protect(func=None, reject=None, method='pin'): if func is None: - return partial(auth_protect, reject=reject) + return partial(auth_protect, reject=reject, method=method) @wraps(func) def wrapper(self, *args, **kwargs): @@ -15,14 +15,14 @@ def auth_protect(func=None, reject=None): self._logger.debug('object already has a pending authed function call') raise Exception('object already has a pending authed function call') setattr(self, '__auth_fcall', (func,args,kwargs,reject)) - getattr(self, 'authRequired').emit() + getattr(self, 'authRequired').emit(method) return wrapper class AuthMixin: _auth_logger = get_logger(__name__) - authRequired = pyqtSignal() + authRequired = pyqtSignal([str],arguments=['method']) @pyqtSlot() def authProceed(self): diff --git a/electrum/gui/qml/components/Pin.qml b/electrum/gui/qml/components/Pin.qml new file mode 100644 index 000000000..492135180 --- /dev/null +++ b/electrum/gui/qml/components/Pin.qml @@ -0,0 +1,90 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +import "controls" + +Dialog { + id: root + + width: parent.width * 2/3 + height: parent.height * 1/3 + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + modal: true + parent: Overlay.overlay + Overlay.modal: Rectangle { + color: "#aa000000" + } + + focus: true + + standardButtons: Dialog.Cancel + + property string mode // [check, enter, change] + property string pincode // old one passed in when change, new one passed out + + property int _phase: mode == 'enter' ? 1 : 0 // 0 = existing pin, 1 = new pin, 2 = re-enter new pin + property string _pin + + function submit() { + if (_phase == 0) { + if (pin.text == pincode) { + pin.text = '' + if (mode == 'check') + accepted() + else + _phase = 1 + return + } + } + if (_phase == 1) { + _pin = pin.text + pin.text = '' + _phase = 2 + return + } + if (_phase == 2) { + if (_pin == pin.text) { + pincode = pin.text + accepted() + } + return + } + } + + ColumnLayout { + width: parent.width + height: parent.height + + Label { + text: [qsTr('Enter PIN'), qsTr('Enter New PIN'), qsTr('Re-enter New PIN')][_phase] + font.pixelSize: constants.fontSizeXXLarge + Layout.alignment: Qt.AlignHCenter + } + + TextField { + id: pin + Layout.preferredWidth: root.width *2/3 + Layout.alignment: Qt.AlignHCenter + font.pixelSize: constants.fontSizeXXLarge + maximumLength: 6 + inputMethodHints: Qt.ImhDigitsOnly + echoMode: TextInput.Password + focus: true + onTextChanged: { + if (text.length == 6) { + submit() + } + } + } + + Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } + } + +} diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index 7509b73a2..ef3251ea8 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -6,6 +6,8 @@ import QtQuick.Controls.Material 2.0 import org.electrum 1.0 Pane { + id: preferences + property string title: qsTr("Preferences") ColumnLayout { @@ -116,6 +118,49 @@ Pane { } } + Label { + text: qsTr('PIN') + } + + RowLayout { + Label { + text: Config.pinCode == '' ? qsTr('Off'): qsTr('On') + color: Material.accentColor + Layout.rightMargin: constants.paddingMedium + } + Button { + text: qsTr('Enable') + visible: Config.pinCode == '' + onClicked: { + var dialog = pinSetup.createObject(preferences, {mode: 'enter'}) + dialog.accepted.connect(function() { + Config.pinCode = dialog.pincode + dialog.close() + }) + dialog.open() + } + } + Button { + text: qsTr('Change') + visible: Config.pinCode != '' + onClicked: { + var dialog = pinSetup.createObject(preferences, {mode: 'change', pincode: Config.pinCode}) + dialog.accepted.connect(function() { + Config.pinCode = dialog.pincode + dialog.close() + }) + dialog.open() + } + } + Button { + text: qsTr('Remove') + visible: Config.pinCode != '' + onClicked: { + Config.pinCode = '' + } + } + } + Label { text: qsTr('Lightning Routing') } @@ -136,6 +181,11 @@ Pane { } + Component { + id: pinSetup + Pin {} + } + Component.onCompleted: { baseUnit.currentIndex = ['BTC','mBTC','bits','sat'].indexOf(Config.baseUnit) thousands.checked = Config.thousandsSeparator diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index a2d048e3b..b496a4026 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -179,6 +179,14 @@ ApplicationWindow } } + property alias pinDialog: _pinDialog + Component { + id: _pinDialog + Pin { + onClosed: destroy() + } + } + NotificationPopup { id: notificationPopup } @@ -221,7 +229,7 @@ ApplicationWindow interval: 5000 repeat: false } - + Connections { target: Daemon function onWalletRequiresPassword() { @@ -233,6 +241,9 @@ ApplicationWindow var dialog = app.messageDialog.createObject(app, {'text': error}) dialog.open() } + function onAuthRequired(method) { + handleAuthRequired(Daemon, method) + } } Connections { @@ -244,45 +255,61 @@ ApplicationWindow Connections { target: Daemon.currentWallet - function onAuthRequired() { + function onAuthRequired(method) { + handleAuthRequired(Daemon.currentWallet, method) + } + // TODO: add to notification queue instead of barging through + function onPaymentSucceeded(key) { + notificationPopup.show(qsTr('Payment Succeeded')) + } + function onPaymentFailed(key, reason) { + notificationPopup.show(qsTr('Payment Failed') + ': ' + reason) + } + } + + Connections { + target: Config + function onAuthRequired(method) { + handleAuthRequired(Config, method) + } + } + + function handleAuthRequired(qtobject, method) { + console.log('AUTHENTICATING USING METHOD ' + method) + if (method == 'wallet') { if (Daemon.currentWallet.verify_password('')) { // wallet has no password - Daemon.currentWallet.authProceed() + qtobject.authProceed() } else { var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')}) dialog.accepted.connect(function() { if (Daemon.currentWallet.verify_password(dialog.password)) { - Daemon.currentWallet.authProceed() + qtobject.authProceed() } else { - Daemon.currentWallet.authCancel() + qtobject.authCancel() } }) dialog.rejected.connect(function() { - Daemon.currentWallet.authCancel() + qtobject.authCancel() + }) + dialog.open() + } + } else if (method == 'pin') { + if (Config.pinCode == '') { + // no PIN configured + qtobject.authProceed() + } else { + var dialog = app.pinDialog.createObject(app, {mode: 'check', pincode: Config.pinCode}) + dialog.accepted.connect(function() { + qtobject.authProceed() + dialog.close() + }) + dialog.rejected.connect(function() { + qtobject.authCancel() }) dialog.open() } - } - // TODO: add to notification queue instead of barging through - function onPaymentSucceeded(key) { - notificationPopup.show(qsTr('Payment Succeeded')) - } - function onPaymentFailed(key, reason) { - notificationPopup.show(qsTr('Payment Failed') + ': ' + reason) } } - Connections { - target: Daemon - function onAuthRequired() { - var dialog = app.messageDialog.createObject(app, {'text': 'Auth placeholder', 'yesno': true}) - dialog.yesClicked.connect(function() { - Daemon.authProceed() - }) - dialog.noClicked.connect(function() { - Daemon.authCancel() - }) - dialog.open() - } - } } diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 88bb0ee7e..cf1f28de6 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -6,8 +6,9 @@ from electrum.logging import get_logger from electrum.util import DECIMAL_POINT_DEFAULT, format_satoshis from .qetypes import QEAmount +from .auth import AuthMixin, auth_protect -class QEConfig(QObject): +class QEConfig(AuthMixin, QObject): def __init__(self, config, parent=None): super().__init__(parent) self.config = config @@ -80,6 +81,22 @@ class QEConfig(QObject): self.config.set_key('confirmed_only', not checked, True) self.spendUnconfirmedChanged.emit() + pinCodeChanged = pyqtSignal() + @pyqtProperty(str, notify=pinCodeChanged) + def pinCode(self): + return self.config.get('pin_code', '') + + @pinCode.setter + def pinCode(self, pin_code): + if pin_code == '': + self.pinCodeRemoveAuth() + self.config.set_key('pin_code', pin_code, True) + self.pinCodeChanged.emit() + + @auth_protect(method='wallet') + def pinCodeRemoveAuth(self): + pass # no-op + useGossipChanged = pyqtSignal() @pyqtProperty(bool, notify=useGossipChanged) def useGossip(self):