qml: use new wizard approach in qml and also implement 2FA/trustedcoin
This commit is contained in:
@@ -8,4 +8,4 @@ description = ''.join([
|
||||
])
|
||||
requires_wallet_type = ['2fa']
|
||||
registers_wallet_type = '2fa'
|
||||
available_for = ['qt', 'cmdline', 'kivy']
|
||||
available_for = ['qt', 'cmdline', 'kivy', 'qml']
|
||||
|
||||
332
electrum/plugins/trustedcoin/qml.py
Normal file
332
electrum/plugins/trustedcoin/qml.py
Normal file
@@ -0,0 +1,332 @@
|
||||
import threading
|
||||
import socket
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.plugin import hook
|
||||
from electrum.bip32 import xpub_type
|
||||
from electrum import keystore
|
||||
|
||||
from electrum.gui.qml.qewallet import QEWallet
|
||||
from electrum.gui.qml.plugins import PluginQObject
|
||||
|
||||
from .trustedcoin import (TrustedCoinPlugin, server, ErrorConnectingServer,
|
||||
MOBILE_DISCLAIMER, get_user_id, get_signing_xpub,
|
||||
TrustedCoinException, make_xpub)
|
||||
|
||||
class Plugin(TrustedCoinPlugin):
|
||||
|
||||
class QSignalObject(PluginQObject):
|
||||
requestView = pyqtSignal([str], arguments=['component'])
|
||||
|
||||
canSignWithoutServerChanged = pyqtSignal()
|
||||
_canSignWithoutServer = False
|
||||
termsAndConditionsChanged = pyqtSignal()
|
||||
_termsAndConditions = ''
|
||||
termsAndConditionsErrorChanged = pyqtSignal()
|
||||
_termsAndConditionsError = ''
|
||||
createRemoteKeyErrorChanged = pyqtSignal()
|
||||
_createRemoteKeyError = ''
|
||||
otpError = pyqtSignal()
|
||||
otpSuccess = pyqtSignal()
|
||||
disclaimerChanged = pyqtSignal()
|
||||
keystoreChanged = pyqtSignal()
|
||||
otpSecretChanged = pyqtSignal()
|
||||
_otpSecret = ''
|
||||
shortIdChanged = pyqtSignal()
|
||||
_shortId = ''
|
||||
|
||||
def __init__(self, plugin, parent):
|
||||
super().__init__(plugin, parent)
|
||||
|
||||
@pyqtSlot(result=str)
|
||||
def settingsComponent(self): return '../../../plugins/trustedcoin/qml/Settings.qml'
|
||||
|
||||
@pyqtProperty(str, notify=disclaimerChanged)
|
||||
def disclaimer(self):
|
||||
return '\n\n'.join(MOBILE_DISCLAIMER)
|
||||
|
||||
@pyqtProperty(bool, notify=canSignWithoutServerChanged)
|
||||
def canSignWithoutServer(self):
|
||||
return self._canSignWithoutServer
|
||||
|
||||
@pyqtProperty('QVariantMap', notify=keystoreChanged)
|
||||
def keystore(self):
|
||||
return self._keystore
|
||||
|
||||
@pyqtProperty(str, notify=otpSecretChanged)
|
||||
def otpSecret(self):
|
||||
return self._otpSecret
|
||||
|
||||
@pyqtProperty(str, notify=shortIdChanged)
|
||||
def shortId(self):
|
||||
return self._shortId
|
||||
|
||||
@pyqtSlot(str)
|
||||
def otpSubmit(self, otp):
|
||||
self._plugin.on_otp(otp)
|
||||
|
||||
@pyqtProperty(str, notify=termsAndConditionsChanged)
|
||||
def termsAndConditions(self):
|
||||
return self._termsAndConditions
|
||||
|
||||
@pyqtProperty(str, notify=termsAndConditionsErrorChanged)
|
||||
def termsAndConditionsError(self):
|
||||
return self._termsAndConditionsError
|
||||
|
||||
@pyqtProperty(str, notify=createRemoteKeyErrorChanged)
|
||||
def createRemoteKeyError(self):
|
||||
return self._createRemoteKeyError
|
||||
|
||||
@pyqtSlot()
|
||||
def fetchTermsAndConditions(self):
|
||||
def fetch_task():
|
||||
try:
|
||||
self.plugin.logger.debug('TOS')
|
||||
tos = server.get_terms_of_service()
|
||||
except ErrorConnectingServer as e:
|
||||
self._termsAndConditionsError = _('Error connecting to server')
|
||||
self.termsAndConditionsErrorChanged.emit()
|
||||
except Exception as e:
|
||||
self._termsAndConditionsError = '%s: %s' % (_('Error'), repr(e))
|
||||
self.termsAndConditionsErrorChanged.emit()
|
||||
else:
|
||||
self._termsAndConditions = tos
|
||||
self.termsAndConditionsChanged.emit()
|
||||
finally:
|
||||
self._busy = False
|
||||
self.busyChanged.emit()
|
||||
|
||||
self._busy = True
|
||||
self.busyChanged.emit()
|
||||
t = threading.Thread(target=fetch_task)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def createKeystore(self, email):
|
||||
xprv1, xpub1, xpub2, xpub3, short_id = self.plugin.create_keys()
|
||||
def create_remote_key_task():
|
||||
try:
|
||||
self.plugin.logger.debug('create remote key')
|
||||
r = server.create(xpub1, xpub2, email)
|
||||
|
||||
otp_secret = r['otp_secret']
|
||||
_xpub3 = r['xpubkey_cosigner']
|
||||
_id = r['id']
|
||||
except (socket.error, ErrorConnectingServer):
|
||||
self._createRemoteKeyError = _('Error creating key')
|
||||
self.createRemoteKeyErrorChanged.emit()
|
||||
except TrustedCoinException as e:
|
||||
# if e.status_code == 409: TODO ?
|
||||
# r = None
|
||||
self._createRemoteKeyError = str(e)
|
||||
self.createRemoteKeyErrorChanged.emit()
|
||||
except (KeyError,TypeError) as e: # catch any assumptions
|
||||
self._createRemoteKeyError = str(e)
|
||||
self.createRemoteKeyErrorChanged.emit()
|
||||
else:
|
||||
if short_id != _id:
|
||||
self._createRemoteKeyError = "unexpected trustedcoin short_id: expected {}, received {}".format(short_id, _id)
|
||||
self.createRemoteKeyErrorChanged.emit()
|
||||
return
|
||||
if xpub3 != _xpub3:
|
||||
self._createRemoteKeyError = "unexpected trustedcoin xpub3: expected {}, received {}".format(xpub3, _xpub3)
|
||||
self.createRemoteKeyErrorChanged.emit()
|
||||
return
|
||||
self._otpSecret = otp_secret
|
||||
self.otpSecretChanged.emit()
|
||||
self._shortId = short_id
|
||||
self.shortIdChanged.emit()
|
||||
finally:
|
||||
self._busy = False
|
||||
self.busyChanged.emit()
|
||||
|
||||
self._busy = True
|
||||
self.busyChanged.emit()
|
||||
t = threading.Thread(target=create_remote_key_task)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
def checkOtp(self, short_id, otp):
|
||||
def check_otp_task():
|
||||
try:
|
||||
self.plugin.logger.debug(f'check OTP, shortId={short_id}, otp={otp}')
|
||||
server.auth(short_id, otp)
|
||||
except TrustedCoinException as e:
|
||||
if e.status_code == 400: # invalid OTP
|
||||
self.plugin.logger.debug('Invalid one-time password.')
|
||||
self.otpError.emit()
|
||||
else:
|
||||
self.plugin.logger.error(str(e))
|
||||
self._createRemoteKeyError = str(e)
|
||||
self.createRemoteKeyErrorChanged.emit()
|
||||
except Exception as e:
|
||||
self.plugin.logger.error(str(e))
|
||||
self._createRemoteKeyError = str(e)
|
||||
self.createRemoteKeyErrorChanged.emit()
|
||||
else:
|
||||
self.plugin.logger.debug('OTP verify success')
|
||||
self.otpSuccess.emit()
|
||||
finally:
|
||||
self._busy = False
|
||||
self.busyChanged.emit()
|
||||
|
||||
self._busy = True
|
||||
self.busyChanged.emit()
|
||||
t = threading.Thread(target=check_otp_task)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
@hook
|
||||
def load_wallet(self, wallet: 'Abstract_Wallet'):
|
||||
if not isinstance(wallet, self.wallet_class):
|
||||
return
|
||||
self.logger.debug(f'plugin enabled for wallet "{str(wallet)}"')
|
||||
#wallet.handler_2fa = HandlerTwoFactor(self, window)
|
||||
if wallet.can_sign_without_server():
|
||||
self.so._canSignWithoutServer = True
|
||||
self.so.canSignWithoutServerChanged.emit()
|
||||
|
||||
msg = ' '.join([
|
||||
_('This wallet was restored from seed, and it contains two master private keys.'),
|
||||
_('Therefore, two-factor authentication is disabled.')
|
||||
])
|
||||
#action = lambda: window.show_message(msg)
|
||||
#else:
|
||||
#action = partial(self.settings_dialog, window)
|
||||
#button = StatusBarButton(read_QIcon("trustedcoin-status.png"),
|
||||
#_("TrustedCoin"), action)
|
||||
#window.statusBar().addPermanentWidget(button)
|
||||
self.start_request_thread(wallet)
|
||||
|
||||
@hook
|
||||
def init_qml(self, gui: 'ElectrumGui'):
|
||||
self.logger.debug(f'init_qml hook called, gui={str(type(gui))}')
|
||||
self._app = gui.app
|
||||
# important: QSignalObject needs to be parented, as keeping a ref
|
||||
# in the plugin is not enough to avoid gc
|
||||
self.so = Plugin.QSignalObject(self, self._app)
|
||||
|
||||
# extend wizard
|
||||
self.extend_wizard()
|
||||
|
||||
def extend_wizard(self):
|
||||
wizard = self._app.daemon.newWalletWizard
|
||||
self.logger.debug(repr(wizard))
|
||||
views = {
|
||||
'trustedcoin_start': {
|
||||
'gui': '../../../../plugins/trustedcoin/qml/Disclaimer',
|
||||
'next': 'trustedcoin_choose_seed'
|
||||
},
|
||||
'trustedcoin_choose_seed': {
|
||||
'gui': '../../../../plugins/trustedcoin/qml/ChooseSeed',
|
||||
'next': self.on_choose_seed
|
||||
},
|
||||
'trustedcoin_create_seed': {
|
||||
'gui': 'WCCreateSeed',
|
||||
'next': 'trustedcoin_confirm_seed'
|
||||
},
|
||||
'trustedcoin_confirm_seed': {
|
||||
'gui': 'WCConfirmSeed',
|
||||
'next': 'trustedcoin_tos_email'
|
||||
},
|
||||
'trustedcoin_have_seed': {
|
||||
'gui': 'WCHaveSeed',
|
||||
'next': 'trustedcoin_tos_email'
|
||||
},
|
||||
'trustedcoin_tos_email': {
|
||||
'gui': '../../../../plugins/trustedcoin/qml/Terms',
|
||||
'next': 'trustedcoin_show_confirm_otp'
|
||||
},
|
||||
'trustedcoin_show_confirm_otp': {
|
||||
'gui': '../../../../plugins/trustedcoin/qml/ShowConfirmOTP',
|
||||
'accept': self.on_accept_otp_secret,
|
||||
'next': 'wallet_password',
|
||||
'last': wizard.last_if_single_password
|
||||
}
|
||||
}
|
||||
wizard.navmap_merge(views)
|
||||
|
||||
def on_choose_seed(self, wizard_data):
|
||||
self.logger.debug('on_choose_seed')
|
||||
if wizard_data['keystore_type'] == 'createseed':
|
||||
return 'trustedcoin_create_seed'
|
||||
else:
|
||||
return 'trustedcoin_have_seed'
|
||||
|
||||
# combined create_keystore and create_remote_key pre
|
||||
def create_keys(self):
|
||||
wizard = self._app.daemon.newWalletWizard
|
||||
wizard_data = wizard._current.wizard_data
|
||||
|
||||
xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(wizard_data['seed'], wizard_data['seed_extra_words'])
|
||||
|
||||
# NOTE: at this point, old style wizard creates a wallet file (w. password if set) and
|
||||
# stores the keystores and wizard state, in order to separate offline seed creation
|
||||
# and online retrieval of the OTP secret. For mobile, we don't do this, but
|
||||
# for desktop the wizard should support this usecase.
|
||||
|
||||
data = {'x1/': {'xpub': xpub1}, 'x2/': {'xpub': xpub2}}
|
||||
|
||||
# Generate third key deterministically.
|
||||
long_user_id, short_id = get_user_id(data)
|
||||
xtype = xpub_type(xpub1)
|
||||
xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id)
|
||||
|
||||
return (xprv1,xpub1,xpub2,xpub3,short_id)
|
||||
|
||||
def on_accept_otp_secret(self, wizard_data):
|
||||
self.logger.debug('on accept otp: ' + repr(wizard_data))
|
||||
|
||||
xprv1,xpub1,xpub2,xpub3,short_id = self.create_keys()
|
||||
|
||||
k1 = keystore.from_xprv(xprv1)
|
||||
k2 = keystore.from_xpub(xpub2)
|
||||
k3 = keystore.from_xpub(xpub3)
|
||||
|
||||
wizard_data['x1/'] = k1.dump()
|
||||
wizard_data['x2/'] = k2.dump()
|
||||
wizard_data['x3/'] = k3.dump()
|
||||
# wizard_data['use_trustedcoin'] = True
|
||||
|
||||
|
||||
|
||||
# wizard
|
||||
def request_otp_dialog(self, wizard, short_id, otp_secret, xpub3):
|
||||
f = lambda otp, reset: self.check_otp(wizard, short_id, otp_secret, xpub3, otp, reset)
|
||||
wizard.otp_dialog(otp_secret=otp_secret, run_next=f)
|
||||
|
||||
# regular wallet prompt function
|
||||
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
|
||||
self.logger.debug('prompt_user_for_otp')
|
||||
self.on_success = on_success
|
||||
self.on_failure = on_failure
|
||||
self.wallet = wallet
|
||||
self.tx = tx
|
||||
self.so.requestView.emit('../../../../plugins/trustedcoin/qml/OTP.qml')
|
||||
|
||||
def on_otp(self, otp):
|
||||
try:
|
||||
self.wallet.on_otp(self.tx, otp)
|
||||
except TrustedCoinException as e:
|
||||
if e.status_code == 400: # invalid OTP
|
||||
# Clock.schedule_once(lambda dt:
|
||||
self.on_failure(_('Invalid one-time password.'))
|
||||
# )
|
||||
else:
|
||||
# Clock.schedule_once(lambda dt, bound_e=e:
|
||||
self.on_failure(_('Error') + ':\n' + str(bound_e))
|
||||
# )
|
||||
except Exception as e:
|
||||
# Clock.schedule_once(lambda dt, bound_e=e:
|
||||
self.on_failure(_('Error') + ':\n' + str(bound_e))
|
||||
# )
|
||||
else:
|
||||
self.on_success(tx)
|
||||
38
electrum/plugins/trustedcoin/qml/ChooseSeed.qml
Normal file
38
electrum/plugins/trustedcoin/qml/ChooseSeed.qml
Normal file
@@ -0,0 +1,38 @@
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Layouts 1.0
|
||||
import QtQuick.Controls 2.1
|
||||
|
||||
import "../../../gui/qml/components/wizard"
|
||||
|
||||
WizardComponent {
|
||||
valid: keystoregroup.checkedButton !== null
|
||||
|
||||
onAccept: {
|
||||
wizard_data['keystore_type'] = keystoregroup.checkedButton.keystoretype
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
id: keystoregroup
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
Label {
|
||||
text: qsTr('Do you want to create a new seed, or restore a wallet using an existing seed?')
|
||||
Layout.preferredWidth: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
RadioButton {
|
||||
ButtonGroup.group: keystoregroup
|
||||
property string keystoretype: 'createseed'
|
||||
checked: true
|
||||
text: qsTr('Create a new seed')
|
||||
}
|
||||
RadioButton {
|
||||
ButtonGroup.group: keystoregroup
|
||||
property string keystoretype: 'haveseed'
|
||||
text: qsTr('I already have a seed')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
electrum/plugins/trustedcoin/qml/Disclaimer.qml
Normal file
27
electrum/plugins/trustedcoin/qml/Disclaimer.qml
Normal file
@@ -0,0 +1,27 @@
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Layouts 1.0
|
||||
import QtQuick.Controls 2.1
|
||||
|
||||
import org.electrum 1.0
|
||||
|
||||
import "../../../gui/qml/components/wizard"
|
||||
|
||||
WizardComponent {
|
||||
valid: true
|
||||
|
||||
property QtObject plugin
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
Layout.preferredWidth: parent.width
|
||||
text: plugin ? plugin.disclaimer : ''
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
plugin = AppController.plugin('trustedcoin')
|
||||
}
|
||||
}
|
||||
46
electrum/plugins/trustedcoin/qml/Settings.qml
Normal file
46
electrum/plugins/trustedcoin/qml/Settings.qml
Normal file
@@ -0,0 +1,46 @@
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Layouts 1.0
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Controls.Material 2.0
|
||||
|
||||
import org.electrum 1.0
|
||||
|
||||
//import "controls"
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: rootLayout.height
|
||||
|
||||
property QtObject plugin
|
||||
|
||||
RowLayout {
|
||||
id: rootLayout
|
||||
Button {
|
||||
text: 'Force upload'
|
||||
enabled: !plugin.busy
|
||||
onClicked: plugin.upload()
|
||||
}
|
||||
Button {
|
||||
text: 'Force download'
|
||||
enabled: !plugin.busy
|
||||
onClicked: plugin.download()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: plugin
|
||||
function onUploadSuccess() {
|
||||
console.log('upload success')
|
||||
}
|
||||
function onUploadFailed() {
|
||||
console.log('upload failed')
|
||||
}
|
||||
function onDownloadSuccess() {
|
||||
console.log('download success')
|
||||
}
|
||||
function onDownloadFailed() {
|
||||
console.log('download failed')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
101
electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml
Normal file
101
electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml
Normal file
@@ -0,0 +1,101 @@
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Layouts 1.0
|
||||
import QtQuick.Controls 2.1
|
||||
|
||||
import "../../../gui/qml/components/wizard"
|
||||
import "../../../gui/qml/components/controls"
|
||||
|
||||
WizardComponent {
|
||||
valid: otpVerified
|
||||
|
||||
property QtObject plugin
|
||||
|
||||
property bool otpVerified: false
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: qsTr('Authenticator secret')
|
||||
}
|
||||
|
||||
InfoTextArea {
|
||||
iconStyle: InfoTextArea.IconStyle.Error
|
||||
visible: plugin ? plugin.createRemoteKeyError : false
|
||||
text: plugin ? plugin.createRemoteKeyError : ''
|
||||
}
|
||||
|
||||
QRImage {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
qrdata: encodeURI('otpauth://totp/Electrum 2FA ' + wizard_data['wallet_name']
|
||||
+ '?secret=' + plugin.otpSecret + '&digits=6')
|
||||
render: plugin ? plugin.otpSecret : false
|
||||
}
|
||||
|
||||
TextHighlightPane {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: plugin.otpSecret
|
||||
Label {
|
||||
text: plugin.otpSecret
|
||||
font.family: FixedFont
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.preferredWidth: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
text: qsTr('Enter or scan into authenticator app. Then authenticate below')
|
||||
visible: plugin.otpSecret && !otpVerified
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: otp_auth
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
focus: true
|
||||
visible: plugin.otpSecret && !otpVerified
|
||||
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhDigitsOnly
|
||||
font.family: FixedFont
|
||||
font.pixelSize: constants.fontSizeLarge
|
||||
onTextChanged: {
|
||||
if (text.length >= 6) {
|
||||
plugin.checkOtp(plugin.shortId, otp_auth.text)
|
||||
text = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
source: '../../../gui/icons/confirmed.png'
|
||||
visible: otpVerified
|
||||
Layout.preferredWidth: constants.iconSizeLarge
|
||||
Layout.preferredHeight: constants.iconSizeLarge
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
visible: plugin ? plugin.busy : false
|
||||
running: visible
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
plugin = AppController.plugin('trustedcoin')
|
||||
plugin.createKeystore(wizard_data['2fa_email'])
|
||||
otp_auth.forceActiveFocus()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: plugin
|
||||
function onOtpError() {
|
||||
console.log('OTP verify error')
|
||||
// TODO: show error in UI
|
||||
}
|
||||
function onOtpSuccess() {
|
||||
console.log('OTP verify success')
|
||||
otpVerified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
67
electrum/plugins/trustedcoin/qml/Terms.qml
Normal file
67
electrum/plugins/trustedcoin/qml/Terms.qml
Normal file
@@ -0,0 +1,67 @@
|
||||
import QtQuick 2.6
|
||||
import QtQuick.Layouts 1.0
|
||||
import QtQuick.Controls 2.1
|
||||
|
||||
import org.electrum 1.0
|
||||
|
||||
import "../../../gui/qml/components/wizard"
|
||||
import "../../../gui/qml/components/controls"
|
||||
|
||||
WizardComponent {
|
||||
valid: !plugin ? false
|
||||
: email.text.length > 0 // TODO: validate email address
|
||||
&& plugin.termsAndConditions
|
||||
|
||||
property QtObject plugin
|
||||
|
||||
onAccept: {
|
||||
wizard_data['2fa_email'] = email.text
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Label { text: qsTr('Terms and conditions') }
|
||||
|
||||
TextHighlightPane {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
rightPadding: 0
|
||||
|
||||
Flickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: termsText.height
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Label {
|
||||
id: termsText
|
||||
width: parent.width
|
||||
rightPadding: constants.paddingSmall
|
||||
wrapMode: Text.Wrap
|
||||
text: plugin ? plugin.termsAndConditions : ''
|
||||
}
|
||||
ScrollIndicator.vertical: ScrollIndicator { }
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
visible: plugin ? plugin.busy : false
|
||||
running: visible
|
||||
}
|
||||
}
|
||||
|
||||
Label { text: qsTr('Email') }
|
||||
|
||||
TextField {
|
||||
id: email
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr('Enter your email address')
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
plugin = AppController.plugin('trustedcoin')
|
||||
plugin.fetchTermsAndConditions()
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ def get_billing_xpub():
|
||||
return "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU"
|
||||
|
||||
|
||||
DISCLAIMER = [
|
||||
DESKTOP_DISCLAIMER = [
|
||||
_("Two-factor authentication is a service provided by TrustedCoin. "
|
||||
"It uses a multi-signature wallet, where you own 2 of 3 keys. "
|
||||
"The third key is stored on a remote server that signs transactions on "
|
||||
@@ -86,8 +86,9 @@ DISCLAIMER = [
|
||||
"To be safe from malware, you may want to do this on an offline "
|
||||
"computer, and move your wallet later to an online computer."),
|
||||
]
|
||||
DISCLAIMER = DESKTOP_DISCLAIMER
|
||||
|
||||
KIVY_DISCLAIMER = [
|
||||
MOBILE_DISCLAIMER = [
|
||||
_("Two-factor authentication is a service provided by TrustedCoin. "
|
||||
"To use it, you must have a separate device with Google Authenticator."),
|
||||
_("This service uses a multi-signature wallet, where you own 2 of 3 keys. "
|
||||
@@ -98,6 +99,8 @@ KIVY_DISCLAIMER = [
|
||||
"your funds at any time and at no cost, without the remote server, by "
|
||||
"using the 'restore wallet' option with your wallet seed."),
|
||||
]
|
||||
KIVY_DISCLAIMER = MOBILE_DISCLAIMER
|
||||
|
||||
RESTORE_MSG = _("Enter the seed for your 2-factor wallet:")
|
||||
|
||||
class TrustedCoinException(Exception):
|
||||
|
||||
Reference in New Issue
Block a user