Big Bang Payments Refactor. Moved core logic out of the PyQt classes, instead it uses payment_actions, which coordinates with core background threaded tasks

This commit is contained in:
SimplifiedPrivacy 2026-10-07 13:59:24 -04:00
parent 5b97f6ad1b
commit 40ee53197e
9 changed files with 640 additions and 97 deletions

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -318,6 +318,11 @@ def profile_status_message(profile: BaseProfile, is_enabled: bool, idle: bool =
"""Generate status text outside Qt so workers and pages report identical subscription state.""" """Generate status text outside Qt so workers and pages report identical subscription state."""
subscription = getattr(profile, "subscription", None) subscription = getattr(profile, "subscription", None)
profile_date = getattr(subscription, "expires_at", None) profile_date = getattr(subscription, "expires_at", None)
billing_pending = getattr(subscription, "billing_pending", None)
if billing_pending:
return "Click Enable to Resume Billing."
if profile_date is None: if profile_date is None:
return "Offline. No subscription found." return "Offline. No subscription found."

View file

@ -0,0 +1,95 @@
from core.services.payment_phase.payment_state import PaymentData
from core.services.subscriptions.check_payment_loop import CheckPaidTimer
from core.services.subscriptions.subscriptions_by_protocol import create_invoice, save_billing_code, return_pre_existing_payment_data, get_profile_subscription
from core.models.enum_models import PaymentType
from core.models.pydantic_models.ServerReply import SubscriptionInvoice
from core.models.Result import Result
from core.models.Subscription import Subscription
from core.errors.logger import logger
# 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
from typing import Callable, Optional
def gui_starts_invoice(profile_id: int, ui_caller: Callable) -> Result:
"""Create a new invoice if we don't have one yet, or re-use the saved data."""
subscription = get_profile_subscription(profile_id)
print(f"in gui_starts_invoice, subscription is {subscription}")
if not subscription:
logger.info(f"Starting a New Invoice for Profile {profile_id}..")
take_action(
action=create_invoice,
ui_caller=ui_caller,
profile_id=profile_id,
connection_observer=connection_observer
)
return Result(valid=True)
else:
return return_pre_existing_payment_data(profile_id=profile_id, subscription=subscription)
def save_and_begin_timer(
ticketing: bool,
gui_callback: Callable,
billing_code: str,
profile_id: Optional[int] = None,
expires_at: Optional[str] = None,
) -> bool:
"""
First saves the billing code & profile expiration, if it's an individual (non-ticket) crypto-based one.
Then it Starts a loop of checking if the invoice is paid and saves it.
"""
if ticketing:
payment_type = PaymentType.TICKETS
logger.error("Not yet setup for ticketing")
return False
# NON-ticketing:
elif profile_id and expires_at:
payment_type = PaymentType.CRYPTO
saved = save_billing_code(profile_id=profile_id, billing_code=billing_code, expires_at=expires_at)
if not saved.valid:
return False
else:
logger.error("You need a profile id and expires_at for individual profiles")
return False
# Setup PyQt6 GUI as mainthread to bring back to:
pyqt_threader = ThreadSafetyTool()
# setup the timer with the callback:
# it doesn't return result here. It calls the gui_callback function.
check_paid_timer = CheckPaidTimer(
view_callback=gui_callback,
billing_code=billing_code,
ui_dispatcher=pyqt_threader,
payment_type=payment_type,
connection_observer=connection_observer
)
return True # this is just that it started
# def create_an_invoice(ui_caller: Callable, profile_id: int, connection_observer: ConnectionObserver) -> SubscriptionInvoice|str:
# """Create an Invoice for regular single profile billing"""
# take_action(
# action=create_invoice,
# ui_caller=ui_caller,
# profile_id=profile_id,
# connection_observer=connection_observer
# )
# subscription_plan_id=subscription_plan_id,
# location_id=location_id,
# protocol_choice=protocol_choice,
# currency=currency_choice,

View file

@ -23,7 +23,7 @@ PAGE_REGISTRY = {
"policy_suggestion": ("gui.v2.ui.pages.policy_suggestion_page", "PolicySuggestionPage"), "policy_suggestion": ("gui.v2.ui.pages.policy_suggestion_page", "PolicySuggestionPage"),
"duration_selection": ("gui.v2.ui.pages.duration_selection_page", "DurationSelectionPage"), "duration_selection": ("gui.v2.ui.pages.duration_selection_page", "DurationSelectionPage"),
"currency_selection": ("gui.v2.ui.pages.currency_selection_page", "CurrencySelectionPage"), "currency_selection": ("gui.v2.ui.pages.currency_selection_page", "CurrencySelectionPage"),
"payment_details": ("gui.v2.ui.pages.payment_details_page", "PaymentDetailsPage"), "payment_details": ("gui.v2.ui.pages.payments2", "PaymentDetailsPage"),
"plan_picker": ("gui.v2.ui.pages.plan_picker_page", "PlanPickerPage"), "plan_picker": ("gui.v2.ui.pages.plan_picker_page", "PlanPickerPage"),
"ticket_crypto_picker": ("gui.v2.ui.pages.ticket_crypto_picker_page", "TicketCryptoPickerPage"), "ticket_crypto_picker": ("gui.v2.ui.pages.ticket_crypto_picker_page", "TicketCryptoPickerPage"),
"ticket_prep": ("gui.v2.ui.pages.ticket_prep_page", "TicketPrepPage"), "ticket_prep": ("gui.v2.ui.pages.ticket_prep_page", "TicketPrepPage"),
@ -62,10 +62,8 @@ PRELOAD_ORDER = [
"settings", "settings",
"editor", "editor",
"ticket_or_billing_choice", "ticket_or_billing_choice",
"payment_details",
"plan_picker", "plan_picker",
"ticket_crypto_picker", "ticket_crypto_picker",
"duration_selection",
"currency_selection", "currency_selection",
"policy_suggestion", "policy_suggestion",
"systemwide_prompt", "systemwide_prompt",
@ -85,3 +83,6 @@ PRELOAD_ORDER = [
"install_system_package", "install_system_package",
"sync_screen", "sync_screen",
] ]
# "duration_selection",
# "payment_details",

View file

@ -1,3 +1,5 @@
from gui.v2.ui.popups.error_popup import ErrorPopup
import os import os
from PyQt6.QtWidgets import QWidget, QLabel, QPushButton from PyQt6.QtWidgets import QWidget, QLabel, QPushButton
@ -101,3 +103,17 @@ class Page(QWidget):
except TypeError: except TypeError:
pass pass
button.clicked.connect(handler) button.clicked.connect(handler)
def show_a_popup(self, message: str, title: str = "Error"):
"""Display a popup with an error"""
error_popup = ErrorPopup(
choice=False,
title=title,
message=message,
end_app = False,
on_answer = None,
passed_result = None,
)
error_popup.show()

View file

@ -1,11 +1,14 @@
import os 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
from gui.v2.ui.pages.Page import Page
from PyQt6.QtWidgets import QButtonGroup, QLabel, QPushButton import os
from PyQt6 import QtCore
from PyQt6.QtWidgets import QLabel, QPushButton, QButtonGroup
from PyQt6.QtWidgets import QVBoxLayout, QWidget, QSpacerItem, QSizePolicy
from PyQt6.QtGui import QIcon from PyQt6.QtGui import QIcon
from PyQt6.QtCore import QSize from PyQt6.QtCore import QSize
from PyQt6 import QtCore
from gui.v2.ui.pages.Page import Page
class CurrencySelectionPage(Page): class CurrencySelectionPage(Page):
@ -13,53 +16,79 @@ class CurrencySelectionPage(Page):
super().__init__("Select Currency", page_stack, main_window, parent) super().__init__("Select Currency", page_stack, main_window, parent)
self.update_status = main_window self.update_status = main_window
self.selected_duration = None self.selected_duration = None
self.create_interface_elements()
# Map to link the button object to its currency string value
self.button_to_currency = {}
self.create_initial_elements()
self.button_reverse.setVisible(True) self.button_reverse.setVisible(True)
self.button_next.setVisible(False) self.button_next.setVisible(False)
def create_interface_elements(self):
def create_initial_elements(self):
self.title = QLabel("Payment Method", self) self.title = QLabel("Payment Method", self)
self.title.setGeometry(QtCore.QRect(510, 30, 250, 40))
self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.button_reverse.clicked.connect(self.reverse) self.button_reverse.clicked.connect(self.reverse)
button_info = [ # container widget to hold the 'block' of buttons
("monero", (545, 75)), self.button_container = QWidget(self)
("bitcoin", (545, 290)),
("lightnering", (545, 180)), # Position of the entire group, not each individual button
("litecoin", (545, 395)) self.button_container.setGeometry(545, 75, 185, 400)
]
# Vertical Layout
self.layout = QVBoxLayout(self.button_container)
# margins avoid cutting off image:
self.layout.setContentsMargins(0, 0, 0, 0)
# Consistent gap between buttons
self.layout.setSpacing(20)
self.layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.buttonGroup = QButtonGroup(self) self.buttonGroup = QButtonGroup(self)
self.buttons = []
for j, (icon_name, position) in enumerate(button_info): # loop through enums and get the relevant image,
for currency_val in ALL_CURRENCIES:
button = QPushButton(self) button = QPushButton(self)
button.setGeometry(position[0], position[1], 185, 75)
# size required for the image
button.setFixedSize(185, 75)
button.setIconSize(QSize(190, 120)) button.setIconSize(QSize(190, 120))
button.setCheckable(True) button.setCheckable(True)
self.buttons.append(button) button.setIcon(QIcon(os.path.join(self.btn_path, f"{currency_val}.png")))
self.buttonGroup.addButton(button, j)
button.setIcon(
QIcon(os.path.join(self.btn_path, f"{icon_name}.png")))
button.clicked.connect(self.on_currency_selected) button.clicked.connect(self.on_currency_selected)
self.button_to_currency[button] = currency_val
self.buttonGroup.addButton(button)
self.layout.addWidget(button)
def on_currency_selected(self): def on_currency_selected(self):
self.custom_window.navigator.navigate("payment_details") """Sets the value selected to the singleton used across pages"""
payment_details_page = self.custom_window.navigator.get_cached("payment_details")
if payment_details_page is not None:
payment_details_page.selected_duration = self.selected_duration
payment_details_page.selected_currency = self.get_selected_currency()
payment_details_page.check_invoice()
self.update_status.update_status('Loading payment details...')
def get_selected_currency(self): 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)
# set the value as an Enum to be used in a different page,
self.billing_data.currency = self._get_selected_currency()
self.update_status.update_status('Loading payment details...')
self.custom_window.navigator.navigate("payment_details")
def _get_selected_currency(self):
selected_button = self.buttonGroup.checkedButton() selected_button = self.buttonGroup.checkedButton()
if selected_button:
index = self.buttonGroup.id(selected_button) # Retrieve the value from the map using the button object as the key
currencies = ["monero", "bitcoin", "lightning", "litecoin"] raw_selection = self.button_to_currency.get(selected_button)
return currencies[index]
return None # convert to enum:
return Currency(raw_selection)
def reverse(self): def reverse(self):
self.custom_window.navigator.navigate("duration_selection") self.custom_window.navigator.navigate("duration_selection")

View file

@ -1,17 +1,28 @@
# Core
from core.services.payment_phase.payment_state import PaymentData
from gui.v2.infrastructure.take_plumbing_action import take_action
from core.controllers.profile_state.data_filters import get_plans_for_profile
from core.models.Result import Result #, ResultError
# GUI
from gui.v2.ui.styles.payment_styles import DURATION_SELECTION_STYLE, NEXT_BUTTON_STYLE
from gui.v2.ui.pages.Page import Page
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
mainthread = ThreadSafetyTool()
from PyQt6.QtWidgets import QComboBox, QLabel, QPushButton from PyQt6.QtWidgets import QComboBox, QLabel, QPushButton
from PyQt6 import QtCore from PyQt6 import QtCore
from gui.v2.ui.pages.Page import Page
class DurationSelectionPage(Page): class DurationSelectionPage(Page):
def __init__(self, page_stack, main_window=None, parent=None): def __init__(self, page_stack, main_window=None, parent=None):
super().__init__("Select Duration", page_stack, main_window, parent) super().__init__("Select Duration", page_stack, main_window, parent)
self.update_status = main_window self.update_status = main_window
self.create_interface_elements() self.create_intitial_elements()
self.button_reverse.setVisible(True) self.button_reverse.setVisible(True)
def create_interface_elements(self): def create_intitial_elements(self):
self.title = QLabel("Select Duration", self) self.title = QLabel("Select Duration", self)
self.title.setGeometry(QtCore.QRect(530, 30, 210, 40)) self.title.setGeometry(QtCore.QRect(530, 30, 210, 40))
self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
@ -19,74 +30,76 @@ class DurationSelectionPage(Page):
self.combo_box = QComboBox(self) self.combo_box = QComboBox(self)
self.combo_box.setGeometry(200, 200, 400, 60) self.combo_box.setGeometry(200, 200, 400, 60)
self.combo_box.addItems(
["1 month (€1.50)", "3 months (€4.30)", "6 months (€8)", "12 months (€16)"])
self.combo_box.setEditable(False) self.combo_box.setEditable(False)
self.combo_box.setMaxVisibleItems(4) self.combo_box.setMaxVisibleItems(4)
self.combo_box.setDuplicatesEnabled(True) self.combo_box.setDuplicatesEnabled(True)
self.combo_box.setPlaceholderText("Select options") self.combo_box.setPlaceholderText("Select options")
self.combo_box.setStyleSheet(""" self.combo_box.setStyleSheet(DURATION_SELECTION_STYLE)
QComboBox {
font-size: 16px;
padding: 10px;
background-color: #000000;
color: #00ffff;
border: 2px solid #00ffff;
border-radius: 8px;
}
QComboBox::drop-down {
width: 30px;
background-color: #1a1a1a;
border-left: 1px solid #00ffff;
}
QComboBox::down-arrow {
width: 12px;
height: 12px;
color: #00ffff;
}
QComboBox QAbstractItemView {
background-color: #0a0a0a;
border: 1px solid #00ffff;
border-radius: 4px;
selection-background-color: #003333;
selection-color: #00ffff;
}
QComboBox QAbstractItemView::item {
padding: 8px;
border: none;
color: #00ffff;
background-color: #0a0a0a;
}
QComboBox QAbstractItemView::item:hover {
background-color: #1a1a1a;
color: #00ffff;
}
QComboBox QAbstractItemView::item:selected {
background-color: #003333;
color: #00ffff;
}
""")
self.next_button = QPushButton("Next", self) self.next_button = QPushButton("Next", self)
self.next_button.setGeometry(350, 300, 100, 40) self.next_button.setGeometry(350, 300, 100, 40)
self.next_button.setStyleSheet(""" self.next_button.setStyleSheet(NEXT_BUTTON_STYLE)
QPushButton {
font-size: 18px;
padding: 10px;
border: 2px solid #ccc;
border-radius: 10px;
background-color: cyan;
color: black;
font-weight: bold;
}
""")
self.next_button.clicked.connect(self.show_next) self.next_button.clicked.connect(self.show_next)
# Background thread launch:
self.get_data()
def get_data(self):
"""Launches background thread to get data"""
# get profile id:
profile_id = int(self.update_status.current_profile_id)
# setup singleton
self.billing_data = PaymentData(instance_id=profile_id)
# Thread SQL in background:
take_action(
action=get_plans_for_profile,
ui_caller=self.populate_with_data,
profile_id=profile_id,
)
@mainthread
def populate_with_data(self, result: Result):
if not result.valid:
self.show_a_popup(result.message)
return
self.choice_to_id = {}
self.dict_of_choices = result.data
if not self.dict_of_choices:
self.show_a_popup("There is no plan data. Go sync or remove the database and try again.")
return
if not isinstance(self.dict_of_choices, dict):
self.show_a_popup("Plan data is in an invalid format.")
return
for plan_string, plan_id in self.dict_of_choices.items():
self.combo_box.addItem(str(plan_string))
def show_next(self): def show_next(self):
self.custom_window.navigator.navigate("currency_selection") # get current selection:
current_choice = self.combo_box.currentText()
# which plan id is that:
plan_id = self.dict_of_choices[current_choice]
# save to singleton across pages:
self.billing_data.plan_id = plan_id
# go to next page:
currency_page = self.custom_window.navigator.get_cached("currency_selection") currency_page = self.custom_window.navigator.get_cached("currency_selection")
if currency_page is not None: if currency_page is not None:
currency_page.selected_duration = self.combo_box.currentText() self.custom_window.navigator.navigate("currency_selection")
def reverse(self): def reverse(self):
self.custom_window.navigator.navigate("id") self.custom_window.navigator.navigate("id")
# currency_page.selected_duration
# self.combo_box.addItems(
# ["1 month (€1.50)", "3 months (€4.30)", "6 months (€8)", "12 months (€16)"])

View file

@ -0,0 +1,328 @@
# 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

View file

@ -0,0 +1,56 @@
DURATION_SELECTION_STYLE = """
QComboBox {
font-size: 16px;
padding: 10px;
background-color: #000000;
color: #00ffff;
border: 2px solid #00ffff;
border-radius: 8px;
}
QComboBox::drop-down {
width: 30px;
background-color: #1a1a1a;
border-left: 1px solid #00ffff;
}
QComboBox::down-arrow {
width: 12px;
height: 12px;
color: #00ffff;
}
QComboBox QAbstractItemView {
background-color: #0a0a0a;
border: 1px solid #00ffff;
border-radius: 4px;
selection-background-color: #003333;
selection-color: #00ffff;
}
QComboBox QAbstractItemView::item {
padding: 8px;
border: none;
color: #00ffff;
background-color: #0a0a0a;
}
QComboBox QAbstractItemView::item:hover {
background-color: #1a1a1a;
color: #00ffff;
}
QComboBox QAbstractItemView::item:selected {
background-color: #003333;
color: #00ffff;
}
"""
NEXT_BUTTON_STYLE = """
QPushButton {
font-size: 18px;
padding: 10px;
border: 2px solid #ccc;
border-radius: 10px;
background-color: cyan;
color: black;
font-weight: bold;
}
"""