From e071b0f502e34a983fc332cbe86f6a43f56060c8 Mon Sep 17 00:00:00 2001 From: SimplifiedPrivacy Date: Sat, 10 Oct 2026 15:24:36 -0400 Subject: [PATCH] Multiple invoices resumed, each one stops the timer on the other. --- gui/v2/actions/payment_actions.py | 105 ++++++++- gui/v2/actions/ticket_actions.py | 34 +++ gui/v2/infrastructure/navigator.py | 36 ++-- gui/v2/infrastructure/page_registry.py | 4 +- gui/v2/ui/pages/currency_selection_page.py | 30 ++- gui/v2/ui/pages/duration_selection_page.py | 21 +- gui/v2/ui/pages/payments2.py | 201 ++++++++++++------ gui/v2/ui/pages/plan_picker_page.py | 51 ++++- .../ui/pages/ticket_or_billing_choice_page.py | 154 ++++++++------ gui/v2/ui/pages/ticket_prep_page.py | 2 +- gui/v2/ui/styles/css/ticket_billing_styles.py | 30 +++ 11 files changed, 500 insertions(+), 168 deletions(-) create mode 100644 gui/v2/actions/ticket_actions.py create mode 100644 gui/v2/ui/styles/css/ticket_billing_styles.py diff --git a/gui/v2/actions/payment_actions.py b/gui/v2/actions/payment_actions.py index e7eb50c..c759c19 100644 --- a/gui/v2/actions/payment_actions.py +++ b/gui/v2/actions/payment_actions.py @@ -1,7 +1,8 @@ # core -from core.services.subscriptions.check_payment_loop import CheckPaidTimer -from core.services.subscriptions.subscriptions_by_protocol import create_invoice, save_billing_code, get_profile_subscription -from core.models.enum_models import PaymentType +from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData +from core.services.payment_phase.check_payment_loop import CheckPaidTimer +from core.services.subscriptions.subscriptions_by_protocol import create_individual_invoice, save_billing_code, get_profile_subscription, start_billing_state, wipe_billing_cycle +from core.models.enum_models import PaymentType, PaymentCategory from core.models.Result import Result, ResultError from core.errors.logger import logger @@ -13,24 +14,105 @@ from gui.v2.infrastructure.setup_observers import connection_observer # generic from typing import Callable, Optional -def gui_starts_invoice(profile_id: int, invoice_ui_caller: Callable, timer_resume_caller: Callable) -> str | None: - """Create a new invoice if we don't have one yet, or re-use the saved data. Returns any errors as strings for UI to display.""" - subscription = get_profile_subscription(profile_id) +def new_individual_billing(profile_id: int): + """Wipes pre-existing global payment cycle. And Calls upon core to setup a new individual profile cycle & data state.""" + logger.info("starting a new individual profile state. And wiping the old one") + return start_billing_state( + category=PaymentCategory.INDIVIDUAL, + profile_id=profile_id, + wipe_existing=True + ) + + +def gui_starts_invoice(invoice_ui_caller: Callable, timer_resume_caller: Callable, profile_id: Optional[int] = None) -> str | None: + + logger.info(f"gui_starts_invoice is being called with profile id {profile_id}") + + # Is there currently a process? + payment_cycle = OverallPaymentCycle() + + # start a new individual one if not existing: + if not payment_cycle: + logger.info("There's no current payment cycle") + if profile_id: + return fetch_individual_invoice(invoice_ui_caller=invoice_ui_caller, timer_resume_caller=timer_resume_caller, profile_id=profile_id) + else: + return "Need a profile id for an individual profile." + + billing_data = payment_cycle.instance + category = payment_cycle.category + + if not billing_data: + msg = "There is a payment cycle, but no current billing data in it. So we're starting a new cycle.." + logger.info(msg) + result = new_individual_billing(profile_id=profile_id) + if not result.valid: + return result + billing_data = result.data + + # already conflicting one: + elif profile_id and profile_id != payment_cycle.profile_id: + logger.info("The new profile id request is different than the pre-existing running one. So we're wiping it and stopping the timer.") + worked, message = stop_payment_timer() + if not worked: + return message + + # start over: + payment_cycle.wipe() + new_data = new_individual_billing(profile_id) + if not new_data.valid: + return new_data # result object + category = PaymentCategory.INDIVIDUAL + # billing_data = new_cycle.data #---------------------------- # INVOICE DATA #---------------------------- + if category == PaymentCategory.INDIVIDUAL: + if profile_id: + return fetch_individual_invoice(invoice_ui_caller=invoice_ui_caller, timer_resume_caller=timer_resume_caller, profile_id=profile_id) + else: + return "Need a profile id for an individual profile." + + elif category == PaymentCategory.TICKET: + print("ready to start for tickets") + + + +def fetch_individual_invoice(invoice_ui_caller: Callable, timer_resume_caller: Callable, profile_id: int) -> str | None: + """ + Purpose: + Create a new invoice if we don't have one yet, or re-uses the saved data. + And starts polling if it's not already. + + Context: + For individual profiles. + + Returns: + Returns any errors as strings for UI to display. + """ + # Fetch: take_action( - action=create_invoice, + action=create_individual_invoice, ui_caller=invoice_ui_caller, profile_id=profile_id, connection_observer=connection_observer ) #---------------------------- - # FILTER + # FILTER ALREADY POLLING #---------------------------- + payment_cycle = OverallPaymentCycle() + if payment_cycle.already_polling: + logger.info("This cycle is already polling, so we don't need to start another poll") + return None + + #---------------------------- + # FILTER LACK OF DATA + #---------------------------- + subscription = get_profile_subscription(profile_id) + if not subscription: logger.info(f"Started a New Invoice for Profile {profile_id}..") return None @@ -50,6 +132,9 @@ def gui_starts_invoice(profile_id: int, invoice_ui_caller: Callable, timer_resum # Setup PyQt6 GUI as mainthread to bring back to: pyqt_threader = ThreadSafetyTool() + # update the global singleton: + payment_cycle.already_polling = True + # setup the timer with the callback: # it doesn't return result here. It calls the view_callback function. check_paid_timer = CheckPaidTimer( @@ -112,6 +197,10 @@ def save_and_begin_timer( def stop_payment_timer() -> tuple: """Gets the single instance of the payment checker timer, and stops it.""" try: + # Wipe the cycle, so this is not the active poller (blocking others) + wipe_billing_cycle() + + # stop the timer: check_paid_timer = CheckPaidTimer.get_instance() if check_paid_timer: check_paid_timer.stop_polling() diff --git a/gui/v2/actions/ticket_actions.py b/gui/v2/actions/ticket_actions.py new file mode 100644 index 0000000..cb805e6 --- /dev/null +++ b/gui/v2/actions/ticket_actions.py @@ -0,0 +1,34 @@ +# core +from core.services.payment_phase.payment_state import OverallPaymentCycle, PaymentData, TicketPaymentData +from core.models.enum_models import PaymentType +from core.controllers.tickets.UseTicketController import get_unused_tickets +from core.models.Result import Result, ResultError +# from core.controllers.ProfileController import ProfileController + +# gui +from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool +from gui.v2.infrastructure.take_plumbing_action import take_action +from gui.v2.infrastructure.setup_observers import connection_observer, ticket_observer + + + +def get_ticket_data() -> Result: + """This is running in the background ALREADY. Get a list of unused tickets from core""" + + ticket_results = get_unused_tickets() + valid = ticket_results.get('valid') + + if not valid: + message = ticket_results.get('message') + if not message: + message = "Error gathering ticket data" + Result(valid=False, error_type=ResultError.MISSING_FILE, message=message) + + raw_tickets = ticket_results.get('data') + print(f"raw_tickets is {raw_tickets}") + if not raw_tickets: + Result(valid=True, data=raw_tickets, message="No tickets available.") + + formatted_data = [(f"Ticket #{t}", t) for t in raw_tickets] + print(f"formatted_data is {formatted_data}") + return Result(valid=True, data=formatted_data) \ No newline at end of file diff --git a/gui/v2/infrastructure/navigator.py b/gui/v2/infrastructure/navigator.py index e5e8e5a..f4af942 100755 --- a/gui/v2/infrastructure/navigator.py +++ b/gui/v2/infrastructure/navigator.py @@ -85,25 +85,25 @@ class Navigator(QObject): def _preload_one(self, name): if name in self._instances: - logger.debug(f"[preload] {name:30s} SKIP (already cached)") + # logger.debug(f"[preload] {name:30s} SKIP (already cached)") return False if name not in PAGE_REGISTRY: - logger.debug(f"[preload] {name:30s} SKIP (not in registry)") + # logger.debug(f"[preload] {name:30s} SKIP (not in registry)") return False module_path, class_name = PAGE_REGISTRY[name] - logger.debug(f"[preload] {name:30s} LOAD {module_path}.{class_name}") + # logger.debug(f"[preload] {name:30s} LOAD {module_path}.{class_name}") t0 = time.perf_counter() try: page = self._instantiate(name) self.register_instance(name, page) dt_ms = (time.perf_counter() - t0) * 1000.0 - logger.debug(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, " - f"cached={len(self._instances)}, stack_size={self.page_stack.count()})") + # logger.debug(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, " + # f"cached={len(self._instances)}, stack_size={self.page_stack.count()})") return True except Exception as e: dt_ms = (time.perf_counter() - t0) * 1000.0 - logger.debug(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) " - f"{type(e).__name__}: {e}") + # logger.debug(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) " + # f"{type(e).__name__}: {e}") traceback.print_exc() from core.errors.logger import logger as core_logger core_logger.warning(f"Background preload failed for '{name}': " @@ -126,22 +126,22 @@ class Navigator(QObject): self._preload_inflight = 0 self._preload_summary_emitted = False self._preload_t_start = time.perf_counter() - logger.debug(f"[preload] ============================================================") - logger.debug(f"[preload] starting background preload: {self._preload_total} pages queued") - logger.debug(f"[preload] order : {', '.join(self._preload_queue)}") - logger.debug(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)") - if fast_mode: - logger.debug(f"[preload] fastmode: ON -> skipping regular-flow pages: " - f"{', '.join(skipped_regular) if skipped_regular else '(none)'}") - logger.debug(f"[preload] gap : 250 ms between loads, 700 ms initial delay") - logger.debug(f"[preload] ============================================================") + # logger.debug(f"[preload] ============================================================") + # logger.debug(f"[preload] starting background preload: {self._preload_total} pages queued") + # logger.debug(f"[preload] order : {', '.join(self._preload_queue)}") + # logger.debug(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)") + # if fast_mode: + # logger.debug(f"[preload] fastmode: ON -> skipping regular-flow pages: " + # f"{', '.join(skipped_regular) if skipped_regular else '(none)'}") + # logger.debug(f"[preload] gap : 250 ms between loads, 700 ms initial delay") + # logger.debug(f"[preload] ============================================================") self._preload_timer.start(700) def _preload_next(self): while self._preload_queue and self._preload_queue[0] in self._instances: skipped = self._preload_queue.pop(0) self._preload_skipped_runtime += 1 - logger.debug(f"[preload] {skipped:30s} SKIP (user navigated here first)") + # logger.debug(f"[preload] {skipped:30s} SKIP (user navigated here first)") if not self._preload_queue: self._maybe_emit_preload_summary() return @@ -197,7 +197,7 @@ class Navigator(QObject): def _on_preload_failed(self, name, error): self._preload_inflight -= 1 - logger.debug(f"[preload] {name:30s} FAIL (async prepare) {error}") + # logger.debug(f"[preload] {name:30s} FAIL (async prepare) {error}") if name not in self._instances: ok = self._preload_one(name) if ok: diff --git a/gui/v2/infrastructure/page_registry.py b/gui/v2/infrastructure/page_registry.py index 374b352..934a887 100755 --- a/gui/v2/infrastructure/page_registry.py +++ b/gui/v2/infrastructure/page_registry.py @@ -61,10 +61,8 @@ REGULAR_FLOW_ONLY = { PRELOAD_ORDER = [ "settings", "editor", - "ticket_or_billing_choice", "plan_picker", "ticket_crypto_picker", - "currency_selection", "policy_suggestion", "systemwide_prompt", "fast_mode_prompt", @@ -86,3 +84,5 @@ PRELOAD_ORDER = [ # "duration_selection", # "payment_details", +# "ticket_or_billing_choice", +# "currency_selection", diff --git a/gui/v2/ui/pages/currency_selection_page.py b/gui/v2/ui/pages/currency_selection_page.py index e3ead10..9821f8b 100755 --- a/gui/v2/ui/pages/currency_selection_page.py +++ b/gui/v2/ui/pages/currency_selection_page.py @@ -1,6 +1,10 @@ +# core +from core.services.subscriptions.subscriptions_by_protocol import paying_for_tickets, get_current_billing_data from core.controllers.profile_state.data_filters import ALL_CURRENCIES from core.models.enum_models import Currency from core.services.payment_phase.payment_state import PaymentData + +# gui from gui.v2.ui.pages.Page import Page import os @@ -71,10 +75,27 @@ class CurrencySelectionPage(Page): profile_id = int(self.update_status.current_profile_id) # get the singleton (this was created on a previous page) - self.billing_data = PaymentData(instance_id=profile_id) + print("doing the new method") + data_result = get_current_billing_data() + + if not data_result.valid: + message = data_result.message + if not message: + message = "Error fetching data" + self.show_a_popup(str(message)) + + # get the singleton (this was created on a previous page) + billing_singleton = data_result.data + + if not billing_singleton: + message = "Error, no billing data" + self.show_a_popup(str(message)) + + # prior direct method: + # self.billing_data = PaymentData(instance_id=profile_id) # set the value as an Enum to be used in a different page, - self.billing_data.currency = self._get_selected_currency() + billing_singleton.currency = self._get_selected_currency() self.update_status.update_status('Loading payment details...') self.custom_window.navigator.navigate("payment_details") @@ -91,4 +112,7 @@ class CurrencySelectionPage(Page): def reverse(self): - self.custom_window.navigator.navigate("duration_selection") + if paying_for_tickets(): + self.custom_window.navigator.navigate("plan_picker") + else: + self.custom_window.navigator.navigate("duration_selection") diff --git a/gui/v2/ui/pages/duration_selection_page.py b/gui/v2/ui/pages/duration_selection_page.py index 9e6aadf..e71f80c 100755 --- a/gui/v2/ui/pages/duration_selection_page.py +++ b/gui/v2/ui/pages/duration_selection_page.py @@ -5,6 +5,7 @@ from core.controllers.profile_state.data_filters import get_plans_for_profile from core.models.Result import Result #, ResultError # GUI +from gui.v2.actions.payment_actions import new_individual_billing from gui.v2.ui.styles.payment_styles import DURATION_SELECTION_STYLE, NEXT_BUTTON_STYLE from gui.v2.ui.pages.Page import Page @@ -51,8 +52,18 @@ class DurationSelectionPage(Page): # get profile id: profile_id = int(self.update_status.current_profile_id) + # start entire payment cycle process: + result = new_individual_billing(profile_id=profile_id) + if not result.valid: + self.show_a_popup(str(result.message)) + return + # setup singleton - self.billing_data = PaymentData(instance_id=profile_id) + self.billing_data = result.data + # PaymentData(instance_id=profile_id) + + if self.billing_data: + print(f"we have billing data: {self.billing_data}") # Thread SQL in background: take_action( @@ -89,12 +100,14 @@ class DurationSelectionPage(Page): plan_id = self.dict_of_choices[current_choice] # save to singleton across pages: + print(f"saving plan_id to the billing data") self.billing_data.plan_id = plan_id # go to next page: - currency_page = self.custom_window.navigator.get_cached("currency_selection") - if currency_page is not None: - self.custom_window.navigator.navigate("currency_selection") + print("going to the next page") + # currency_page = self.custom_window.navigator.get_cached("currency_selection") + # if currency_page is not None: + self.custom_window.navigator.navigate("currency_selection") def reverse(self): self.custom_window.navigator.navigate("id") diff --git a/gui/v2/ui/pages/payments2.py b/gui/v2/ui/pages/payments2.py index 0b060ad..18f2d3d 100644 --- a/gui/v2/ui/pages/payments2.py +++ b/gui/v2/ui/pages/payments2.py @@ -1,17 +1,23 @@ # core +from core.services.subscriptions.subscriptions_by_protocol import paying_for_tickets from core.services.payment_phase.payment_state import PaymentData from core.models.pydantic_models.ServerReply import SubscriptionInvoice from core.models.enum_models import Currency from core.models.Result import Result # GUI -from gui.v2.actions.payment_actions import save_and_begin_timer, gui_starts_invoice, stop_payment_timer +from gui.v2.actions.payment_actions import ( + save_and_begin_timer, + gui_starts_invoice, + stop_payment_timer, +) from gui.v2.ui.pages.Page import Page, DataError from gui.v2.ui.popups.qrcode_dialog import QRCodeDialog from gui.v2.ui.styles.css.payment_styles import PAYMENT_STYLE_SHEET # Bring background threads to the main thread UI: from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool + mainthread = ThreadSafetyTool() # generic @@ -19,46 +25,69 @@ from typing import Optional import os from PyQt6 import QtCore, QtWidgets from PyQt6.QtWidgets import ( - QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QWidget, QApplication, QFrame, QSpacerItem, QSizePolicy, QStackedWidget, QMainWindow + QVBoxLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QWidget, + QApplication, + QFrame, + QSpacerItem, + QSizePolicy, + QStackedWidget, + QMainWindow, ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QIcon + class PaymentDetailsPage(Page): """ Display Payment Details, fetch an invoice, and launch a repeat timer to check payment status. - + From Page: Inherits display_error, show_a_popup From Navigator: gets the functions navigate, get_cached, page_stack object """ - def __init__(self, page_stack: QStackedWidget, main_window: Optional[QMainWindow] = None, parent: Optional[QWidget] = None): + def __init__( + self, + page_stack: QStackedWidget, + main_window: Optional[QMainWindow] = None, + parent: Optional[QWidget] = None, + ): super().__init__("Payment Details", page_stack, main_window, parent) self.update_status = main_window - + self.fields = {} - self.ticketing = False + self.ticketing = paying_for_tickets() self.polling = False self.setStyleSheet(PAYMENT_STYLE_SHEET) - + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.create_initial_elements() self.button_reverse.setVisible(True) + # self.begin_invoice() + + + def showEvent(self, event): + # Call the parent class showEvent first + super().showEvent(event) + # runs every time the widget is displayed self.begin_invoice() def create_initial_elements(self): """Setup Payment UI Buttons as part of a dict by field key.""" main_layout = QVBoxLayout(self) - + # put the entire group (card + buttons) in the middle: - main_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) - + main_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + main_layout.setSpacing(15) - main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.setContentsMargins(10, 10, 10, 10) # --- THE INFO CARD --- card = QFrame() @@ -70,7 +99,7 @@ class PaymentDetailsPage(Page): def add_info_row(label_text, field_key=None, show_copy=False): row_layout = QHBoxLayout() row_layout.setContentsMargins(0, 0, 0, 0) - + lbl = QLabel(label_text) lbl.setStyleSheet("font-size: 12px;") lbl.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) @@ -80,25 +109,29 @@ class PaymentDetailsPage(Page): edit = QLineEdit() edit.setReadOnly(True) edit.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.fields[field_key] = edit + self.fields[field_key] = edit row_layout.addWidget(edit) if show_copy: btn = QPushButton() btn.setFixedSize(28, 28) btn.setIcon(QIcon(os.path.join(self.btn_path, "paste_button.png"))) - btn.clicked.connect(lambda checked, key=field_key: self.copy_text(key)) + btn.clicked.connect( + lambda checked, key=field_key: self.copy_text(key) + ) row_layout.addWidget(btn) - + card_layout.addLayout(row_layout) add_info_row("Billing ID:", field_key="billing_code", show_copy=True) add_info_row("Subscription End-date", field_key="duration", show_copy=False) add_info_row("Currency:", field_key="currency", show_copy=False) add_info_row("Amount:", field_key="amount", show_copy=True) - + addr_sep = QLabel("Wallet Address") - addr_sep.setStyleSheet("font-size: 11px; font-weight: bold; color: #89b4fa; margin-top: 2px;") + addr_sep.setStyleSheet( + "font-size: 11px; font-weight: bold; color: #89b4fa; margin-top: 2px;" + ) card_layout.addWidget(addr_sep) add_info_row("", field_key="address", show_copy=True) @@ -113,13 +146,13 @@ class PaymentDetailsPage(Page): self.qr_code_button.setMinimumHeight(40) self.qr_code_button.clicked.connect(self.show_qr_code) self.qr_code_button.setDisabled(True) - + self.stop_button = QPushButton(" Stop Checking") self.stop_button.setObjectName("StopButton") self.stop_button.setMinimumHeight(40) self.stop_button.clicked.connect(self.stop_or_start_check) self.stop_button.setDisabled(True) - + self.switch_to_background_button = QPushButton(" Check in Background") self.switch_to_background_button.setMinimumHeight(40) self.switch_to_background_button.clicked.connect(self.background_fetch_mode) @@ -133,21 +166,31 @@ class PaymentDetailsPage(Page): self.button_reverse.clicked.connect(self.reverse) def begin_invoice(self): - """"Fetches an Invoice from GUI's actions modules""" - self.update_status.update_status('Getting Invoice...') + """ "Fetches an Invoice from GUI's actions modules""" + self.update_status.update_status("Getting Invoice...") self.profile_id = int(self.update_status.current_profile_id) + print(f"profile_id is {self.profile_id}") + if not self.profile_id: - self.display_error(whats_wrong=DataError.BLANK, whats_it_called="Profile ID", the_thing=profile_id) + self.display_error( + whats_wrong=DataError.BLANK, + whats_it_called="Profile ID", + the_thing=profile_id, + ) return # this is not used here, but this is the right place to put it. Since we should have it by now: self.currency = self._get_currency() if not self.currency: - return # error is already displayed in subfunction. + return # error is already displayed in subfunction. # launches background thread tasks: - error_msg = gui_starts_invoice(profile_id=self.profile_id, invoice_ui_caller=self.receive_invoice, timer_resume_caller=self.timer_updates_gui) + error_msg = gui_starts_invoice( + invoice_ui_caller=self.receive_invoice, + timer_resume_caller=self.timer_updates_gui, + profile_id=self.profile_id, + ) if error_msg: self.show_a_popup(error_msg) @@ -157,7 +200,6 @@ class PaymentDetailsPage(Page): self.stop_button.setDisabled(False) self.switch_to_background_button.setDisabled(False) - def stop_or_start_check(self): """Toggle On/Off Polling""" # If it's on: @@ -177,7 +219,6 @@ class PaymentDetailsPage(Page): else: self.begin_invoice() - def background_fetch_mode(self): """Continue checking if paid, but navigate back to the menu page""" if self.polling: @@ -186,36 +227,42 @@ class PaymentDetailsPage(Page): message = "It was not polling to begin with." self.show_a_popup(message) - def copy_text(self, key: str): """Copy/Paste fields and update the bottom status""" try: # Use the key to determine if we use elided text or the full raw address text = self.full_address if key == "address" else self.fields[key].text() QApplication.clipboard().setText(text) - + # messages based on the key msg_map = { "billing_code": "Billing code copied!", "address": "Address copied!", - "amount": "Pay amount copied!" + "amount": "Pay amount copied!", } self.update_status.update_status(msg_map.get(key, "Copied!")) except Exception as e: self.show_a_popup(str(e)) - self.update_status.update_status('No content available for copying') - + self.update_status.update_status("No content available for copying") @mainthread def receive_invoice(self, invoice_result: Result): """Callback for the background API thread to update the UI""" if not invoice_result: - self.display_error(whats_wrong=DataError.BLANK, whats_it_called="Backend Invoice Result", the_thing=invoice_result) + self.display_error( + whats_wrong=DataError.BLANK, + whats_it_called="Backend Invoice Result", + the_thing=invoice_result, + ) return if not isinstance(invoice_result, Result): - self.display_error(whats_wrong=DataError.FORMAT, whats_it_called="Backend Invoice Result", the_thing=invoice_result) + self.display_error( + whats_wrong=DataError.FORMAT, + whats_it_called="Backend Invoice Result", + the_thing=invoice_result, + ) return if not invoice_result.valid: @@ -227,29 +274,38 @@ class PaymentDetailsPage(Page): invoice = invoice_result.data if not invoice: - self.display_error(whats_wrong=DataError.BLANK, whats_it_called="invoice", the_thing=invoice) + self.display_error( + whats_wrong=DataError.BLANK, + whats_it_called="invoice", + the_thing=invoice, + ) return if not isinstance(invoice, SubscriptionInvoice): - self.display_error(whats_wrong=DataError.FORMAT, whats_it_called="invoice", the_thing=invoice) + self.display_error( + whats_wrong=DataError.FORMAT, + whats_it_called="invoice", + the_thing=invoice, + ) return self.fill_in_invoice_data(invoice) - self.update_status.update_status('Invoice generated. Awaiting payment...') + self.update_status.update_status("Invoice generated. Awaiting payment...") # kick off the timer and save the billing id, saved_and_started = save_and_begin_timer( - ticketing=self.ticketing, + ticketing=paying_for_tickets(), gui_callback=self.timer_updates_gui, billing_code=invoice.billing_code, profile_id=int(self.update_status.current_profile_id), - expires_at=invoice.expires_at + expires_at=invoice.expires_at, ) if not saved_and_started: - self.show_a_popup("The save and start checking if paid did not even begin. Please see the error logs/") + self.show_a_popup( + "The save and start checking if paid did not even begin. Please see the error logs/" + ) self.update_status.update_status("Could not begin") - def fill_in_invoice_data(self, invoice: SubscriptionInvoice): """Extract and populate invoice data into the labels""" @@ -264,23 +320,27 @@ class PaymentDetailsPage(Page): if duration: self.fields["duration"].setText(duration) - self.fields["duration"].setStyleSheet('font-size: 13px;') + self.fields["duration"].setStyleSheet("font-size: 13px;") if amount_text: self.fields["amount"].setText(amount_text) - + # Dynamic font sizing for amount font_size = 14 - if len(amount_text) > 9: font_size = 15 - if len(amount_text) > 13: font_size = 10 - self.fields["amount"].setStyleSheet(f"font-size: {font_size}px; font-weight: bold;") + if len(amount_text) > 9: + font_size = 15 + if len(amount_text) > 13: + font_size = 10 + self.fields["amount"].setStyleSheet( + f"font-size: {font_size}px; font-weight: bold;" + ) if self.full_address: metrics = self.fields["address"].fontMetrics() elided_text = metrics.elidedText( - self.full_address, - Qt.TextElideMode.ElideMiddle, - self.fields["address"].width() + self.full_address, + Qt.TextElideMode.ElideMiddle, + self.fields["address"].width(), ) self.fields["address"].setText(elided_text) @@ -294,7 +354,6 @@ class PaymentDetailsPage(Page): self.stop_button.setDisabled(False) self.switch_to_background_button.setDisabled(False) - @mainthread def timer_updates_gui(self, paid: bool, stopped: bool, message: str): """Callback for the background thread timer to repeatedly update the UI""" @@ -308,10 +367,9 @@ class PaymentDetailsPage(Page): if stopped and not paid: self._display_it_stopped(message) - def _finish_after_paid(self): """Last step. Notify the user and potentially navigate away.""" - self.update_status.update_status('Payment received.') + self.update_status.update_status("Payment received.") # TICKETS if self.ticketing: @@ -320,7 +378,6 @@ class PaymentDetailsPage(Page): if prep_page: prep_page.start_prep() - def _display_it_stopped(self, message: str): """Notify the user if it's NOT paid, but stopped""" if not message: @@ -330,15 +387,18 @@ class PaymentDetailsPage(Page): self.update_status.update_status("Timer Stopped") self.show_a_popup(error_msg) - def show_qr_code(self): """Launch an external QR scanning popup screen""" if self.full_address and self.currency: - qr_dialog = QRCodeDialog(self.full_address, self.fields["amount"].text(), self.currency.value, self) + qr_dialog = QRCodeDialog( + self.full_address, + self.fields["amount"].text(), + self.currency.value, + self, + ) qr_dialog.exec() else: - self.update_status.update_status('No address available for QR code') - + self.update_status.update_status("No address available for QR code") def reverse(self): """Navigate away from the page, depending on which payment flow.""" @@ -348,24 +408,39 @@ class PaymentDetailsPage(Page): else: self.custom_window.navigator.navigate("menu") - def _get_currency(self): """Get the currency from prior cached PaymentData""" if not self.profile_id or not isinstance(self.profile_id, int): - self.display_error(whats_wrong=DataError.FORMAT, whats_it_called="profile_id", the_thing=self.profile_id) - + self.display_error( + whats_wrong=DataError.FORMAT, + whats_it_called="profile_id", + the_thing=self.profile_id, + ) + billing_data = PaymentData(instance_id=self.profile_id) if not billing_data: - self.display_error(whats_wrong=DataError.BLANK, whats_it_called="Profile's Billing Data", the_thing=billing_data) + self.display_error( + whats_wrong=DataError.BLANK, + whats_it_called="Profile's Billing Data", + the_thing=billing_data, + ) currency = billing_data.currency if not currency: - self.display_error(whats_wrong=DataError.BLANK, whats_it_called="currency", the_thing=currency) + self.display_error( + whats_wrong=DataError.BLANK, + whats_it_called="currency", + the_thing=currency, + ) if not isinstance(currency, Currency): - self.display_error(whats_wrong=DataError.FORMAT, whats_it_called="currency", the_thing=currency) + self.display_error( + whats_wrong=DataError.FORMAT, + whats_it_called="currency", + the_thing=currency, + ) return currency diff --git a/gui/v2/ui/pages/plan_picker_page.py b/gui/v2/ui/pages/plan_picker_page.py index 2a51ec0..8848be3 100755 --- a/gui/v2/ui/pages/plan_picker_page.py +++ b/gui/v2/ui/pages/plan_picker_page.py @@ -1,16 +1,29 @@ -from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem -from PyQt6 import QtCore +# core +from core.services.subscriptions.subscriptions_by_protocol import start_billing_state +from core.models.enum_models import PaymentCategory +# gui +# from gui.v2.actions.payment_actions import start_billing_state from gui.v2.ui.pages.Page import Page from gui.v2.ui.styles.styles import TERMINAL_LIST_QSS from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread +# generic +from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem +from PyQt6 import QtCore + class PlanPickerPage(Page): def __init__(self, page_stack, main_window=None, parent=None): super().__init__("PickPlan", page_stack, main_window, parent) self.update_status = main_window + + # this page only, immediate pricing display: self.pricing_data = None + + # cross page cache overall data: + self.billing_data = None + self.worker = None self.title.setText("Pick a Plan") @@ -39,6 +52,19 @@ class PlanPickerPage(Page): self.status_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) def start_sync(self): + # STATE + result = start_billing_state(category=PaymentCategory.TICKET) + if not result.valid: + error_msg = result.message + if error_msg: + self.show_a_popup(str(error_msg)) + else: + self.show_a_popup("Error with Starting Billing") + + # cache data to save the choice to it: + self.billing_data = result.data + + # Begin Tickets: self.plan_list.clear() self.status_label.setText("Syncing prices...") self.update_status.update_status("Syncing ticket prices...") @@ -88,10 +114,23 @@ class PlanPickerPage(Page): plan_key = item.data(QtCore.Qt.ItemDataRole.UserRole) if not plan_key: return - self.custom_window.navigator.navigate("ticket_crypto_picker") - crypto_page = self.custom_window.navigator.get_cached("ticket_crypto_picker") - if crypto_page is not None: - crypto_page.set_selected_plan(plan_key) + + print(f"plan_key is {plan_key}") + + # in temp cache save choice: + self.billing_data.which_key = plan_key + + # go to next page: + currency_page = self.custom_window.navigator.get_cached("currency_selection") + if currency_page is not None: + self.custom_window.navigator.navigate("currency_selection") + + + + # self.custom_window.navigator.navigate("ticket_crypto_picker") + # crypto_page = self.custom_window.navigator.get_cached("ticket_crypto_picker") + # if crypto_page is not None: + # crypto_page.set_selected_plan(plan_key) def on_error(self, msg): self.status_label.setText(f"Error: {msg}") diff --git a/gui/v2/ui/pages/ticket_or_billing_choice_page.py b/gui/v2/ui/pages/ticket_or_billing_choice_page.py index 599e292..2310115 100755 --- a/gui/v2/ui/pages/ticket_or_billing_choice_page.py +++ b/gui/v2/ui/pages/ticket_or_billing_choice_page.py @@ -1,13 +1,25 @@ -from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem, QPushButton -from PyQt6 import QtCore - -from core.controllers.ProfileController import ProfileController -from core.controllers.tickets.UseTicketController import get_unused_tickets +# core +# from core.controllers.ProfileController import ProfileController +# from core.controllers.tickets.UseTicketController import get_unused_tickets +from core.models.Result import Result, ResultError +# gui +from gui.v2.ui.styles.css.ticket_billing_styles import TICKET_LIST_STYLE_ONE, TICKET_BUTTON_A, USE_BILLING_BUTTON_STYLE from gui.v2.actions.locations import location_candidates from gui.v2.actions.menu_tasks import enable_profile from gui.v2.infrastructure.setup_observers import ticket_observer from gui.v2.ui.pages.Page import Page +from gui.v2.infrastructure.take_plumbing_action import take_action +from gui.v2.actions.ticket_actions import get_ticket_data + +# Bring background threads to the main thread UI: +from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool +mainthread = ThreadSafetyTool() + + +from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem, QPushButton +from PyQt6 import QtCore + class TicketOrBillingChoicePage(Page): @@ -32,87 +44,103 @@ class TicketOrBillingChoicePage(Page): self.ticket_list = QListWidget(self) self.ticket_list.setGeometry(60, 140, 380, 320) - self.ticket_list.setStyleSheet(""" - QListWidget { - background-color: #000000; color: #00ffff; - border: 2px solid #00ffff; border-radius: 8px; - font-size: 14px; - } - QListWidget::item { padding: 10px; } - QListWidget::item:hover { background-color: #003333; } - QListWidget::item:selected { background-color: #006666; color: white; } - """) + self.ticket_list.setStyleSheet(TICKET_LIST_STYLE_ONE) self.use_ticket_button = QPushButton("Use Selected Ticket", self) self.use_ticket_button.setGeometry(60, 470, 380, 40) - self.use_ticket_button.setStyleSheet(""" - QPushButton { - background-color: #00aaff; color: white; border: none; - border-radius: 5px; font-size: 14px; font-weight: bold; - } - QPushButton:disabled { background-color: #555555; } - """) + self.use_ticket_button.setStyleSheet(TICKET_BUTTON_A) self.use_ticket_button.clicked.connect(self.on_use_ticket) self.use_billing_button = QPushButton("Use a Billing Code Instead", self) self.use_billing_button.setGeometry(470, 200, 280, 60) - self.use_billing_button.setStyleSheet(""" - QPushButton { - background-color: #000000; color: #00ffff; - border: 2px solid #00ffff; border-radius: 10px; - font-size: 14px; font-weight: bold; - } - QPushButton:hover { background-color: #003333; } - """) + self.use_billing_button.setStyleSheet(USE_BILLING_BUTTON_STYLE) self.use_billing_button.clicked.connect(self.on_use_billing) self.status_label = QLabel("", self) self.status_label.setGeometry(20, 520, 760, 20) self.status_label.setStyleSheet("color: #cccccc; font-size: 12px;") self.status_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.get_tickets() - def populate(self): + def get_tickets(self): + """Gets tickets in the background""" + take_action( + action=get_ticket_data, + ui_caller=self.populate + ) + + @mainthread + def populate(self, result: Result): + """Callback to Populate tickets.""" self.ticket_list.clear() - try: - unused = get_unused_tickets(ticket_observer) - except Exception as e: - self.status_label.setText(f"Error reading tickets: {e}") + + message = result.message + + if not result.valid: + self.status_label.setText("Error reading tickets") + if message: + self.show_a_popup(message) # this is an error return - if unused.get('valid'): - for t in unused.get('data', []): - item = QListWidgetItem(f"Ticket #{t}") - item.setData(QtCore.Qt.ItemDataRole.UserRole, t) - self.ticket_list.addItem(item) - else: - self.status_label.setText(unused.get('message', 'No tickets available.')) + + data = result.data + if not data: + if message: + self.status_label.setText(message) # this is valid state of none + return + + # POPULATE + for display_text, ticket_id in result.data: + item = QListWidgetItem(display_text) + item.setData(QtCore.Qt.ItemDataRole.UserRole, ticket_id) + self.ticket_list.addItem(item) def on_use_ticket(self): + # 1. Get the visual item the user clicked on item = self.ticket_list.currentItem() + if not item: self.status_label.setText("Pick a ticket from the list first.") return + + # 2. This returns the actual ID (e.g., 1), NOT the string "Ticket #1" which_ticket = item.data(QtCore.Qt.ItemDataRole.UserRole) - profile_id = self.update_status.current_profile_id - try: - profile = ProfileController.get(int(profile_id)) - except Exception: - profile = None - candidates = location_candidates(profile) - if not candidates: - self.status_label.setText("Could not determine profile location.") - return - profile_data = { - 'id': int(profile_id), - 'use_ticket': which_ticket, - 'ticket_location': candidates[0] - } - menu_page = self.custom_window.navigator.get_cached("menu") - if menu_page: - self.update_status.update_status(f"Using ticket #{which_ticket}...") - self.profile_enable_worker = enable_profile( - profile_data, menu_page.handle_task_result, allow_uninstalled_tor=True, - ) - self.custom_window.navigator.navigate("menu") + + print(f"which_ticket is {which_ticket}") + profile_id = int(self.update_status.current_profile_id) + print(f"profile_id is {profile_id}") + + + + # def on_use_ticket(self): + # item = self.ticket_list.currentItem() + # if not item: + # self.status_label.setText("Pick a ticket from the list first.") + # return + # which_ticket = item.data(QtCore.Qt.ItemDataRole.UserRole) + + + + # profile_id = self.update_status.current_profile_id + # try: + # profile = ProfileController.get(int(profile_id)) + # except Exception: + # profile = None + # candidates = location_candidates(profile) + # if not candidates: + # self.status_label.setText("Could not determine profile location.") + # return + # profile_data = { + # 'id': int(profile_id), + # 'use_ticket': which_ticket, + # 'ticket_location': candidates[0] + # } + # menu_page = self.custom_window.navigator.get_cached("menu") + # if menu_page: + # self.update_status.update_status(f"Using ticket #{which_ticket}...") + # self.profile_enable_worker = enable_profile( + # profile_data, menu_page.handle_task_result, allow_uninstalled_tor=True, + # ) + # self.custom_window.navigator.navigate("menu") def on_use_billing(self): self.custom_window.navigator.navigate("id") diff --git a/gui/v2/ui/pages/ticket_prep_page.py b/gui/v2/ui/pages/ticket_prep_page.py index ef82fba..8b41a02 100755 --- a/gui/v2/ui/pages/ticket_prep_page.py +++ b/gui/v2/ui/pages/ticket_prep_page.py @@ -100,7 +100,7 @@ class TicketPrepPage(Page): def on_error(self, msg): self.terminal.append(f"EXCEPTION: {msg}") - self.status_label.setText(f"An unkown error occured") + self.status_label.setText(f"An unknown error occured") self.continue_button.setEnabled(True) def on_continue(self): diff --git a/gui/v2/ui/styles/css/ticket_billing_styles.py b/gui/v2/ui/styles/css/ticket_billing_styles.py new file mode 100644 index 0000000..fa0fe2d --- /dev/null +++ b/gui/v2/ui/styles/css/ticket_billing_styles.py @@ -0,0 +1,30 @@ + + + +TICKET_LIST_STYLE_ONE = """ + QListWidget { + background-color: #000000; color: #00ffff; + border: 2px solid #00ffff; border-radius: 8px; + font-size: 14px; + } + QListWidget::item { padding: 10px; } + QListWidget::item:hover { background-color: #003333; } + QListWidget::item:selected { background-color: #006666; color: white; } +""" + +TICKET_BUTTON_A = """ + QPushButton { + background-color: #00aaff; color: white; border: none; + border-radius: 5px; font-size: 14px; font-weight: bold; + } + QPushButton:disabled { background-color: #555555; } +""" + +USE_BILLING_BUTTON_STYLE = """ + QPushButton { + background-color: #000000; color: #00ffff; + border: 2px solid #00ffff; border-radius: 10px; + font-size: 14px; font-weight: bold; + } + QPushButton:hover { background-color: #003333; } +""" \ No newline at end of file