sp-hydra-veil-gui/gui/v2/ui/pages/payment_details_page.py
2026-06-13 16:19:11 -04:00

419 lines
17 KiB
Python
Executable file

import os
from PyQt6.QtWidgets import QApplication, QLabel, QLineEdit, QPushButton
from PyQt6.QtGui import QIcon, QPixmap
from PyQt6.QtCore import QSize, QTimer
from PyQt6 import QtCore
from gui.v2.ui.pages.Page import Page
from gui.v2.ui.popups.qrcode_dialog import QRCodeDialog
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
from gui.v2.workers.worker_thread import WorkerThread
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.text_fields = []
self.invoice_data = {}
self.selected_duration = None
self.selected_currency = None
self.ticketing = False
self.temp_billing_code = None
self._ticket_check_running = False
self._ticket_worker = None
self.ticket_poll_timer = QTimer(self)
self.ticket_poll_timer.timeout.connect(self._tick_check_paid)
self.create_interface_elements()
self.button_reverse.setVisible(True)
def create_interface_elements(self):
self.title = QLabel("Payment Details", self)
self.title.setGeometry(QtCore.QRect(530, 30, 210, 40))
self.title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.button_reverse.clicked.connect(self.reverse)
self.qr_code_button = QPushButton(self)
self.qr_code_button.setGeometry(360, 435, 50, 50)
qr_icon = QIcon(os.path.join(self.btn_path, "qr-code.png"))
self.qr_code_button.setIcon(qr_icon)
self.qr_code_button.setIconSize(self.qr_code_button.size())
self.qr_code_button.clicked.connect(self.show_qr_code)
self.qr_code_button.setToolTip("Show QR Code")
self.qr_code_button.setDisabled(True)
labels_info = [
("Your new billing ID", None, (20, 40), (240, 50)),
("", "cuadro400x50", (20, 90), (400, 50)),
("Duration", None, (20, 155), (150, 50)),
("", "cuadro150x50", (200, 155), (150, 50)),
("Amount", None, (10, 220), (150, 50)),
("", "cuadro150x50", (200, 220), (150, 50)),
("Hit this white icon to copy paste the address:",
None, (15, 305), (500, 30)),
("", "cuadro400x50", (20, 350), (400, 50)),
("Hit this icon to Scan the QR code:", None, (15, 450), (360, 30)),
]
for text, image_name, position, size in labels_info:
label = QLabel(self)
label.setGeometry(position[0], position[1], size[0], size[1])
if "Hit" in text:
label.setStyleSheet("font-size: 16px;")
label.setAlignment(
QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter)
else:
label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
if image_name:
pixmap = QPixmap(os.path.join(
self.btn_path, f"{image_name}.png"))
label.setPixmap(pixmap)
label.setScaledContents(True)
else:
label.setText(text)
line_edit_info = [
(20, 90, 400, 50),
(200, 155, 150, 50),
(200, 220, 150, 50),
(20, 350, 400, 50),
]
for x, y, width, height in line_edit_info:
line_edit = QLineEdit(self)
line_edit.setGeometry(x, y, width, height)
line_edit.setReadOnly(True)
self.text_fields.append(line_edit)
line_edit.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
copy_button_info = [
(430, 90, 0),
(360, 220, 2),
(480, 290, 3),
]
for x, y, field_index in copy_button_info:
button = QPushButton(self)
button.setGeometry(x, y, 50, 50)
icon = QIcon(os.path.join(self.btn_path, "paste_button.png"))
button.setIcon(icon)
button.setIconSize(button.size())
button.clicked.connect(
lambda checked, index=field_index: self.copy_text(index))
currency_button_info = [
("monero", (545, 75)),
("bitcoin", (545, 290)),
("lightnering", (545, 180)),
("litecoin", (545, 395))
]
self.currency_display_buttons = []
for icon_name, position in currency_button_info:
button = QPushButton(self)
button.setGeometry(position[0], position[1], 185, 75)
button.setIconSize(QSize(190, 120))
button.setCheckable(True)
button.setEnabled(False)
self.currency_display_buttons.append(button)
button.setIcon(
QIcon(os.path.join(self.btn_path, f"{icon_name}.png")))
def fetch_invoice_duration(self):
duration_month_num = int(self.selected_duration.split()[0])
if duration_month_num == 12:
total_hours = 365 * 24
else:
total_hours = duration_month_num * (30 * 24)
return total_hours
def display_selected_options(self):
if self.selected_duration:
self.text_fields[1].setText(self.selected_duration)
self.text_fields[1].setStyleSheet('font-size: 13px;')
if self.selected_currency:
currency_index_map = {
"monero": 0,
"bitcoin": 1,
"lightning": 2,
"litecoin": 3
}
currency_index = currency_index_map.get(self.selected_currency, -1)
if currency_index >= 0:
for i, button in enumerate(self.currency_display_buttons):
button.setChecked(i == currency_index)
button.setEnabled(i == currency_index)
def on_request_invoice(self):
total_hours = self.fetch_invoice_duration()
if self.selected_currency is None:
self.update_status.update_status('No currency selected')
return
profile_data = {
'id': int(self.update_status.current_profile_id),
'duration': total_hours,
'currency': 'xmr' if self.selected_currency == 'monero' else 'btc' if self.selected_currency == 'bitcoin' else 'btc-ln' if self.selected_currency == 'lightning' else 'ltc' if self.selected_currency == 'litecoin' else None
}
self.update_status.update_status('Generating Invoice...')
self.worker_thread = WorkerThread(
'GET_SUBSCRIPTION', profile_data=profile_data)
self.worker_thread.text_output.connect(self.invoice_update_text_output)
self.worker_thread.invoice_output.connect(
self.on_invoice_generation_finished)
self.worker_thread.invoice_finished.connect(self.on_invoice_finished)
self.worker_thread.start()
def invoice_update_text_output(self, text):
self.update_status.update_status(text)
if 'No compatible' in text:
for line in self.text_fields:
line.setText('')
def store_invoice_data(self, billing_details: dict, duration: int):
self.invoice_data = {
'billing_details': billing_details,
'duration': duration
}
def check_invoice(self):
self.display_selected_options()
total_hours = self.fetch_invoice_duration()
if self.invoice_data and self.invoice_data['duration'] == total_hours:
self.update_status.update_status('Checking invoice status...')
self.check_invoice_status(
self.invoice_data['billing_details'].billing_code)
else:
self.on_request_invoice()
def check_invoice_status(self, billing_code: str):
self.worker_thread = WorkerThread('CHECK_INVOICE_STATUS', profile_data={
'billing_code': billing_code})
self.worker_thread.invoice_finished.connect(
self.on_invoice_status_finished)
self.worker_thread.start()
def on_invoice_status_finished(self, result):
if result:
self.parse_invoice_data(self.invoice_data['billing_details'])
else:
self.update_status.update_status(
'Invoice has expired. Generating new invoice...')
self.on_request_invoice()
def on_invoice_generation_finished(self, billing_details: object, text: str):
total_hours = self.fetch_invoice_duration()
self.store_invoice_data(billing_details, duration=total_hours)
self.parse_invoice_data(billing_details)
def parse_invoice_data(self, invoice_data: object):
billing_details = {
'billing_code': invoice_data.billing_code
}
if self.selected_currency.lower() == 'lightning':
currency = 'Bitcoin Lightning'
else:
currency = self.selected_currency
preferred_method = next(
(pm for pm in invoice_data.payment_methods if pm.name.lower() == currency.lower()), None)
if preferred_method:
billing_details['due_amount'] = preferred_method.due
billing_details['address'] = preferred_method.address
else:
self.update_status.update_status(
'No payment method found for the selected currency.')
return
billing_values = list(billing_details.values())
text_field_indices = [0, 2, 3]
for i, dict_value in enumerate(billing_values):
if i < 3:
field_index = text_field_indices[i]
if i == 2:
text = str(dict_value)
self.full_address = text
metrics = self.text_fields[field_index].fontMetrics()
width = self.text_fields[field_index].width()
elided_text = metrics.elidedText(
text, QtCore.Qt.TextElideMode.ElideMiddle, width)
self.text_fields[field_index].setText(elided_text)
elif i == 1:
text = str(dict_value)
self.text_fields[field_index].setProperty("fullText", text)
self.text_fields[field_index].setText(text)
font_size = 14
if len(text) > 9:
font_size = 15
if len(text) > 13:
font_size = 10
if len(text) > 16:
font_size = 10
self.text_fields[field_index].setStyleSheet(
f"font-size: {font_size}px; font-weight: bold;")
else:
self.text_fields[field_index].setProperty(
"fullText", str(dict_value))
self.text_fields[field_index].setText(str(dict_value))
self.qr_code_button.setDisabled(False)
self.update_status.update_status(
'Invoice generated. Awaiting payment...')
def on_invoice_finished(self, result):
if result:
self.invoice_data.clear()
self.show_payment_confirmed(self.text_fields[0].text())
return
self.update_status.update_status(
'An error occurred when generating invoice')
def show_payment_confirmed(self, billing_code):
self.custom_window.navigator.navigate("payment_confirmed")
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 find_payment_confirmed_page(self):
return self.custom_window.navigator.get_cached("payment_confirmed")
def copy_text(self, field: int):
try:
original_status = self.update_status.status_label.text()
original_status = original_status.replace("Status: ", "")
if int(field) == 3:
text = self.full_address
else:
text = self.text_fields[int(field)].text()
QApplication.clipboard().setText(text)
if field == 0:
self.update_status.update_status(
'Billing code copied to clipboard!')
elif field == 3:
self.update_status.update_status(
'Address copied to clipboard!')
else:
self.update_status.update_status(
'Pay amount copied to clipboard!')
QTimer.singleShot(
2000, lambda: self.update_status.update_status(original_status))
except AttributeError:
self.update_status.update_status(
'No content available for copying')
except Exception as e:
self.update_status.update_status(
f'An error occurred when copying the text')
def reverse(self):
if self.ticket_poll_timer.isActive():
self.ticket_poll_timer.stop()
if self.ticketing:
self.ticketing = False
self.custom_window.navigator.navigate("ticket_crypto_picker")
return
self.custom_window.navigator.navigate("currency_selection")
def set_ticket_invoice(self, invoice, plan_key):
self.ticketing = True
self.temp_billing_code = getattr(invoice, 'temp_billing_code', None)
raw_currency = getattr(invoice, 'selected_currency', None) or ''
self.selected_currency = self._normalize_currency(raw_currency)
self.selected_duration = f"Plan {plan_key}" if plan_key else "Plan"
self._populate_ticket_fields(invoice)
if self.ticket_poll_timer.isActive():
self.ticket_poll_timer.stop()
self.ticket_poll_timer.start(3000)
self.update_status.update_status('Awaiting ticket payment...')
def _populate_ticket_fields(self, invoice):
self.text_fields[0].setText(str(getattr(invoice, 'temp_billing_code', '') or ''))
self.text_fields[1].setText(self.selected_duration)
self.text_fields[1].setStyleSheet('font-size: 13px;')
amount = getattr(invoice, 'due_amount', '') or ''
self.text_fields[2].setText(str(amount))
addr = str(getattr(invoice, 'address', '') or '')
self.full_address = addr
metrics = self.text_fields[3].fontMetrics()
width = self.text_fields[3].width()
elided = metrics.elidedText(addr, QtCore.Qt.TextElideMode.ElideMiddle, width)
self.text_fields[3].setText(elided)
currency_index_map = {'monero': 0, 'bitcoin': 1, 'lightning': 2, 'litecoin': 3}
idx = currency_index_map.get(self.selected_currency, -1)
if idx >= 0:
for i, btn in enumerate(self.currency_display_buttons):
btn.setChecked(i == idx)
btn.setEnabled(i == idx)
self.qr_code_button.setDisabled(False)
def _normalize_currency(self, raw):
if not raw:
return None
raw = str(raw).lower()
m = {
'monero': 'monero', 'xmr': 'monero',
'bitcoin': 'bitcoin', 'btc': 'bitcoin',
'lightning': 'lightning', 'btc-ln': 'lightning', 'ln': 'lightning',
'litecoin': 'litecoin', 'ltc': 'litecoin',
}
return m.get(raw, raw)
def _tick_check_paid(self):
if not self.temp_billing_code or self._ticket_check_running:
return
self._ticket_check_running = True
self._ticket_worker = TicketingWorkerThread('CHECK_PAID', params={
'temp_billing_code': self.temp_billing_code
})
self._ticket_worker.paid.connect(self._on_ticket_paid)
self._ticket_worker.not_paid.connect(self._on_ticket_not_paid)
self._ticket_worker.paid_check_failed.connect(self._on_ticket_check_failed)
self._ticket_worker.error.connect(self._on_ticket_check_failed)
self._ticket_worker.finished.connect(self._on_ticket_check_done)
self._ticket_worker.start()
def _on_ticket_check_done(self):
self._ticket_check_running = False
def _on_ticket_paid(self):
self.ticket_poll_timer.stop()
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()
def _on_ticket_not_paid(self):
self.update_status.update_status('Awaiting ticket payment...')
def _on_ticket_check_failed(self, msg):
self.update_status.update_status(f'Payment check failed: {msg}')
def show_qr_code(self):
full_amount = self.text_fields[2].text()
if hasattr(self, 'full_address') and self.full_address:
currency_type = getattr(self, 'selected_currency', None)
qr_dialog = QRCodeDialog(
self.full_address, full_amount, currency_type, self)
qr_dialog.exec()
else:
self.update_status.update_status(
'No address available for QR code')