forked from Support/sp-hydra-veil-gui
427 lines
17 KiB
Python
Executable file
427 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, QThread
|
|
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()
|
|
|
|
### NEW THREAD SAFETY DEBUG SECTION ####
|
|
# print(f"Timer starting from thread: {QThread.currentThread()}")
|
|
# print(f"Main thread is: {QApplication.instance().thread()}")
|
|
# if QThread.currentThread() != QApplication.instance().thread():
|
|
# print("⚠️ WARNING: Not on main thread!")
|
|
###############################################################
|
|
|
|
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('Not Paid')
|
|
|
|
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')
|