# core 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 from gui.v2.ui.pages.Page import Page from gui.v2.ui.popups.qrcode_dialog import QRCodeDialog # Bring background threads to the main thread UI: from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool mainthread = ThreadSafetyTool() import os from typing import Any from PyQt6 import QtCore, QtWidgets from PyQt6.QtWidgets import ( QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QWidget, QApplication ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QIcon from enum import Enum class DataError(Enum): FORMAT: "format" BLANK: "blank" class PaymentDetailsPage(Page): 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.create_initial_elements() self.button_reverse.setVisible(True) self.begin_invoice() def begin_invoice(self): """"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) if not self.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. # launches background thread tasks: gui_starts_invoice(profile_id=self.profile_id, ui_caller=self.receive_invoice) 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) # 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 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) 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) # 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 _display_error(self, whats_wrong: DataError, whats_it_called: str, the_thing: Any): # Diagnosis if whats_wrong == DataError.FORMAT: error_msg = f"The {whats_it_called} is in the wrong format" status_update = f'Error: Bad {whats_it_called} Format' elif whats_wrong == DataError.BLANK: error_msg = f"The {whats_it_called} is blank! But you need a {whats_it_called} to continue!" status_update = f'Error: Blank {whats_it_called}' else: error_msg = f"The {whats_it_called} has unknown errors!" status_update = f'Unknown Error with {whats_it_called}' # Display if the_thing and isinstance(the_thing, str): self.show_a_popup(f"{error_msg}: {the_thing}") elif the_thing and isinstance(the_thing, dict): message = the_thing.get('message', "!") self.show_a_popup(f"{error_msg} {message}") else: self.show_a_popup(error_msg) self.update_status.update_status(status_update) def copy_text(self, key: str): """Copy/Paste feature""" 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 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') @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) return if not isinstance(invoice_result, Result): self._display_error(whats_wrong=DataError.FORMAT, whats_it_called="Backend Invoice Result", the_thing=invoice_result) return if not invoice_result.valid: self.update_status.update_status(str(invoice_result.error_type.value)) self.show_a_popup(str(invoice_result.message)) return # pull the actual invoice out of the result wrapper, invoice = invoice_result.data if not 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) return self.fill_in_invoice_data(invoice) 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, 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 ) 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.update_status.update_status("Could not begin") def fill_in_invoice_data(self, invoice: SubscriptionInvoice): """Extract and populate invoice data in the labels""" # Extract from the already validated object: billing_code = str(invoice.billing_code) duration = str(invoice.expires_at) amount_text = str(invoice.due_amount) self.full_address = str(invoice.address) if billing_code: self.fields["billing_code"].setText(billing_code) if duration: self.fields["duration"].setText(duration) 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 self.full_address: metrics = self.fields["address"].fontMetrics() elided_text = metrics.elidedText( self.full_address, Qt.TextElideMode.ElideMiddle, self.fields["address"].width() ) self.fields["address"].setText(elided_text) # Now that we have the data populated, show the QR code: self.qr_code_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""" if message and isinstance(message, str): self.update_status.update_status(message) if paid: self._finish_after_paid() 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.') # TICKETS if self.ticketing: self.custom_window.navigator.navigate("ticket_prep") prep_page = self.custom_window.navigator.get_cached("ticket_prep") 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: error_msg = "It stopped checking if it's paid. Could be a connection timeout or other issue" else: error_msg = f"It stopped checking if it's paid. {message}" 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.exec() else: self.update_status.update_status('No address available for QR code') def reverse(self): if self.ticketing: self.ticketing = False self.custom_window.navigator.navigate("ticket_crypto_picker") else: self.custom_window.navigator.navigate("currency_selection") 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) 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) currency = billing_data.currency if not 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) return currency