forked from Support/sp-hydra-veil-gui
475 lines
16 KiB
Python
475 lines
16 KiB
Python
# core
|
|
from core.services.subscriptions.subscriptions_by_protocol import paying_for_tickets, get_current_billing_data
|
|
from core.services.payment_phase.payment_state import PaymentData
|
|
from core.models.pydantic_models.ServerReply import SubscriptionInvoice
|
|
from core.models.invoice.TicketInvoice import TicketInvoice
|
|
from core.models.enum_models import Currency
|
|
from core.models.Result import Result
|
|
from core.observers.Notifications import Notifications, EventType
|
|
|
|
# GUI
|
|
from gui.v2.actions.payment_actions import (
|
|
start_polling,
|
|
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
|
|
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,
|
|
)
|
|
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.
|
|
|
|
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,
|
|
):
|
|
super().__init__("Payment Details", page_stack, main_window, parent)
|
|
self.update_status = main_window
|
|
|
|
self.fields = {}
|
|
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):
|
|
"""Refresh data. This runs every time the widget is displayed"""
|
|
|
|
# Call the parent class showEvent first
|
|
super().showEvent(event)
|
|
|
|
self.ticketing = paying_for_tickets()
|
|
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...")
|
|
self.profile_id = int(self.update_status.current_profile_id)
|
|
|
|
print(f"profile_id is {self.profile_id}")
|
|
|
|
if not self.ticketing and not self.profile_id:
|
|
self.show_a_popup(f"Missing a profile id for an individual profile.")
|
|
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:
|
|
self.custom_window.navigator.navigate("currency_selection")
|
|
return # error is already displayed in subfunction.
|
|
|
|
# launches background thread tasks:
|
|
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)
|
|
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 stop_or_start_check(self):
|
|
"""Toggle On/Off Polling"""
|
|
# If it's on:
|
|
if self.polling:
|
|
timer_result = stop_payment_timer()
|
|
|
|
message = timer_result.message
|
|
|
|
if not timer_result.valid:
|
|
self.update_status.update_status("Stopped checking!")
|
|
self.stop_button.setDisabled(True)
|
|
self.switch_to_background_button.setDisabled(True)
|
|
|
|
# regardless of if it worked or not:
|
|
if message:
|
|
self.show_a_popup(str(message))
|
|
|
|
self.custom_window.navigator.navigate("menu")
|
|
|
|
# # Turn it back on if off:
|
|
# else:
|
|
# self.begin_invoice()
|
|
|
|
def background_fetch_mode(self):
|
|
"""Continue checking if paid, but navigate back to the menu page"""
|
|
if self.polling:
|
|
notification = "Checking if Paid in Background.."
|
|
Notifications.emit(EventType.STATUS_UPDATE, notification)
|
|
self.custom_window.navigator.navigate("menu")
|
|
else:
|
|
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!",
|
|
}
|
|
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")
|
|
|
|
@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.show_a_popup("Missing Invoice from API call's data")
|
|
self.custom_window.navigator.navigate("id")
|
|
return
|
|
|
|
# Fill in UI:
|
|
self.fill_in_invoice_data(invoice)
|
|
|
|
# Start polling:
|
|
error_msg = start_polling(
|
|
billing_code=invoice.billing_code,
|
|
profile_id=int(self.update_status.current_profile_id),
|
|
ui_caller=self.timer_updates_gui
|
|
)
|
|
if error_msg:
|
|
self.show_a_popup(error_msg)
|
|
else:
|
|
self.update_status.update_status("Invoice generated. Awaiting payment...")
|
|
|
|
def fill_in_invoice_data(self, invoice: SubscriptionInvoice):
|
|
"""Extract and populate invoice data into the labels"""
|
|
|
|
print(f"filling in {invoice}")
|
|
|
|
# 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)
|
|
|
|
if self.currency:
|
|
self.fields["currency"].setText(self.currency.value)
|
|
|
|
# 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, billing_code: str):
|
|
"""Callback for the background thread timer to repeatedly update the UI"""
|
|
|
|
# CONTINUED UPDATES
|
|
if message and isinstance(message, str):
|
|
self.update_status.update_status(message)
|
|
|
|
# PAID
|
|
if paid:
|
|
self.update_status.update_status("Payment received.")
|
|
if self.ticketing:
|
|
self.ticket_payment_confirmed()
|
|
else:
|
|
self.regular_payment_confirmed(billing_code=billing_code)
|
|
|
|
# NOT PAID BUT ENDED
|
|
elif not paid and stopped:
|
|
self._display_it_stopped(message)
|
|
|
|
|
|
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):
|
|
"""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("menu")
|
|
|
|
def _get_currency(self):
|
|
"""Get the currency from prior cached Payment Data"""
|
|
|
|
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,
|
|
)
|
|
|
|
# get the singleton (this was created on a previous page)
|
|
data_result = get_current_billing_data(profile_id=self.profile_id)
|
|
|
|
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
|
|
|
|
print(f"Inside payments screen GUI of _get_currency, the billing_singleton is {billing_singleton}")
|
|
|
|
if not billing_singleton:
|
|
message = "Error, no billing data"
|
|
self.show_a_popup(str(message))
|
|
|
|
currency = billing_singleton.currency
|
|
|
|
if not currency:
|
|
return None
|
|
# self.show_a_popup('Missing Currency')
|
|
|
|
if not isinstance(currency, Currency):
|
|
self.display_error(
|
|
whats_wrong=DataError.FORMAT,
|
|
whats_it_called="currency",
|
|
the_thing=currency,
|
|
)
|
|
|
|
return currency
|
|
|
|
|
|
def regular_payment_confirmed(self, billing_code: str):
|
|
"""Conclude a regular payment by putting the billing code on the final confirmation window."""
|
|
payment_page = self.custom_window.navigator.get_cached("payment_confirmed")
|
|
if payment_page is not None:
|
|
for line in self.text_fields:
|
|
line.setText('')
|
|
payment_page.set_billing_code(billing_code)
|
|
else:
|
|
print("PaymentConfirmed page not found")
|
|
|
|
def ticket_payment_confirmed(self):
|
|
"""Conclude a ticket payment by forwarding to the prep page."""
|
|
# self.ticketing = False
|
|
self.update_status.update_status('Payment received.')
|
|
self.custom_window.navigator.navigate("ticket_prep")
|
|
prep_page = self.custom_window.navigator.get_cached("ticket_prep")
|
|
if prep_page is not None:
|
|
prep_page.start_prep()
|
|
|
|
|
|
|
|
|