diff --git a/gui/v2/actions/payment_actions.py b/gui/v2/actions/payment_actions.py index a404d61..e7eb50c 100644 --- a/gui/v2/actions/payment_actions.py +++ b/gui/v2/actions/payment_actions.py @@ -2,6 +2,7 @@ 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.models.Result import Result, ResultError from core.errors.logger import logger # gui @@ -103,5 +104,25 @@ def save_and_begin_timer( connection_observer=connection_observer, profile_id=profile_id ) - + logger.info("starting polling from GUI action..") + check_paid_timer.start_polling() return True # this is just that it started + + +def stop_payment_timer() -> tuple: + """Gets the single instance of the payment checker timer, and stops it.""" + try: + check_paid_timer = CheckPaidTimer.get_instance() + if check_paid_timer: + check_paid_timer.stop_polling() + + content_to_display = "Stopped! You can resume a different profile's invoice now" + return True, content_to_display + + except Exception as e: + error_msg = f"Unable to stop the payment timer: {str(e)}" + logger.error(error_msg) + return False, error_msg + + + diff --git a/gui/v2/ui/pages/payments2.py b/gui/v2/ui/pages/payments2.py index 062c6ca..509ca90 100644 --- a/gui/v2/ui/pages/payments2.py +++ b/gui/v2/ui/pages/payments2.py @@ -5,43 +5,135 @@ 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 +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 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 import os from typing import Any from PyQt6 import QtCore, QtWidgets from PyQt6.QtWidgets import ( QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QWidget, QApplication + QPushButton, QWidget, QApplication, QFrame, QSpacerItem, QSizePolicy ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QIcon from enum import Enum +# For formatting errors graphically: class DataError(Enum): FORMAT: "format" BLANK: "blank" class PaymentDetailsPage(Page): + """Display Payment Details, fetch an invoice, and launch a repeat timer to check payment status.""" + def __init__(self, page_stack, main_window=None, parent=None): super().__init__("Payment Details", page_stack, main_window, parent) self.update_status = main_window self.fields = {} self.ticketing = False + 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 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.setSpacing(15) + main_layout.setContentsMargins(10, 10, 10, 10) + + # --- THE INFO CARD --- + card = QFrame() + card.setObjectName("InfoCard") + card_layout = QVBoxLayout(card) + card_layout.setSpacing(8) + card_layout.setContentsMargins(12, 12, 12, 12) + + 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) + row_layout.addWidget(lbl) + + if field_key is not None: + edit = QLineEdit() + edit.setReadOnly(True) + edit.setAlignment(Qt.AlignmentFlag.AlignCenter) + 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)) + 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;") + card_layout.addWidget(addr_sep) + add_info_row("", field_key="address", show_copy=True) + + main_layout.addWidget(card) + + action_bar = QHBoxLayout() + action_bar.setSpacing(10) + action_bar.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.qr_code_button = QPushButton(" Scan QR Code") + self.qr_code_button.setIcon(QIcon(os.path.join(self.btn_path, "qr-code.png"))) + 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) + self.switch_to_background_button.setDisabled(True) + + action_bar.addWidget(self.qr_code_button) + action_bar.addWidget(self.stop_button) + action_bar.addWidget(self.switch_to_background_button) + + main_layout.addLayout(action_bar) + 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...') @@ -61,77 +153,46 @@ class PaymentDetailsPage(Page): if error_msg: self.show_a_popup(error_msg) + else: + self.polling = True + # And make sure it's allowed to be stopped: + self.stop_button.setDisabled(False) + self.switch_to_background_button.setDisabled(False) - def create_initial_elements(self): - container = QWidget(self) - container.setFixedSize(600, 500) - main_layout = QVBoxLayout(container) - main_layout.setSpacing(15) - main_layout.setContentsMargins(30, 30, 30, 30) - self.title = QLabel("Payment Details") - self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.title.setStyleSheet("font-size: 20px; font-weight: bold; margin-bottom: 10px;") - main_layout.addWidget(self.title) + def stop_or_start_check(self): + """Toggle On/Off Polling""" + # If it's on: + if self.polling: + stopped, message = stop_payment_timer() - # takes field_key (string) instead of field_index (int) - def add_info_row(label_text, field_key=None, show_copy=False): - row_layout = QHBoxLayout() - - lbl = QLabel(label_text) - lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) - row_layout.addWidget(lbl) + if stopped: + self.update_status.update_status("Stopped checking!") + self.stop_button.setDisabled(True) + self.switch_to_background_button.setDisabled(True) - if field_key is not None: - edit = QLineEdit() - edit.setReadOnly(True) - edit.setAlignment(Qt.AlignmentFlag.AlignCenter) - - # Store by name: e.g., self.fields["amount"] = edit_object - self.fields[field_key] = edit - - row_layout.addWidget(edit) + # regardless of if it worked or not: + if message: + self.show_a_popup(str(message)) - if show_copy: - btn = QPushButton() - btn.setFixedSize(40, 40) - btn.setIcon(QIcon(os.path.join(self.btn_path, "paste_button.png"))) - # Pass the string key to the copy function - btn.clicked.connect(lambda checked, key=field_key: self.copy_text(key)) - row_layout.addWidget(btn) - - main_layout.addLayout(row_layout) + # Turn it back on if off: + else: + self.begin_invoice() - # Build the Form using descriptive keys - add_info_row("Your new billing ID:", field_key="billing_code", show_copy=True) - add_info_row("Duration:", 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_prompt = QLabel("Hit the icon to copy paste the address:") - addr_prompt.setStyleSheet("font-size: 14px;") - main_layout.addWidget(addr_prompt) - add_info_row("", field_key="address", show_copy=True) - qr_prompt = QLabel("Hit this icon to Scan the QR code:") - qr_prompt.setAlignment(Qt.AlignmentFlag.AlignCenter) - main_layout.addWidget(qr_prompt) - self.qr_code_button = QPushButton() - self.qr_code_button.setFixedSize(60, 60) - self.qr_code_button.setIcon(QIcon(os.path.join(self.btn_path, "qr-code.png"))) - self.qr_code_button.clicked.connect(self.show_qr_code) - self.qr_code_button.setDisabled(True) - - qr_container = QHBoxLayout() - qr_container.addWidget(self.qr_code_button, alignment=Qt.AlignmentFlag.AlignCenter) - main_layout.addLayout(qr_container) - - self.setLayout(main_layout) - self.button_reverse.clicked.connect(self.reverse) + def background_fetch_mode(self): + """Continue checking if paid, but navigate back to the menu page""" + if self.polling: + self.custom_window.navigator.navigate("menu") + else: + message = "It was not polling to begin with." + self.show_a_popup(message) def _display_error(self, whats_wrong: DataError, whats_it_called: str, the_thing: Any): + """Generic re-usable error prompt for various data related issues""" + # Diagnosis if whats_wrong == DataError.FORMAT: error_msg = f"The {whats_it_called} is in the wrong format" @@ -155,20 +216,19 @@ class PaymentDetailsPage(Page): def copy_text(self, key: str): - """Copy/Paste feature""" + """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) - # Descriptive messages based on the key + # messages based on the key msg_map = { "billing_code": "Billing code copied!", "address": "Address copied!", "amount": "Pay amount copied!" } self.update_status.update_status(msg_map.get(key, "Copied!")) - # QTimer.singleShot(2000, lambda: self.update_status.update_status("Ready")) except Exception as e: self.show_a_popup(str(e)) self.update_status.update_status('No content available for copying') @@ -219,7 +279,7 @@ class PaymentDetailsPage(Page): def fill_in_invoice_data(self, invoice: SubscriptionInvoice): - """Extract and populate invoice data in the labels""" + """Extract and populate invoice data into the labels""" # Extract from the already validated object: billing_code = str(invoice.billing_code) @@ -258,6 +318,10 @@ class PaymentDetailsPage(Page): # Now that we have the data populated, show the QR code: self.qr_code_button.setDisabled(False) + # And make sure it's allowed to be stopped: + self.stop_button.setDisabled(False) + self.switch_to_background_button.setDisabled(False) + @mainthread def timer_updates_gui(self, paid: bool, stopped: bool, message: str): @@ -305,11 +369,13 @@ class PaymentDetailsPage(Page): def reverse(self): + """Navigate away from the page, depending on which payment flow.""" if self.ticketing: self.ticketing = False self.custom_window.navigator.navigate("ticket_crypto_picker") else: - self.custom_window.navigator.navigate("currency_selection") + self.custom_window.navigator.navigate("menu") + def _get_currency(self): """Get the currency from prior cached PaymentData""" diff --git a/gui/v2/ui/styles/css/payment_styles.py b/gui/v2/ui/styles/css/payment_styles.py new file mode 100644 index 0000000..9e570ae --- /dev/null +++ b/gui/v2/ui/styles/css/payment_styles.py @@ -0,0 +1,57 @@ + +PAYMENT_STYLE_SHEET = """ + QWidget { + color: #cdd6f4; + font-family: 'Segoe UI', Arial, sans-serif; + background-color: transparent; /* Force the main container to be invisible */ + } + + QLabel { + color: #bac2de; + background-color: transparent; /* Ensure labels don't bring back the color */ + } + + /* The 'Card' still keeps its color because it's a specific ID */ + #InfoCard { + background-color: #313244; + border-radius: 15px; + border: 1px solid #45475a; + } + + QLineEdit { + background-color: #181825; + border: 2px solid #45475a; + border-radius: 8px; + padding: 5px; + color: #f5e0dc; + font-weight: bold; + } + + QLineEdit:read-only { + color: #a6adc8; + border: 1px solid #585b70; + } + + QPushButton { + background-color: #89b4fa; + color: #11111b; + border-radius: 10px; + font-weight: bold; + } + + QPushButton:hover { + background-color: #b4befe; + } + + QPushButton:disabled { + background-color: #585b70; + color: #7f849c; + } + + #StopButton { + background-color: #f38ba8; + } + #StopButton:hover { + background-color: #eba0ac; + } +"""