forked from Support/sp-hydra-veil-gui
Stable Bigbang GUI refactor for Payments of individual profiles. Features: Resume payments, new look, separation of UI from logic, coordinate data locally vs server-side, and put payment polling loop in the background, when navigating off the page.
This commit is contained in:
parent
41ac3a75ff
commit
e1a3b920c9
3 changed files with 213 additions and 69 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
from core.services.subscriptions.check_payment_loop import CheckPaidTimer
|
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.services.subscriptions.subscriptions_by_protocol import create_invoice, save_billing_code, get_profile_subscription
|
||||||
from core.models.enum_models import PaymentType
|
from core.models.enum_models import PaymentType
|
||||||
|
from core.models.Result import Result, ResultError
|
||||||
from core.errors.logger import logger
|
from core.errors.logger import logger
|
||||||
|
|
||||||
# gui
|
# gui
|
||||||
|
|
@ -103,5 +104,25 @@ def save_and_begin_timer(
|
||||||
connection_observer=connection_observer,
|
connection_observer=connection_observer,
|
||||||
profile_id=profile_id
|
profile_id=profile_id
|
||||||
)
|
)
|
||||||
|
logger.info("starting polling from GUI action..")
|
||||||
|
check_paid_timer.start_polling()
|
||||||
return True # this is just that it started
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,43 +5,135 @@ from core.models.enum_models import Currency
|
||||||
from core.models.Result import Result
|
from core.models.Result import Result
|
||||||
|
|
||||||
# GUI
|
# 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.pages.Page import Page
|
||||||
from gui.v2.ui.popups.qrcode_dialog import QRCodeDialog
|
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:
|
# Bring background threads to the main thread UI:
|
||||||
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
|
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
|
||||||
mainthread = ThreadSafetyTool()
|
mainthread = ThreadSafetyTool()
|
||||||
|
|
||||||
|
# generic
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from PyQt6 import QtCore, QtWidgets
|
from PyQt6 import QtCore, QtWidgets
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||||
QPushButton, QWidget, QApplication
|
QPushButton, QWidget, QApplication, QFrame, QSpacerItem, QSizePolicy
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
# For formatting errors graphically:
|
||||||
class DataError(Enum):
|
class DataError(Enum):
|
||||||
FORMAT: "format"
|
FORMAT: "format"
|
||||||
BLANK: "blank"
|
BLANK: "blank"
|
||||||
|
|
||||||
|
|
||||||
class PaymentDetailsPage(Page):
|
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):
|
def __init__(self, page_stack, main_window=None, parent=None):
|
||||||
super().__init__("Payment Details", page_stack, main_window, parent)
|
super().__init__("Payment Details", page_stack, main_window, parent)
|
||||||
self.update_status = main_window
|
self.update_status = main_window
|
||||||
|
|
||||||
self.fields = {}
|
self.fields = {}
|
||||||
self.ticketing = False
|
self.ticketing = False
|
||||||
|
self.polling = False
|
||||||
|
|
||||||
|
self.setStyleSheet(PAYMENT_STYLE_SHEET)
|
||||||
|
|
||||||
|
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||||
|
|
||||||
self.create_initial_elements()
|
self.create_initial_elements()
|
||||||
self.button_reverse.setVisible(True)
|
self.button_reverse.setVisible(True)
|
||||||
self.begin_invoice()
|
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):
|
def begin_invoice(self):
|
||||||
""""Fetches an Invoice from GUI's actions modules"""
|
""""Fetches an Invoice from GUI's actions modules"""
|
||||||
self.update_status.update_status('Getting Invoice...')
|
self.update_status.update_status('Getting Invoice...')
|
||||||
|
|
@ -61,77 +153,46 @@ class PaymentDetailsPage(Page):
|
||||||
|
|
||||||
if error_msg:
|
if error_msg:
|
||||||
self.show_a_popup(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")
|
def stop_or_start_check(self):
|
||||||
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
"""Toggle On/Off Polling"""
|
||||||
self.title.setStyleSheet("font-size: 20px; font-weight: bold; margin-bottom: 10px;")
|
# If it's on:
|
||||||
main_layout.addWidget(self.title)
|
if self.polling:
|
||||||
|
stopped, message = stop_payment_timer()
|
||||||
|
|
||||||
# takes field_key (string) instead of field_index (int)
|
if stopped:
|
||||||
def add_info_row(label_text, field_key=None, show_copy=False):
|
self.update_status.update_status("Stopped checking!")
|
||||||
row_layout = QHBoxLayout()
|
self.stop_button.setDisabled(True)
|
||||||
|
self.switch_to_background_button.setDisabled(True)
|
||||||
|
|
||||||
lbl = QLabel(label_text)
|
# regardless of if it worked or not:
|
||||||
lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
if message:
|
||||||
row_layout.addWidget(lbl)
|
self.show_a_popup(str(message))
|
||||||
|
|
||||||
if field_key is not None:
|
# Turn it back on if off:
|
||||||
edit = QLineEdit()
|
else:
|
||||||
edit.setReadOnly(True)
|
self.begin_invoice()
|
||||||
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:
|
def background_fetch_mode(self):
|
||||||
btn = QPushButton()
|
"""Continue checking if paid, but navigate back to the menu page"""
|
||||||
btn.setFixedSize(40, 40)
|
if self.polling:
|
||||||
btn.setIcon(QIcon(os.path.join(self.btn_path, "paste_button.png")))
|
self.custom_window.navigator.navigate("menu")
|
||||||
# Pass the string key to the copy function
|
else:
|
||||||
btn.clicked.connect(lambda checked, key=field_key: self.copy_text(key))
|
message = "It was not polling to begin with."
|
||||||
row_layout.addWidget(btn)
|
self.show_a_popup(message)
|
||||||
|
|
||||||
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):
|
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
|
# Diagnosis
|
||||||
if whats_wrong == DataError.FORMAT:
|
if whats_wrong == DataError.FORMAT:
|
||||||
error_msg = f"The {whats_it_called} is in the wrong 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):
|
def copy_text(self, key: str):
|
||||||
"""Copy/Paste feature"""
|
"""Copy/Paste fields and update the bottom status"""
|
||||||
try:
|
try:
|
||||||
# Use the key to determine if we use elided text or the full raw address
|
# 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()
|
text = self.full_address if key == "address" else self.fields[key].text()
|
||||||
QApplication.clipboard().setText(text)
|
QApplication.clipboard().setText(text)
|
||||||
|
|
||||||
# Descriptive messages based on the key
|
# messages based on the key
|
||||||
msg_map = {
|
msg_map = {
|
||||||
"billing_code": "Billing code copied!",
|
"billing_code": "Billing code copied!",
|
||||||
"address": "Address copied!",
|
"address": "Address copied!",
|
||||||
"amount": "Pay amount copied!"
|
"amount": "Pay amount copied!"
|
||||||
}
|
}
|
||||||
self.update_status.update_status(msg_map.get(key, "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:
|
except Exception as e:
|
||||||
self.show_a_popup(str(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')
|
||||||
|
|
@ -219,7 +279,7 @@ class PaymentDetailsPage(Page):
|
||||||
|
|
||||||
|
|
||||||
def fill_in_invoice_data(self, invoice: SubscriptionInvoice):
|
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:
|
# Extract from the already validated object:
|
||||||
billing_code = str(invoice.billing_code)
|
billing_code = str(invoice.billing_code)
|
||||||
|
|
@ -258,6 +318,10 @@ class PaymentDetailsPage(Page):
|
||||||
# Now that we have the data populated, show the QR code:
|
# Now that we have the data populated, show the QR code:
|
||||||
self.qr_code_button.setDisabled(False)
|
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
|
@mainthread
|
||||||
def timer_updates_gui(self, paid: bool, stopped: bool, message: str):
|
def timer_updates_gui(self, paid: bool, stopped: bool, message: str):
|
||||||
|
|
@ -305,11 +369,13 @@ class PaymentDetailsPage(Page):
|
||||||
|
|
||||||
|
|
||||||
def reverse(self):
|
def reverse(self):
|
||||||
|
"""Navigate away from the page, depending on which payment flow."""
|
||||||
if self.ticketing:
|
if self.ticketing:
|
||||||
self.ticketing = False
|
self.ticketing = False
|
||||||
self.custom_window.navigator.navigate("ticket_crypto_picker")
|
self.custom_window.navigator.navigate("ticket_crypto_picker")
|
||||||
else:
|
else:
|
||||||
self.custom_window.navigator.navigate("currency_selection")
|
self.custom_window.navigator.navigate("menu")
|
||||||
|
|
||||||
|
|
||||||
def _get_currency(self):
|
def _get_currency(self):
|
||||||
"""Get the currency from prior cached PaymentData"""
|
"""Get the currency from prior cached PaymentData"""
|
||||||
|
|
|
||||||
57
gui/v2/ui/styles/css/payment_styles.py
Normal file
57
gui/v2/ui/styles/css/payment_styles.py
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
"""
|
||||||
Loading…
Reference in a new issue