update: updated billing page, pagedataworker, forced_sync

This commit is contained in:
JOhn 2026-07-02 17:12:23 -04:00
parent 7910586e1a
commit 91e657a06d
11 changed files with 168 additions and 123 deletions

View file

@ -10,7 +10,7 @@ import traceback
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton,
QDialog, QVBoxLayout, QHBoxLayout
QDialog, QVBoxLayout, QHBoxLayout, QMessageBox
)
from PyQt6.QtGui import QPixmap, QFont, QFontDatabase
from PyQt6 import QtGui
@ -61,6 +61,7 @@ from gui.v2.actions.ticket_failure import (
clear_ticket_verification_failure,
)
from gui.v2.actions.should_be_synchronized import should_be_synchronized
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
class CustomWindow(QMainWindow):
@ -192,6 +193,23 @@ class CustomWindow(QMainWindow):
else:
self.navigator.navigate("menu")
self.navigator.start_preload()
if self.force_sync:
QTimer.singleShot(0, self._prompt_force_sync)
def _prompt_force_sync(self):
connection = ConfigurationController.get_connection()
box = QMessageBox(self)
box.setWindowTitle("Data Update Needed")
box.setText(
"Please download the new data to avoid errors. You have profiles that need the data to function.\n\n"
f"Connection type: {connection}")
style_message_box(box)
sync_button = box.addButton("Sync", QMessageBox.ButtonRole.AcceptRole)
mark_confirm_button(sync_button)
box.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
box.exec()
if box.clickedButton() == sync_button:
self.sync()
def perform_update_check(self):
from core.controllers.ClientController import ClientController

View file

@ -157,10 +157,16 @@ class Navigator(QObject):
else:
self._maybe_emit_preload_summary()
def _prepare_page_data(self, name):
module_path, class_name = PAGE_REGISTRY[name]
module = importlib.import_module(module_path)
cls = getattr(module, class_name)
return cls.prepare_data(self.custom_window)
def _dispatch_async_preload(self, name):
module_path, class_name = PAGE_REGISTRY[name]
print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True)
worker = PageDataWorker(name, module_path, class_name, self.custom_window)
worker = PageDataWorker(name, self._prepare_page_data, name)
worker.data_ready.connect(self._on_preload_data_ready)
worker.failed.connect(self._on_preload_failed)
worker.finished.connect(lambda n=name: self._cleanup_preload_worker(n))

View file

@ -16,6 +16,7 @@ from gui.v2.ui.pages.browser_page import BrowserPage
from gui.v2.ui.pages.location_page import LocationPage
from gui.v2.ui.pages.screen_page import ScreenPage
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
from gui.v2.workers.page_data_worker import PageDataWorker
class EditorPage(Page):
@ -80,6 +81,13 @@ class EditorPage(Page):
self.brow_disp.hide()
self.brow_disp.lower()
self._prefetched_profiles = None
self._editor_workers = set()
@staticmethod
def prepare_data(custom_window):
return ProfileController.get_all()
def _selected_profiles_str(self):
menu_page = self.find_menu_page()
if menu_page is not None and menu_page.selected_profiles:
@ -117,8 +125,28 @@ class EditorPage(Page):
def showEvent(self, event):
super().showEvent(event)
self.res_hint_shown = False
worker = PageDataWorker("editor", EditorPage.prepare_data, self.custom_window)
worker.data_ready.connect(
lambda name, profiles: self._on_editor_data_ready(profiles))
worker.failed.connect(
lambda name, error: self._on_editor_data_failed(error))
worker.finished.connect(
lambda w=worker: self._cleanup_editor_worker(w))
self._editor_workers.add(worker)
worker.start()
def _on_editor_data_ready(self, profiles):
self._prefetched_profiles = profiles
self.extraccion()
def _on_editor_data_failed(self, error):
self.update_status.update_status(f"Editor data load failed: {error}")
self.extraccion()
def _cleanup_editor_worker(self, worker):
self._editor_workers.discard(worker)
worker.deleteLater()
def extraccion(self):
self.data_profile = {}
for label in self.labels:
@ -131,6 +159,10 @@ class EditorPage(Page):
selected_profiles_str = self._selected_profiles_str()
menu_page = self.find_menu_page()
if menu_page:
if self._prefetched_profiles is not None:
new_profiles = self._prefetched_profiles
self._prefetched_profiles = None
else:
new_profiles = ProfileController.get_all()
self.profiles_data = menu_page.match_core_profiles(
profiles_dict=new_profiles)

View file

@ -15,6 +15,7 @@ from gui.v2.ui.pages.Page import Page
from gui.v2.ui.pages.browser_page import BrowserPage
from gui.v2.ui.pages.location_page import LocationPage
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
from gui.v2.workers.page_data_worker import PageDataWorker
from gui.v2.workers.worker_thread import WorkerThread
@ -73,9 +74,15 @@ class FastRegistrationPage(Page):
'resolution': '1024x760'
}
self.res_index = 1
self._prefetched_profiles = None
self._fast_reg_workers = set()
self.initialize_default_selections()
@staticmethod
def prepare_data(custom_window):
return ProfileController.get_all()
def initialize_default_selections(self):
if not self.selected_values['location']:
locations = self.connection_manager.get_location_list()
@ -107,6 +114,24 @@ class FastRegistrationPage(Page):
super().showEvent(event)
self.initialize_default_selections()
self.create_interface_elements()
self._start_profiles_prefetch()
def _start_profiles_prefetch(self):
worker = PageDataWorker(
"fast_registration", FastRegistrationPage.prepare_data, self.custom_window)
worker.data_ready.connect(
lambda name, profiles: self._on_profiles_prefetched(profiles))
worker.finished.connect(
lambda w=worker: self._cleanup_fast_reg_worker(w))
self._fast_reg_workers.add(worker)
worker.start()
def _on_profiles_prefetched(self, profiles):
self._prefetched_profiles = profiles
def _cleanup_fast_reg_worker(self, worker):
self._fast_reg_workers.discard(worker)
worker.deleteLater()
def create_interface_elements(self):
for label in self.labels:
@ -728,7 +753,7 @@ class FastRegistrationPage(Page):
self.update_status.update_status('Invalid location selected')
return
profiles = ProfileController.get_all()
profiles = self._prefetched_profiles if self._prefetched_profiles is not None else ProfileController.get_all()
profile_id = self.get_next_available_profile_id(profiles)
profile_data_for_resume = {
'id': profile_id,
@ -762,7 +787,7 @@ class FastRegistrationPage(Page):
connection_type = 'tor' if profile_data['connection'] == 'tor' else 'system'
profiles = ProfileController.get_all()
profiles = self._prefetched_profiles if self._prefetched_profiles is not None else ProfileController.get_all()
profile_id = self.get_next_available_profile_id(profiles)
profile_data_for_resume = {
'id': profile_id,

View file

@ -2,13 +2,16 @@ import os
import re
from PyQt6.QtWidgets import (
QButtonGroup, QFrame, QLabel, QPushButton, QTextEdit,
QButtonGroup, QFrame, QLabel, QMessageBox, QPushButton, QTextEdit,
)
from PyQt6.QtGui import QIcon, QPixmap
from PyQt6.QtCore import Qt
from PyQt6 import QtCore
from core.services.payment_phase.do_we_have_billing_id import do_we_have_billing_id
from gui.v2.ui.pages.Page import Page
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
HOW_MANY_PROFILES_DEFAULT = 6
@ -150,11 +153,44 @@ class IdPage(Page):
self.custom_window.navigator.navigate("duration_selection")
def go_multiple_profiles(self):
billing_id = do_we_have_billing_id()
if billing_id:
self._prompt_existing_tickets(billing_id)
else:
self._continue_multiple_flow()
def _continue_multiple_flow(self):
self.custom_window.navigator.navigate("plan_picker")
plan_page = self.custom_window.navigator.get_cached("plan_picker")
if plan_page is not None:
plan_page.start_sync()
def _prompt_existing_tickets(self, billing_id):
box = QMessageBox(self)
box.setWindowTitle("Existing ticket purchase found")
box.setText(
"You already have a pending ticket purchase. Continue to check if it's paid, "
"or wipe it and start over?")
style_message_box(box)
check_button = box.addButton(
"Check payment", QMessageBox.ButtonRole.AcceptRole)
mark_confirm_button(check_button)
wipe_button = box.addButton(
"Wipe and start over", QMessageBox.ButtonRole.DestructiveRole)
box.exec()
clicked = box.clickedButton()
if clicked == check_button:
self._resume_existing_ticket(billing_id)
elif clicked == wipe_button:
self._continue_multiple_flow()
def _resume_existing_ticket(self, billing_id):
self.update_status.update_status("Checking existing ticket payment...")
self.custom_window.navigator.navigate("payment_details")
payment_page = self.custom_window.navigator.get_cached("payment_details")
if payment_page is not None:
payment_page.resume_ticket_billing(billing_id)
def toggle_button_state(self):
text = self.text_edit.toPlainText()
if text.strip():

View file

@ -351,6 +351,16 @@ class PaymentDetailsPage(Page):
self.ticket_poll_timer.start(3000)
self.update_status.update_status('Awaiting ticket payment...')
def resume_ticket_billing(self, temp_billing_code):
self.ticketing = True
self.temp_billing_code = temp_billing_code
self.selected_currency = None
self.text_fields[0].setText(str(temp_billing_code or ''))
if self.ticket_poll_timer.isActive():
self.ticket_poll_timer.stop()
self.ticket_poll_timer.start(3000)
self.update_status.update_status('Checking existing ticket payment...')
def _populate_ticket_fields(self, invoice):
self.text_fields[0].setText(str(getattr(invoice, 'temp_billing_code', '') or ''))
self.text_fields[1].setText(self.selected_duration)

View file

@ -35,7 +35,6 @@ from core.errors.logger import logger as core_logger
from gui.v2.actions import settings_data
from gui.v2.actions.key_interpretation import interpret_key_results
from gui.v2.actions.profile_order import normalize_profile_order
from gui.v2.infrastructure.page_registry import PAGE_REGISTRY
from gui.v2.infrastructure.setup_observers import ticket_observer
from gui.v2.ui.pages.Page import Page
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
@ -103,7 +102,6 @@ class Settings(Page):
("Create/Edit", self.show_registrations_page),
("Verification", self.show_verification_page),
("Legacy-Version", self.show_systemwide_page),
("Bwrap Permission", self.show_bwrap_page),
("Delete Profile", self.show_delete_page),
("Error Logs", self.show_logs_page),
("Debug Help", self.show_debug_page)
@ -1176,9 +1174,8 @@ class Settings(Page):
super().showEvent(event)
self._settings_refresh_seq += 1
seq = self._settings_refresh_seq
module_path, class_name = PAGE_REGISTRY["settings"]
worker = PageDataWorker(
"settings", module_path, class_name, self.custom_window)
"settings", settings_data.prepare, self.custom_window.gui_config_file)
worker.data_ready.connect(
lambda name, payload, s=seq: self._on_settings_data_ready(payload, s))
worker.failed.connect(
@ -1226,10 +1223,6 @@ class Settings(Page):
self.systemwide_page = self.create_systemwide_page()
self.content_layout.addWidget(self.systemwide_page)
self.content_layout.removeWidget(self.bwrap_page)
self.bwrap_page = self.create_bwrap_page()
self.content_layout.addWidget(self.bwrap_page)
self.content_layout.removeWidget(self.delete_page)
self.delete_page = self.create_delete_page()
self.content_layout.addWidget(self.delete_page)
@ -1443,7 +1436,6 @@ class Settings(Page):
self.registrations_page = self.create_registrations_page()
self.verification_page = self.create_verification_page()
self.systemwide_page = self.create_systemwide_page()
self.bwrap_page = self.create_bwrap_page()
self.logs_page = self.create_logs_page()
self.delete_page = self.create_delete_page()
self.debug_page = self.create_debug_page()
@ -1454,7 +1446,6 @@ class Settings(Page):
self.content_layout.addWidget(self.registrations_page)
self.content_layout.addWidget(self.verification_page)
self.content_layout.addWidget(self.systemwide_page)
self.content_layout.addWidget(self.bwrap_page)
self.content_layout.addWidget(self.logs_page)
self.content_layout.addWidget(self.delete_page)
self.content_layout.addWidget(self.debug_page)
@ -1508,11 +1499,6 @@ class Settings(Page):
self.content_layout.setCurrentWidget(self.systemwide_page)
self._select_menu_button("Legacy-Version")
def show_bwrap_page(self):
core_logger.info("User navigated to Settings -> Bwrap Permission")
self.content_layout.setCurrentWidget(self.bwrap_page)
self._select_menu_button("Bwrap Permission")
def show_logs_page(self):
core_logger.info("User navigated to Settings -> Error Logs")
self.content_layout.setCurrentWidget(self.logs_page)
@ -1995,59 +1981,6 @@ class Settings(Page):
self._prepared_value("systemwide_enabled", settings_data.read_systemwide_enabled))
return page
def create_bwrap_page(self):
page = QWidget()
layout = QVBoxLayout(page)
layout.setSpacing(20)
layout.setContentsMargins(20, 20, 20, 20)
title = QLabel("BWRAP PERMISSION")
title.setStyleSheet(
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}")
layout.addWidget(title)
description = QLabel(
"Control whether HydraVeil configures a capability policy so bwrap can be used without requiring additional permissions.")
description.setWordWrap(True)
description.setStyleSheet(
f"color: white; font-size: 14px; {self.font_style}")
layout.addWidget(description)
status_layout = QHBoxLayout()
status_label = QLabel("Current status:")
status_label.setStyleSheet(
f"color: white; font-size: 14px; {self.font_style}")
self.bwrap_status_value = QLabel("")
self.bwrap_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
status_layout.addWidget(status_label)
status_layout.addWidget(self.bwrap_status_value)
status_layout.addStretch()
layout.addLayout(status_layout)
toggle_layout = QHBoxLayout()
self.bwrap_toggle = QCheckBox("Enable capability policy")
self.bwrap_toggle.setStyleSheet(self.get_checkbox_style())
toggle_layout.addWidget(self.bwrap_toggle)
toggle_layout.addStretch()
layout.addLayout(toggle_layout)
save_button = QPushButton()
save_button.setFixedSize(75, 46)
save_button.setIcon(QIcon(os.path.join(self.btn_path, "save.png")))
save_button.setIconSize(QSize(75, 46))
save_button.clicked.connect(self.save_bwrap_settings)
button_layout = QHBoxLayout()
button_layout.addWidget(save_button)
button_layout.addStretch()
layout.addLayout(button_layout)
layout.addStretch()
self.load_bwrap_settings(
self._prepared_value("bwrap_enabled", settings_data.read_bwrap_enabled))
return page
def load_systemwide_settings(self, enabled=None):
if enabled is None:
enabled = settings_data.read_systemwide_enabled()
@ -2087,45 +2020,6 @@ class Settings(Page):
self.systemwide_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
def load_bwrap_settings(self, enabled=None):
if enabled is None:
enabled = settings_data.read_bwrap_enabled()
self.bwrap_toggle.setChecked(enabled)
if enabled:
self.bwrap_status_value.setText("Enabled")
self.bwrap_status_value.setStyleSheet(
f"color: #2ecc71; font-size: 14px; {self.font_style}")
else:
self.bwrap_status_value.setText("Disabled")
self.bwrap_status_value.setStyleSheet(
f"color: #e67e22; font-size: 14px; {self.font_style}")
def save_bwrap_settings(self):
enable = self.bwrap_toggle.isChecked()
try:
capability_policy = PolicyController.get('capability')
if capability_policy is not None:
if enable:
PolicyController.instate(capability_policy)
else:
if PolicyController.is_instated(capability_policy):
PolicyController.revoke(capability_policy)
self.load_bwrap_settings()
self.update_status.update_status(
"Capability policy settings updated")
except CommandNotFoundError as e:
self.bwrap_status_value.setText(str(e))
self.bwrap_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
except (PolicyAssignmentError, PolicyInstatementError, PolicyRevocationError) as e:
self.bwrap_status_value.setText(str(e))
self.bwrap_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
except Exception:
self.bwrap_status_value.setText("Failed to update policy")
self.bwrap_status_value.setStyleSheet(
f"color: red; font-size: 14px; {self.font_style}")
def load_registrations_settings(self) -> None:
try:
config = self._prepared_value(

View file

@ -9,6 +9,7 @@ from core.controllers.tickets.TicketPayController import check_if_paid
from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer
from gui.v2.ui.pages.Page import Page
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
@ -77,7 +78,13 @@ class TicketCryptoPickerPage(Page):
clipboard = QApplication.clipboard()
clipboard.setText(temp_billing_code)
not_paid_msg = f"The billing code is not yet showing paid for {temp_billing_code}. Right now, your clipboard has the billing code, to paste it in any text editor. If you did pay, either wait longer for blockchain confirmation, or contact customer support with the code in your clipboard now."
QMessageBox.information(None, "Not Paid", not_paid_msg)
info = QMessageBox(self)
info.setWindowTitle("Not Paid")
info.setText(not_paid_msg)
info.setStandardButtons(QMessageBox.StandardButton.Ok)
style_message_box(info)
mark_confirm_button(info.button(QMessageBox.StandardButton.Ok))
info.exec()
def start_initiate_payment(self, currency):
self.update_status.update_status("Initiating payment...")
@ -121,6 +128,8 @@ class TicketCryptoPickerPage(Page):
msg.setWindowTitle("Existing tickets found")
msg.setText("You already have ticket data. Wipe it and start over?")
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
style_message_box(msg)
mark_confirm_button(msg.button(QMessageBox.StandardButton.Yes))
result = msg.exec()
if result == QMessageBox.StandardButton.Yes:
self.bypass_existing = True
@ -135,6 +144,8 @@ class TicketCryptoPickerPage(Page):
msg.setWindowTitle("Existing billing code found")
msg.setText("You already have a ticket billing code. Do you want to use it? Only hit YES if you ALREADY paid.")
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
style_message_box(msg)
mark_confirm_button(msg.button(QMessageBox.StandardButton.Yes))
result = msg.exec()
if result == QMessageBox.StandardButton.Yes:
self.update_status.update_status("Reusing same code")

18
gui/v2/ui/popups/message_box.py Executable file
View file

@ -0,0 +1,18 @@
MESSAGE_BOX_QSS = (
"QMessageBox { background-color: white; }"
"QMessageBox QLabel { color: black; }"
"QMessageBox QPushButton { color: black; padding: 5px 14px; }"
)
CONFIRM_BUTTON_QSS = "color: #2e7d32; font-weight: bold; padding: 5px 14px;"
def style_message_box(box):
box.setStyleSheet(MESSAGE_BOX_QSS)
def mark_confirm_button(button):
if button is None:
return
button.setText(f"{button.text()}")
button.setStyleSheet(CONFIRM_BUTTON_QSS)

View file

@ -1,5 +1,3 @@
import importlib
from PyQt6.QtCore import QThread, pyqtSignal
@ -7,18 +5,15 @@ class PageDataWorker(QThread):
data_ready = pyqtSignal(str, object)
failed = pyqtSignal(str, str)
def __init__(self, name, module_path, class_name, custom_window):
def __init__(self, name, fn, *args):
super().__init__()
self.name = name
self.module_path = module_path
self.class_name = class_name
self.custom_window = custom_window
self.fn = fn
self.args = args
def run(self):
try:
module = importlib.import_module(self.module_path)
cls = getattr(module, self.class_name)
payload = cls.prepare_data(self.custom_window)
payload = self.fn(*self.args)
self.data_ready.emit(self.name, payload)
except Exception as error:
self.failed.emit(self.name, f"{type(error).__name__}: {error}")