From 802c316edb793c44e2577d5a5ea6dc450ab15d50 Mon Sep 17 00:00:00 2001 From: f321x Date: Wed, 7 May 2025 09:50:42 +0200 Subject: [PATCH] qt: show terms of use as first window on setup --- electrum/gui/messages.py | 10 ++++ electrum/gui/qt/__init__.py | 14 +++++ electrum/gui/qt/wizard/server_connect.py | 45 +++++--------- electrum/gui/qt/wizard/terms_of_use.py | 76 ++++++++++++++++++++++++ electrum/simple_config.py | 1 + electrum/wizard.py | 29 +++++++++ 6 files changed, 145 insertions(+), 30 deletions(-) create mode 100644 electrum/gui/qt/wizard/terms_of_use.py diff --git a/electrum/gui/messages.py b/electrum/gui/messages.py index 2b3c573ae..76737cc85 100644 --- a/electrum/gui/messages.py +++ b/electrum/gui/messages.py @@ -78,3 +78,13 @@ MSG_LN_UTXO_RESERVE = ( _("You do not have enough on-chain funds to protect your Lightning channels.") + ' ' + _("You should have at least {} on-chain in order to be able to sweep channel outputs.") ) + +# not to be translated +MSG_TERMS_OF_USE = """ +1. Electrum is distributed under the MIT licence by Electrum Technologies GmbH. Most notably, this means that the Electrum software is provided as is, and that it comes without warranty. + +2. We are neither a bank nor a financial service provider. In addition, we do not not store user account data, and we are not an intermediary in the interaction between our software and the Bitcoin blockchain. Therefore, we do not have the possibility to freeze funds or to undo a fraudulent transaction. + +3. We do not provide private user support. All issue resolutions are public, and take place on Github or public forums. If someone posing as 'Electrum support' proposes to help you via a private channel, this person is most likely an imposter trying to steal your bitcoins. +""" + diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 891a5ba31..c0d673d1d 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -500,6 +500,19 @@ class ElectrumGui(BaseElectrumGui, Logger): window.close() self._create_window_for_wallet(wallet) + def ask_terms_of_use(self): + """Ask the user to accept the terms of use. + This is only shown if the user has not accepted them yet. + """ + if self.config.TERMS_OF_USE_ACCEPTED: + return + from electrum.gui.qt.wizard.terms_of_use import QETermsOfUseWizard + dialog = QETermsOfUseWizard(self.config, self.app) + result = dialog.exec() + if result == QDialog.DialogCode.Rejected: + self.logger.info('terms of use not accepted by user') + raise UserCancelled() + def init_network(self): """Start the network, including showing a first-start network dialog if config does not exist.""" if self.daemon.network: @@ -524,6 +537,7 @@ class ElectrumGui(BaseElectrumGui, Logger): Exception_Hook.maybe_setup(config=self.config) # start network, and maybe show first-start network-setup try: + self.ask_terms_of_use() self.init_network() except UserCancelled: return diff --git a/electrum/gui/qt/wizard/server_connect.py b/electrum/gui/qt/wizard/server_connect.py index ff8412e6c..2cf3cdd02 100644 --- a/electrum/gui/qt/wizard/server_connect.py +++ b/electrum/gui/qt/wizard/server_connect.py @@ -27,7 +27,7 @@ class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard): # attach gui classes self.navmap_merge({ - 'welcome': {'gui': WCWelcome, 'params': {'icon': ''}}, + 'welcome': {'gui': WCWelcome}, 'proxy_config': {'gui': WCProxyConfig}, 'server_config': {'gui': WCServerConfig}, }) @@ -35,31 +35,22 @@ class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard): class WCWelcome(WizardComponent): def __init__(self, parent, wizard): - WizardComponent.__init__(self, parent, wizard, title='') + WizardComponent.__init__(self, parent, wizard, title='Network Configuration') self.wizard_title = _('Electrum Bitcoin Wallet') - self.use_advanced_w = QCheckBox(_('Advanced network settings')) - self.use_advanced_w.setChecked(False) - self.use_advanced_w.stateChanged.connect(self.on_advanced_changed) - self.img_label = QLabel() - pixmap = QPixmap(icon_path('electrum_darkblue_1.png')) - self.img_label.setPixmap(pixmap) - self.img_label2 = QLabel() - pixmap = QPixmap(icon_path('electrum_text.png')) - self.img_label2.setPixmap(pixmap) - hbox_img = QHBoxLayout() - hbox_img.addStretch(1) - hbox_img.addWidget(self.img_label) - hbox_img.addWidget(self.img_label2) - hbox_img.addStretch(1) + self.help_label = QLabel() + self.help_label.setText("\n".join([ + _("Optional settings to customize your network connection."), + _("If you are unsure what this is, leave them unchecked and Electrum will automatically " + "select servers."), + ])) + self.help_label.setWordWrap(True) self.config_proxy_w = QCheckBox(_('Configure Proxy')) self.config_proxy_w.setChecked(False) - self.config_proxy_w.setVisible(False) self.config_proxy_w.stateChanged.connect(self.on_updated) self.config_server_w = QCheckBox(_('Select Server')) self.config_server_w.setChecked(False) - self.config_server_w.setVisible(False) self.config_server_w.stateChanged.connect(self.on_updated) options_w = QWidget() vbox = QVBoxLayout() @@ -68,21 +59,15 @@ class WCWelcome(WizardComponent): vbox.addStretch(1) options_w.setLayout(vbox) - self.layout().addLayout(hbox_img) - self.layout().addSpacing(50) - self.layout().addWidget(self.use_advanced_w, False, Qt.AlignmentFlag.AlignHCenter) - self.layout().addWidget(options_w, False, Qt.AlignmentFlag.AlignHCenter) + self.layout().addWidget(self.help_label) + self.layout().addSpacing(30) + self.layout().addWidget(options_w, False, Qt.AlignmentFlag.AlignLeft) self._valid = True - def on_advanced_changed(self): - self.config_proxy_w.setVisible(self.use_advanced_w.isChecked()) - self.config_server_w.setVisible(self.use_advanced_w.isChecked()) - self.on_updated() - def apply(self): - self.wizard_data['use_defaults'] = not self.use_advanced_w.isChecked() - self.wizard_data['want_proxy'] = self.use_advanced_w.isChecked() and self.config_proxy_w.isChecked() - self.wizard_data['autoconnect'] = not self.use_advanced_w.isChecked() or not self.config_server_w.isChecked() + self.wizard_data['use_defaults'] = not (self.config_server_w.isChecked() or self.config_proxy_w.isChecked()) + self.wizard_data['want_proxy'] = self.config_proxy_w.isChecked() + self.wizard_data['autoconnect'] = not self.config_server_w.isChecked() class WCProxyConfig(WizardComponent): diff --git a/electrum/gui/qt/wizard/terms_of_use.py b/electrum/gui/qt/wizard/terms_of_use.py new file mode 100644 index 000000000..449cdfe59 --- /dev/null +++ b/electrum/gui/qt/wizard/terms_of_use.py @@ -0,0 +1,76 @@ +from typing import TYPE_CHECKING + +from PyQt6.QtCore import QTimer +from PyQt6.QtGui import QPixmap +from PyQt6.QtWidgets import QLabel, QHBoxLayout, QScrollArea + +from electrum.i18n import _ +from electrum.wizard import TermsOfUseWizard +from electrum.gui.qt.util import icon_path +from electrum.gui import messages +from .wizard import QEAbstractWizard, WizardComponent + +if TYPE_CHECKING: + from electrum.simple_config import SimpleConfig + from electrum.gui.qt import QElectrumApplication + + +class QETermsOfUseWizard(TermsOfUseWizard, QEAbstractWizard): + def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication'): + TermsOfUseWizard.__init__(self, config) + QEAbstractWizard.__init__(self, config, app) + self.window_title = _('Terms of Use') + self.finish_label = _('I Accept') + self.title.setVisible(False) + # self.window().setMinimumHeight(565) # Enough to show the whole text without scrolling + self.next_button.setToolTip("You accept the Terms of Use by clicking this button.") + + # attach gui classes + self.navmap_merge({ + 'terms_of_use': {'gui': WCTermsOfUseScreen, 'params': {'icon': ''}}, + }) + +class WCTermsOfUseScreen(WizardComponent): + def __init__(self, parent, wizard): + WizardComponent.__init__(self, parent, wizard, title='') + self.wizard_title = _('Electrum Terms of Use') + self.img_label = QLabel() + pixmap = QPixmap(icon_path('electrum_darkblue_1.png')) + self.img_label.setPixmap(pixmap) + self.img_label2 = QLabel() + pixmap = QPixmap(icon_path('electrum_text.png')) + self.img_label2.setPixmap(pixmap) + hbox_img = QHBoxLayout() + hbox_img.addStretch(1) + hbox_img.addWidget(self.img_label) + hbox_img.addWidget(self.img_label2) + hbox_img.addStretch(1) + + self.layout().addLayout(hbox_img) + + self.tos_label = QLabel() + self.tos_label.setText(messages.MSG_TERMS_OF_USE) + self.tos_label.setWordWrap(True) + self.layout().addWidget(self.tos_label) + self._valid = False + + # Find the scroll area and connect to its scrollbar + QTimer.singleShot(0, self.check_scroll_position) + + def check_scroll_position(self): + # Find the scroll area + scroll_area = self.window().findChild(QScrollArea) + if scroll_area and scroll_area.verticalScrollBar(): + scrollbar = scroll_area.verticalScrollBar() + def on_scroll_change(value): + if value >= scrollbar.maximum() - 5: # Allow 5 pixel margin + self._valid = True + self.on_updated() + scrollbar.valueChanged.connect(on_scroll_change) + else: + # Fallback if the scroll area is not detected + self._valid = True + self.on_updated() + + def apply(self): + pass diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 767e4027d..6e5946bd6 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -847,6 +847,7 @@ Warning: setting this to too low will result in lots of payment failures."""), QR_READER_FLIP_X = ConfigVar('qrreader_flip_x', default=True, type_=bool) WIZARD_DONT_CREATE_SEGWIT = ConfigVar('nosegwit', default=False, type_=bool) CONFIG_FORGET_CHANGES = ConfigVar('forget_config', default=False, type_=bool) + TERMS_OF_USE_ACCEPTED = ConfigVar('terms_of_use_accepted', default=False, type_=bool) # connect to remote submarine swap server SWAPSERVER_URL = ConfigVar('swapserver_url', default='', type_=str) diff --git a/electrum/wizard.py b/electrum/wizard.py index 2f3c2e4fa..18f9a50da 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -20,6 +20,7 @@ if TYPE_CHECKING: from electrum.daemon import Daemon from electrum.plugin import Plugins from electrum.keystore import Hardware_KeyStore + from electrum.simple_config import SimpleConfig class WizardViewState(NamedTuple): @@ -766,3 +767,31 @@ class ServerConnectWizard(AbstractWizard): params = self.navmap[start_view].get('params', {}) self._current = WizardViewState(start_view, initial_data, params) return self._current + + +class TermsOfUseWizard(AbstractWizard): + + _logger = get_logger(__name__) + + def __init__(self, config: 'SimpleConfig'): + AbstractWizard.__init__(self) + self._config = config + self.navmap = { + 'terms_of_use': { + 'accept': self.accept_terms_of_use, + 'last': True, + }, + } + + def accept_terms_of_use(self, _): + self._config.TERMS_OF_USE_ACCEPTED = True + + def start(self, initial_data: dict = None) -> WizardViewState: + if initial_data is None: + initial_data = {} + self.reset() + start_view = 'terms_of_use' + params = self.navmap[start_view].get('params', {}) + self._current = WizardViewState(start_view, initial_data, params) + return self._current +