Spaces:
Sleeping
Sleeping
| from flask import Flask, render_template_string, request, redirect, url_for, jsonify | |
| from flask_sqlalchemy import SQLAlchemy | |
| import uuid | |
| from datetime import datetime | |
| import requests | |
| import time | |
| import threading | |
| from cryptography.fernet import Fernet | |
| import os | |
| app = Flask(__name__) | |
| app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///astrapay.db' | |
| app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False | |
| app.config['SECRET_KEY'] = 'your-secret-key-here' | |
| db = SQLAlchemy(app) | |
| def get_encryption_key(): | |
| """Get or generate encryption key""" | |
| key_file = 'encryption.key' | |
| if os.path.exists(key_file): | |
| with open(key_file, 'rb') as f: | |
| return f.read() | |
| else: | |
| key = Fernet.generate_key() | |
| with open(key_file, 'wb') as f: | |
| f.write(key) | |
| return key | |
| ENCRYPTION_KEY = get_encryption_key() | |
| cipher_suite = Fernet(ENCRYPTION_KEY) | |
| def encrypt_value(value): | |
| """Encrypt a string value""" | |
| if isinstance(value, str): | |
| value = value.encode() | |
| return cipher_suite.encrypt(value).decode() | |
| def decrypt_value(encrypted_value): | |
| """Decrypt an encrypted string value""" | |
| return cipher_suite.decrypt(encrypted_value.encode()).decode() | |
| ENCRYPTED_CONNECT_SID = encrypt_value('s%3AeAVq_RB9TcaxBH_VSqYlYPiaYMlm1PbS.kFS%2BOEI4zH5ZS8VLv1dExjTe7dMiCXvNYeT4YG%2FoV04') | |
| ENCRYPTED_API_URL = encrypt_value('https://astra-bank-moh1812.replit.app/api/transactions') | |
| ENCRYPTED_REFERER = encrypt_value('https://astra-bank-moh1812.replit.app/') | |
| ENCRYPTED_ETAG = encrypt_value('W/"6e3-L0zHI4rHMa4nHmyewyA/4y+lL6c"') | |
| class PaymentLink(db.Model): | |
| id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4())) | |
| amount = db.Column(db.Integer, nullable=False) | |
| description = db.Column(db.String(500), nullable=False) | |
| created_at = db.Column(db.DateTime, default=datetime.utcnow) | |
| paid = db.Column(db.Boolean, default=False) | |
| def __repr__(self): | |
| return f'<PaymentLink {self.id}: {self.amount} Astras>' | |
| def get_transactions(): | |
| url = decrypt_value(ENCRYPTED_API_URL) | |
| referer = decrypt_value(ENCRYPTED_REFERER) | |
| etag = decrypt_value(ENCRYPTED_ETAG) | |
| connect_sid = decrypt_value(ENCRYPTED_CONNECT_SID) | |
| headers = { | |
| 'accept': '*/*', | |
| 'accept-language': 'en-US,en;q=0.9', | |
| 'if-none-match': etag, | |
| 'priority': 'u=1, i', | |
| 'referer': referer, | |
| 'sec-ch-ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"', | |
| 'sec-ch-ua-mobile': '?0', | |
| 'sec-ch-ua-platform': '"macOS"', | |
| 'sec-fetch-dest': 'empty', | |
| 'sec-fetch-mode': 'cors', | |
| 'sec-fetch-site': 'same-origin', | |
| 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' | |
| } | |
| cookies = { | |
| 'sidebar_state': 'true', | |
| 'connect.sid': connect_sid | |
| } | |
| try: | |
| response = requests.get(url, headers=headers, cookies=cookies, timeout=10) | |
| return response.json() | |
| except Exception as e: | |
| print(f"Error fetching transactions: {e}") | |
| return [] | |
| def check_for_payment(amount, existing_transaction_ids): | |
| """Check for new transactions matching the payment amount""" | |
| try: | |
| transactions = get_transactions() | |
| for transaction in transactions: | |
| transaction_id = transaction.get('id') | |
| if (transaction_id not in existing_transaction_ids and | |
| transaction.get('amount') == amount and | |
| transaction.get('transactionType') == 'received'): | |
| return transaction | |
| return None | |
| except Exception as e: | |
| print(f"Error checking for payment: {e}") | |
| return None | |
| with app.app_context(): | |
| db.create_all() | |
| HOME_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AstraPay</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| height: 100vh; | |
| background-color: #000000; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-family: Arial, sans-serif; | |
| } | |
| .container { | |
| text-align: center; | |
| } | |
| .title { | |
| color: #ffffff; | |
| font-size: 4rem; | |
| font-weight: bold; | |
| text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.3); | |
| margin-bottom: 2rem; | |
| } | |
| .button { | |
| background-color: #ffffff; | |
| color: #000000; | |
| border: none; | |
| padding: 1rem 2rem; | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-decoration: none; | |
| display: inline-block; | |
| } | |
| .button:hover { | |
| background-color: #f0f0f0; | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 8px rgba(255, 255, 255, 0.2); | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| z-index: 1; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| } | |
| .modal-content { | |
| background-color: #1a1a1a; | |
| margin: 15% auto; | |
| padding: 2rem; | |
| border-radius: 8px; | |
| width: 90%; | |
| max-width: 500px; | |
| border: 1px solid #333; | |
| } | |
| .modal-header { | |
| color: #ffffff; | |
| margin-bottom: 1.5rem; | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| } | |
| .form-group { | |
| margin-bottom: 1.5rem; | |
| } | |
| .form-label { | |
| display: block; | |
| color: #ffffff; | |
| margin-bottom: 0.5rem; | |
| font-weight: bold; | |
| } | |
| .form-input { | |
| width: 100%; | |
| padding: 0.75rem; | |
| border: 1px solid #555; | |
| border-radius: 4px; | |
| background-color: #2a2a2a; | |
| color: #ffffff; | |
| font-size: 1rem; | |
| } | |
| .form-input:focus { | |
| outline: none; | |
| border-color: #ffffff; | |
| } | |
| .form-textarea { | |
| width: 100%; | |
| padding: 0.75rem; | |
| border: 1px solid #555; | |
| border-radius: 4px; | |
| background-color: #2a2a2a; | |
| color: #ffffff; | |
| font-size: 1rem; | |
| min-height: 100px; | |
| resize: vertical; | |
| } | |
| .form-textarea:focus { | |
| outline: none; | |
| border-color: #ffffff; | |
| } | |
| .modal-buttons { | |
| display: flex; | |
| gap: 1rem; | |
| justify-content: flex-end; | |
| margin-top: 2rem; | |
| } | |
| .btn-secondary { | |
| background-color: #555; | |
| color: #ffffff; | |
| border: none; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| } | |
| .btn-secondary:hover { | |
| background-color: #666; | |
| } | |
| .banner { | |
| background: rgba(255, 193, 7, 0.15); | |
| border: 1px solid rgba(255, 193, 7, 0.3); | |
| border-radius: 8px; | |
| padding: 0.75rem 1.5rem; | |
| margin-bottom: 2rem; | |
| color: #ffc107; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| text-align: center; | |
| max-width: 500px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1 class="title">AstraPay</h1> | |
| <div class="banner"> | |
| Payments May Reset Daily. Ensure your link works. | |
| </div> | |
| <button class="button" onclick="openModal()">Create Payment Link</button> | |
| </div> | |
| <!-- Modal --> | |
| <div id="paymentModal" class="modal"> | |
| <div class="modal-content"> | |
| <div class="modal-header">Create Payment Link</div> | |
| <div class="form-group"> | |
| <label class="form-label" for="astras">Number of Astras</label> | |
| <input type="number" id="astras" class="form-input" min="1" placeholder="Enter number of astras" required> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label" for="description">Description</label> | |
| <textarea id="description" class="form-textarea" placeholder="Enter payment description" required></textarea> | |
| </div> | |
| <div class="modal-buttons"> | |
| <button class="btn-secondary" onclick="closeModal()">Cancel</button> | |
| <button class="button" onclick="createPaymentLink()">Create Link</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| function openModal() { | |
| document.getElementById('paymentModal').style.display = 'block'; | |
| } | |
| function closeModal() { | |
| document.getElementById('paymentModal').style.display = 'none'; | |
| // Clear form | |
| document.getElementById('astras').value = ''; | |
| document.getElementById('description').value = ''; | |
| } | |
| async function createPaymentLink() { | |
| const astras = document.getElementById('astras').value; | |
| const description = document.getElementById('description').value; | |
| if (!astras || !description.trim()) { | |
| alert('Please fill in all fields'); | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/create-payment-link', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| amount: parseInt(astras), | |
| description: description.trim() | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| // Redirect to the payment link | |
| window.location.href = data.payment_url; | |
| } else { | |
| alert('Error creating payment link: ' + data.error); | |
| } | |
| } catch (error) { | |
| alert('Error creating payment link: ' + error.message); | |
| } | |
| } | |
| // Close modal when clicking outside | |
| window.onclick = function(event) { | |
| const modal = document.getElementById('paymentModal'); | |
| if (event.target == modal) { | |
| closeModal(); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| PAYMENT_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Payment Link - AstraPay</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| min-height: 100vh; | |
| background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| color: #ffffff; | |
| line-height: 1.6; | |
| } | |
| .container { | |
| text-align: center; | |
| max-width: 500px; | |
| padding: 2rem; | |
| } | |
| .title { | |
| font-size: 2.2rem; | |
| font-weight: 600; | |
| margin-bottom: 1.5rem; | |
| letter-spacing: -0.02em; | |
| } | |
| .payment-card { | |
| background: rgba(255, 255, 255, 0.02); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 16px; | |
| padding: 2.5rem; | |
| margin-bottom: 2rem; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .payment-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 1px; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); | |
| } | |
| .amount { | |
| font-size: 3.2rem; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 0.5rem; | |
| letter-spacing: -0.02em; | |
| } | |
| .description { | |
| font-size: 1.1rem; | |
| color: rgba(255, 255, 255, 0.7); | |
| margin-bottom: 2rem; | |
| font-weight: 400; | |
| } | |
| .status { | |
| font-size: 0.95rem; | |
| padding: 0.5rem 1.2rem; | |
| border-radius: 20px; | |
| display: inline-block; | |
| margin-bottom: 2rem; | |
| font-weight: 500; | |
| letter-spacing: 0.01em; | |
| } | |
| .status.pending { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: rgba(255, 255, 255, 0.8); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .status.paid { | |
| background: rgba(0, 255, 136, 0.1); | |
| color: #00ff88; | |
| border: 1px solid rgba(0, 255, 136, 0.3); | |
| } | |
| .pay-button { | |
| background: linear-gradient(135deg, #ffffff 0%, #f8f8f8 100%); | |
| color: #000000; | |
| border: none; | |
| padding: 1rem 2.5rem; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| letter-spacing: 0.01em; | |
| box-shadow: 0 4px 20px rgba(255, 255, 255, 0.1); | |
| } | |
| .pay-button:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 30px rgba(255, 255, 255, 0.2); | |
| } | |
| .pay-button:disabled { | |
| background: rgba(255, 255, 255, 0.3); | |
| color: rgba(255, 255, 255, 0.5); | |
| cursor: not-allowed; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .confirmation-text { | |
| font-size: 1.2rem; | |
| color: #00ff88; | |
| font-weight: 500; | |
| margin-top: 1rem; | |
| } | |
| .sender-email { | |
| font-size: 0.9rem; | |
| color: rgba(255, 255, 255, 0.6); | |
| margin-top: 0.5rem; | |
| font-family: 'Monaco', 'Consolas', monospace; | |
| } | |
| .timer { | |
| font-size: 0.8rem; | |
| color: rgba(255, 255, 255, 0.5); | |
| margin-top: 1rem; | |
| font-family: 'Monaco', 'Consolas', monospace; | |
| } | |
| .expired { | |
| color: #ff6b6b; | |
| } | |
| .back-link { | |
| color: rgba(255, 255, 255, 0.6); | |
| text-decoration: none; | |
| font-size: 0.9rem; | |
| margin-top: 2rem; | |
| display: inline-block; | |
| font-weight: 500; | |
| transition: color 0.3s ease; | |
| } | |
| .back-link:hover { | |
| color: rgba(255, 255, 255, 0.9); | |
| } | |
| .banner { | |
| background: rgba(255, 193, 7, 0.15); | |
| border: 1px solid rgba(255, 193, 7, 0.3); | |
| border-radius: 8px; | |
| padding: 0.75rem 1.5rem; | |
| margin-bottom: 2rem; | |
| color: #ffc107; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| text-align: center; | |
| } | |
| .payment-modal { | |
| display: none; | |
| position: fixed; | |
| z-index: 1; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.9); | |
| } | |
| .payment-modal-content { | |
| background-color: #1a1a1a; | |
| margin: 15% auto; | |
| padding: 2rem; | |
| border-radius: 8px; | |
| width: 90%; | |
| max-width: 500px; | |
| border: 1px solid #333; | |
| text-align: center; | |
| } | |
| .payment-modal-title { | |
| color: #ffffff; | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| margin-bottom: 1.5rem; | |
| } | |
| .payment-instruction { | |
| color: #cccccc; | |
| font-size: 1.1rem; | |
| margin-bottom: 2rem; | |
| line-height: 1.6; | |
| } | |
| .email-address { | |
| background-color: #2a2a2a; | |
| border: 1px solid #555; | |
| border-radius: 4px; | |
| padding: 1rem; | |
| margin: 1rem 0; | |
| color: #00ff88; | |
| font-family: monospace; | |
| font-size: 1.1rem; | |
| word-break: break-all; | |
| display: inline-block; | |
| } | |
| .payment-modal-buttons { | |
| display: flex; | |
| gap: 1rem; | |
| justify-content: center; | |
| margin-top: 2rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1 class="title">AstraPay</h1> | |
| <div class="banner"> | |
| Payments May Reset Daily. Ensure your link works. | |
| </div> | |
| <div class="payment-card"> | |
| <div class="amount">{{ amount }} Astra{{ 's' if amount > 1 else '' }}</div> | |
| <div class="description">{{ description }}</div> | |
| <div class="status {{ 'paid' if paid else 'pending' }}"> | |
| {{ 'Paid' if paid else 'Pending Payment' }} | |
| </div> | |
| {% if not paid %} | |
| <button class="pay-button" onclick="processPayment()"> | |
| Pay {{ amount }} Astra{{ 's' if amount > 1 else '' }} | |
| </button> | |
| {% else %} | |
| <div style="font-size: 1.2rem; color: #00ff88;">✓ Payment Completed</div> | |
| {% endif %} | |
| </div> | |
| <a href="/" class="back-link">← Back to AstraPay</a> | |
| </div> | |
| <!-- Payment Modal --> | |
| <div id="paymentModal" class="payment-modal"> | |
| <div class="payment-modal-content"> | |
| <div class="payment-modal-title">Complete Payment</div> | |
| <div class="payment-instruction"> | |
| Please send {{ amount }} Astra{{ 's' if amount > 1 else '' }} to the following address: | |
| </div> | |
| <div class="email-address">matthew@astranova.org</div> | |
| <div id="payment-status" class="payment-instruction"> | |
| <div id="waiting-message"> | |
| Waiting for payment confirmation... | |
| <div id="timer" class="timer">5:00</div> | |
| </div> | |
| <div id="payment-received" style="display: none;"> | |
| ✓ Payment of {{ amount }} Astra{{ 's' if amount > 1 else '' }} received! | |
| <br><br> | |
| Please enter your email to confirm: | |
| <br> | |
| <input type="email" id="user-email" class="form-input" placeholder="Enter your email" style="margin-top: 1rem; width: 100%; max-width: 300px;"> | |
| </div> | |
| <div id="payment-expired" style="display: none;"> | |
| Payment window expired. | |
| <br><br> | |
| Please create a new payment link. | |
| </div> | |
| <div id="confirmation-complete" style="display: none;"> | |
| <div class="confirmation-text">Payment Confirmed ✓</div> | |
| <div class="sender-email" id="sender-email-display"></div> | |
| </div> | |
| </div> | |
| <div class="payment-modal-buttons"> | |
| <button class="btn-secondary" onclick="closePaymentModal()">Cancel</button> | |
| <button id="confirm-btn" class="pay-button" onclick="confirmPayment()" style="display: none;">Confirm Payment</button> | |
| <button id="verify-btn" class="pay-button" onclick="verifyEmail()" style="display: none;">Verify Email</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let paymentCheckInterval; | |
| let timerInterval; | |
| let existingTransactionIds = []; | |
| let timeLeft = 300; // 5 minutes in seconds | |
| let paymentTransaction = null; | |
| function processPayment() { | |
| document.getElementById('paymentModal').style.display = 'block'; | |
| timeLeft = 300; // Reset timer | |
| paymentTransaction = null; // Reset payment data | |
| // Initialize payment checking | |
| initPaymentCheck(); | |
| startTimer(); | |
| } | |
| function closePaymentModal() { | |
| document.getElementById('paymentModal').style.display = 'none'; | |
| stopPaymentCheck(); | |
| stopTimer(); | |
| document.getElementById('waiting-message').style.display = 'block'; | |
| document.getElementById('payment-received').style.display = 'none'; | |
| document.getElementById('payment-expired').style.display = 'none'; | |
| document.getElementById('confirmation-complete').style.display = 'none'; | |
| document.getElementById('confirm-btn').style.display = 'none'; | |
| document.getElementById('verify-btn').style.display = 'none'; | |
| document.getElementById('user-email').value = ''; | |
| document.getElementById('timer').classList.remove('expired'); | |
| existingTransactionIds = []; | |
| } | |
| async function initPaymentCheck() { | |
| try { | |
| const response = await fetch('/init-payment-check/{{ payment_id }}'); | |
| const data = await response.json(); | |
| if (data.existing_transaction_ids) { | |
| existingTransactionIds = data.existing_transaction_ids; | |
| startPaymentCheck(); | |
| } else { | |
| console.error('Failed to initialize payment check'); | |
| } | |
| } catch (error) { | |
| console.error('Error initializing payment check:', error); | |
| } | |
| } | |
| function startPaymentCheck() { | |
| paymentCheckInterval = setInterval(async () => { | |
| if (timeLeft <= 0) return; // Don't check if expired | |
| try { | |
| const existingIdsParam = existingTransactionIds.join(','); | |
| const response = await fetch(`/check-payment/{{ payment_id }}?existing_ids=${existingIdsParam}`); | |
| const data = await response.json(); | |
| if (data.payment_received) { | |
| paymentTransaction = data.transaction; | |
| document.getElementById('waiting-message').style.display = 'none'; | |
| document.getElementById('payment-received').style.display = 'block'; | |
| document.getElementById('verify-btn').style.display = 'inline-block'; | |
| stopPaymentCheck(); | |
| } | |
| } catch (error) { | |
| console.log('Error checking payment status:', error); | |
| } | |
| }, 2000); // Check every 2 seconds | |
| } | |
| function stopPaymentCheck() { | |
| if (paymentCheckInterval) { | |
| clearInterval(paymentCheckInterval); | |
| paymentCheckInterval = null; | |
| } | |
| } | |
| function startTimer() { | |
| timerInterval = setInterval(() => { | |
| timeLeft--; | |
| updateTimerDisplay(); | |
| if (timeLeft <= 0) { | |
| stopTimer(); | |
| stopPaymentCheck(); | |
| document.getElementById('waiting-message').style.display = 'none'; | |
| document.getElementById('payment-expired').style.display = 'block'; | |
| document.getElementById('confirm-btn').style.display = 'none'; | |
| document.getElementById('verify-btn').style.display = 'none'; | |
| } | |
| }, 1000); | |
| } | |
| function stopTimer() { | |
| if (timerInterval) { | |
| clearInterval(timerInterval); | |
| timerInterval = null; | |
| } | |
| } | |
| function updateTimerDisplay() { | |
| const minutes = Math.floor(timeLeft / 60); | |
| const seconds = timeLeft % 60; | |
| const timerElement = document.getElementById('timer'); | |
| timerElement.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
| if (timeLeft <= 60) { | |
| timerElement.classList.add('expired'); | |
| } | |
| } | |
| function verifyEmail() { | |
| const userEmail = document.getElementById('user-email').value.trim(); | |
| const senderEmail = paymentTransaction ? paymentTransaction.from_email : ''; | |
| if (!userEmail) { | |
| alert('Please enter your email address'); | |
| return; | |
| } | |
| if (userEmail.toLowerCase() === senderEmail.toLowerCase()) { | |
| document.getElementById('verify-btn').style.display = 'none'; | |
| document.getElementById('confirm-btn').style.display = 'inline-block'; | |
| } else { | |
| alert('Email address does not match the payment sender. Please use the email you sent the payment from.'); | |
| document.getElementById('user-email').focus(); | |
| } | |
| } | |
| async function confirmPayment() { | |
| const confirmButton = document.getElementById('confirm-btn'); | |
| confirmButton.disabled = true; | |
| confirmButton.textContent = 'Confirming...'; | |
| try { | |
| const response = await fetch('/pay/{{ payment_id }}', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| } | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| // Hide all other elements and show confirmation | |
| document.getElementById('payment-received').style.display = 'none'; | |
| document.getElementById('confirm-btn').style.display = 'none'; | |
| document.getElementById('confirmation-complete').style.display = 'block'; | |
| // Show sender email if available | |
| if (paymentTransaction && paymentTransaction.from_email) { | |
| document.getElementById('sender-email-display').textContent = `From: ${paymentTransaction.from_email}`; | |
| } | |
| stopTimer(); | |
| // Redirect after a delay | |
| setTimeout(() => { | |
| location.reload(); | |
| }, 3000); | |
| } else { | |
| alert('Payment confirmation failed: ' + data.error); | |
| confirmButton.disabled = false; | |
| confirmButton.textContent = 'Confirm Payment'; | |
| } | |
| } catch (error) { | |
| alert('Payment confirmation failed: ' + error.message); | |
| confirmButton.disabled = false; | |
| confirmButton.textContent = 'Confirm Payment'; | |
| } | |
| } | |
| // Close modal when clicking outside | |
| window.onclick = function(event) { | |
| const modal = document.getElementById('paymentModal'); | |
| if (event.target == modal) { | |
| closePaymentModal(); | |
| } | |
| } | |
| // Stop checking when page unloads | |
| window.onbeforeunload = function() { | |
| stopPaymentCheck(); | |
| stopTimer(); | |
| }; | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def home(): | |
| return render_template_string(HOME_TEMPLATE) | |
| def create_payment_link(): | |
| try: | |
| data = request.get_json() | |
| if not data or 'amount' not in data or 'description' not in data: | |
| return jsonify({'error': 'Missing required fields'}), 400 | |
| amount = data['amount'] | |
| description = data['description'] | |
| if not isinstance(amount, int) or amount <= 0: | |
| return jsonify({'error': 'Invalid amount'}), 400 | |
| if not description or len(description.strip()) == 0: | |
| return jsonify({'error': 'Description cannot be empty'}), 400 | |
| payment_link = PaymentLink( | |
| amount=amount, | |
| description=description.strip() | |
| ) | |
| db.session.add(payment_link) | |
| db.session.commit() | |
| payment_url = url_for('view_payment', payment_id=payment_link.id, _external=True) | |
| return jsonify({ | |
| 'success': True, | |
| 'payment_id': payment_link.id, | |
| 'payment_url': payment_url | |
| }) | |
| except Exception as e: | |
| db.session.rollback() | |
| return jsonify({'error': str(e)}), 500 | |
| def init_payment_check(payment_id): | |
| """Initialize payment checking by storing current transaction state""" | |
| try: | |
| payment_link = PaymentLink.query.get_or_404(payment_id) | |
| transactions = get_transactions() | |
| existing_ids = [tx.get('id') for tx in transactions] | |
| return jsonify({ | |
| 'existing_transaction_ids': existing_ids, | |
| 'amount': payment_link.amount | |
| }) | |
| except Exception as e: | |
| return jsonify({'error': str(e)}), 500 | |
| def check_payment_status(payment_id): | |
| """Check if payment has been received""" | |
| try: | |
| payment_link = PaymentLink.query.get_or_404(payment_id) | |
| existing_ids_str = request.args.get('existing_ids', '') | |
| if existing_ids_str: | |
| existing_ids = existing_ids_str.split(',') | |
| else: | |
| transactions = get_transactions() | |
| existing_ids = [tx.get('id') for tx in transactions] | |
| payment_transaction = check_for_payment(payment_link.amount, existing_ids) | |
| if payment_transaction: | |
| return jsonify({ | |
| 'payment_received': True, | |
| 'transaction': { | |
| 'id': payment_transaction.get('id'), | |
| 'amount': payment_transaction.get('amount'), | |
| 'from_email': payment_transaction.get('counterpartEmail'), | |
| 'description': payment_transaction.get('description') | |
| } | |
| }) | |
| else: | |
| return jsonify({'payment_received': False}) | |
| except Exception as e: | |
| return jsonify({'error': str(e)}), 500 | |
| def process_payment(payment_id): | |
| try: | |
| payment_link = PaymentLink.query.get_or_404(payment_id) | |
| if payment_link.paid: | |
| return jsonify({'error': 'Payment already completed'}), 400 | |
| payment_link.paid = True | |
| db.session.commit() | |
| return jsonify({'success': True, 'message': 'Payment confirmed successfully'}) | |
| except Exception as e: | |
| db.session.rollback() | |
| return jsonify({'error': str(e)}), 500 | |
| def view_payment(payment_id): | |
| payment_link = PaymentLink.query.get_or_404(payment_id) | |
| return render_template_string(PAYMENT_TEMPLATE, | |
| amount=payment_link.amount, | |
| description=payment_link.description, | |
| paid=payment_link.paid, | |
| payment_id=payment_link.id) | |
| if __name__ == '__main__': | |
| app.run(debug=True, host='0.0.0.0', port=7860) | |