quantum-ai-v2/templates/core/wallet_demo.html
Claude 0714ec21e5 Add comprehensive Stripe webhook testing demo page
- Created /wallet/demo/ page for testing Stripe payment integration
- Real-time balance monitoring and transaction logging
- Interactive payment testing with amount selection
- Live webhook status monitoring and debugging logs
- AJAX-powered balance refresh and payment creation
- Visual indicators for webhook connectivity status
- Detailed session ID and payment link display

This demo page will help debug webhook delivery issues and test
the complete payment flow from session creation to balance updates.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 14:51:08 +05:30

502 lines
15 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}Wallet Demo - Stripe Webhook Testing{% endblock %}
{% block extra_css %}
<style>
.demo-page {
background: var(--gradient-hero);
min-height: calc(100vh - 80px);
padding: clamp(20px, 5vw, 40px);
}
.demo-container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
align-items: start;
}
.demo-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(20px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.demo-title {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #e74c3c;
animation: pulse 2s infinite;
}
.status-indicator.connected {
background: #27ae60;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.balance-display {
font-size: 36px;
font-weight: 800;
color: var(--primary-blue);
text-align: center;
margin: 20px 0;
padding: 20px;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
border-radius: 12px;
border: 2px solid var(--border-light);
}
.user-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin: 20px 0;
padding: 15px;
background: #f8fafc;
border-radius: 8px;
font-size: 14px;
}
.user-info strong {
color: var(--text-primary);
}
.test-amounts {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin: 20px 0;
}
.amount-btn {
padding: 12px 20px;
border: 2px solid var(--primary-blue);
background: transparent;
color: var(--primary-blue);
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.amount-btn:hover {
background: var(--primary-blue);
color: white;
transform: translateY(-1px);
}
.amount-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.control-buttons {
display: flex;
gap: 12px;
margin: 20px 0;
}
.btn-primary {
flex: 1;
padding: 12px 24px;
background: var(--primary-blue);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-1px);
}
.btn-secondary {
flex: 1;
padding: 12px 24px;
background: transparent;
color: var(--primary-blue);
border: 2px solid var(--primary-blue);
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-secondary:hover {
background: var(--primary-blue);
color: white;
}
.transaction-log {
max-height: 300px;
overflow-y: auto;
background: #f8fafc;
border-radius: 8px;
padding: 15px;
margin: 20px 0;
}
.transaction-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #e2e8f0;
font-size: 14px;
}
.transaction-item:last-child {
border-bottom: none;
}
.transaction-amount {
font-weight: 600;
}
.transaction-amount.positive {
color: #27ae60;
}
.transaction-amount.negative {
color: #e74c3c;
}
.webhook-logs {
max-height: 400px;
overflow-y: auto;
background: #1a1a1a;
color: #00ff00;
border-radius: 8px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
}
.log-entry {
margin: 2px 0;
padding: 2px 0;
}
.log-timestamp {
color: #888;
margin-right: 8px;
}
.log-success {
color: #00ff00;
}
.log-error {
color: #ff6b6b;
}
.log-info {
color: #74c0fc;
}
.selected-amount {
background: var(--primary-blue) !important;
color: white !important;
}
@media (max-width: 768px) {
.demo-container {
grid-template-columns: 1fr;
}
.test-amounts {
grid-template-columns: repeat(2, 1fr);
}
.control-buttons {
flex-direction: column;
}
}
</style>
{% endblock %}
{% block content %}
<div class="demo-page">
<div class="demo-container">
<!-- Payment Testing Panel -->
<div class="demo-card">
<h2 class="demo-title">
🧪 Payment Testing
<div class="status-indicator" id="paymentStatus"></div>
</h2>
<div class="balance-display" id="currentBalance">
{{ current_balance|floatformat:2 }} AED
</div>
<div class="user-info">
<div><strong>User ID:</strong> {{ user_id }}</div>
<div><strong>Email:</strong> {{ user_email }}</div>
<div><strong>Environment:</strong> Railway</div>
<div><strong>Webhook:</strong> Active</div>
</div>
<h3>Test Amounts</h3>
<div class="test-amounts">
<button class="amount-btn" data-amount="10">10 AED</button>
<button class="amount-btn" data-amount="50">50 AED</button>
<button class="amount-btn" data-amount="100">100 AED</button>
<button class="amount-btn" data-amount="500">500 AED</button>
</div>
<div class="control-buttons">
<button class="btn-primary" id="createPayment" disabled>
💳 Create Test Payment
</button>
<button class="btn-secondary" id="refreshBalance">
🔄 Refresh Balance
</button>
</div>
<div id="paymentInfo" style="display: none; margin-top: 15px; padding: 15px; background: #e3f2fd; border-radius: 8px; font-size: 14px;">
<strong>Payment Created:</strong><br>
<span id="sessionId"></span><br>
<a href="#" id="paymentLink" target="_blank" style="color: var(--primary-blue); font-weight: 600;">🔗 Complete Payment</a>
</div>
</div>
<!-- Webhook Monitoring Panel -->
<div class="demo-card">
<h2 class="demo-title">
📡 Webhook Monitor
<div class="status-indicator" id="webhookStatus"></div>
</h2>
<h3>Recent Transactions</h3>
<div class="transaction-log" id="transactionLog">
{% for transaction in recent_transactions %}
<div class="transaction-item">
<div>
<div>{{ transaction.description|default:"Wallet Transaction" }}</div>
<div style="font-size: 12px; color: #666;">{{ transaction.created_at|date:"M d, H:i" }}</div>
</div>
<div class="transaction-amount {% if transaction.amount >= 0 %}positive{% else %}negative{% endif %}">
{% if transaction.amount >= 0 %}+{% endif %}{{ transaction.amount|floatformat:2 }} AED
</div>
</div>
{% empty %}
<div style="text-align: center; color: #666; padding: 20px;">
No transactions yet. Create a test payment to see webhook activity.
</div>
{% endfor %}
</div>
<h3>Webhook Logs</h3>
<div class="webhook-logs" id="webhookLogs">
<div class="log-entry log-info">
<span class="log-timestamp">[INIT]</span> Webhook monitoring started...
</div>
<div class="log-entry log-info">
<span class="log-timestamp">[INFO]</span> Endpoint: https://netcop.up.railway.app/stripe/webhook/
</div>
<div class="log-entry log-info">
<span class="log-timestamp">[INFO]</span> Waiting for webhook events...
</div>
</div>
</div>
</div>
</div>
<!-- Include CSRF token for AJAX requests -->
{% csrf_token %}
<script>
let selectedAmount = null;
let pollingInterval = null;
// Get CSRF token
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
setupEventListeners();
startPolling();
updateStatus('webhook', true);
logMessage('System initialized successfully', 'success');
});
function setupEventListeners() {
// Amount button selection
document.querySelectorAll('.amount-btn').forEach(btn => {
btn.addEventListener('click', function() {
selectedAmount = parseFloat(this.dataset.amount);
// Update UI
document.querySelectorAll('.amount-btn').forEach(b => b.classList.remove('selected-amount'));
this.classList.add('selected-amount');
document.getElementById('createPayment').disabled = false;
logMessage(`Selected amount: ${selectedAmount} AED`, 'info');
});
});
// Create payment button
document.getElementById('createPayment').addEventListener('click', createTestPayment);
// Refresh balance button
document.getElementById('refreshBalance').addEventListener('click', refreshBalance);
}
function createTestPayment() {
if (!selectedAmount) {
logMessage('Please select an amount first', 'error');
return;
}
const btn = document.getElementById('createPayment');
btn.disabled = true;
btn.textContent = '⏳ Creating Payment...';
logMessage(`Creating payment session for ${selectedAmount} AED...`, 'info');
fetch('/wallet/demo/test-payment/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ amount: selectedAmount })
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('sessionId').textContent = data.session_id;
document.getElementById('paymentLink').href = data.payment_url;
document.getElementById('paymentInfo').style.display = 'block';
logMessage(`Payment session created: ${data.session_id}`, 'success');
logMessage(`User ID in session: ${data.user_id}`, 'info');
logMessage('Complete payment to test webhook...', 'info');
updateStatus('payment', true);
} else {
logMessage(`Payment creation failed: ${data.error}`, 'error');
}
})
.catch(error => {
logMessage(`Error: ${error.message}`, 'error');
})
.finally(() => {
btn.disabled = false;
btn.textContent = '💳 Create Test Payment';
});
}
function refreshBalance() {
logMessage('Refreshing balance...', 'info');
fetch('/wallet/demo/check-balance/')
.then(response => response.json())
.then(data => {
document.getElementById('currentBalance').textContent = `${data.balance.toFixed(2)} AED`;
updateTransactionLog(data.recent_transactions);
logMessage(`Balance updated: ${data.balance.toFixed(2)} AED`, 'success');
})
.catch(error => {
logMessage(`Error refreshing balance: ${error.message}`, 'error');
});
}
function updateTransactionLog(transactions) {
const log = document.getElementById('transactionLog');
log.innerHTML = '';
if (transactions.length === 0) {
log.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">No transactions yet.</div>';
return;
}
transactions.forEach(transaction => {
const item = document.createElement('div');
item.className = 'transaction-item';
const date = new Date(transaction.created_at).toLocaleString();
const amountClass = transaction.amount >= 0 ? 'positive' : 'negative';
const amountPrefix = transaction.amount >= 0 ? '+' : '';
item.innerHTML = `
<div>
<div>${transaction.description || 'Wallet Transaction'}</div>
<div style="font-size: 12px; color: #666;">${date}</div>
</div>
<div class="transaction-amount ${amountClass}">
${amountPrefix}${transaction.amount.toFixed(2)} AED
</div>
`;
log.appendChild(item);
});
}
function startPolling() {
// Poll for balance changes every 5 seconds
pollingInterval = setInterval(refreshBalance, 5000);
}
function updateStatus(type, connected) {
const indicator = document.getElementById(type + 'Status');
if (connected) {
indicator.classList.add('connected');
} else {
indicator.classList.remove('connected');
}
}
function logMessage(message, type = 'info') {
const logs = document.getElementById('webhookLogs');
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = `log-entry log-${type}`;
entry.innerHTML = `<span class="log-timestamp">[${timestamp}]</span> ${message}`;
logs.appendChild(entry);
logs.scrollTop = logs.scrollHeight;
// Keep only last 50 log entries
while (logs.children.length > 50) {
logs.removeChild(logs.firstChild);
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (pollingInterval) {
clearInterval(pollingInterval);
}
});
</script>
{% endblock %}