quantum-ai-v3/templates/core/wallet.html
Claude e7d5ccf017 Implement webhook-free Stripe payment verification system
 SOLUTION: Bypass webhook delivery issues entirely

Changes:
- Enhanced checkout session creation with session_id in success URL
- Updated success page to automatically verify payments via Stripe API
- Added manual payment verification modal on wallet page
- Users can verify payments manually if automatic verification fails
- Real-time balance updates without depending on webhooks

Benefits:
- Instant payment confirmation upon return from Stripe
- No webhook delivery dependency
- Manual fallback for edge cases
- Better user experience with immediate feedback

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 16:11:35 +05:30

555 lines
16 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html' %}
{% load static %}
{% block title %}Wallet - NetCop Hub{% endblock %}
{% block extra_css %}
<style>
/* Wallet page styles */
.wallet-page {
background: var(--gradient-hero);
min-height: calc(100vh - 80px);
padding: clamp(20px, 5vw, 40px) 0;
}
/* Hero section */
.wallet-hero {
background: var(--gradient-success);
color: white;
padding: clamp(20px, 4vw, 24px) 0;
text-align: center;
margin-bottom: clamp(16px, 4vw, 20px);
border-radius: clamp(12px, 3vw, 16px);
position: relative;
overflow: hidden;
}
.wallet-hero h1 {
font-size: clamp(20px, 5vw, 24px);
margin: 0 0 8px 0;
font-weight: 700;
position: relative;
z-index: 1;
}
.wallet-hero .current-balance {
font-size: clamp(28px, 7vw, 36px);
font-weight: 800;
margin: 8px 0 16px 0;
position: relative;
z-index: 1;
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.wallet-hero .balance-label {
font-size: clamp(14px, 3.5vw, 16px);
opacity: 0.9;
margin-bottom: 12px;
position: relative;
z-index: 1;
}
/* Top up button */
.topup-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
padding: clamp(12px, 3vw, 16px) clamp(24px, 6vw, 32px);
border-radius: clamp(8px, 2vw, 12px);
font-size: clamp(16px, 4vw, 18px);
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
position: relative;
z-index: 1;
backdrop-filter: blur(10px);
}
.topup-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.2);
color: white;
}
/* Stats grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr));
gap: clamp(16px, 4vw, 24px);
margin-bottom: clamp(24px, 6vw, 32px);
}
.stat-card {
background: rgba(255, 255, 255, 0.95);
border-radius: clamp(12px, 3vw, 16px);
padding: clamp(20px, 5vw, 24px);
text-align: center;
border: 1px solid rgba(16, 185, 129, 0.1);
backdrop-filter: blur(20px);
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.1);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--gradient-success);
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(16, 185, 129, 0.15);
}
.stat-icon {
font-size: clamp(32px, 8vw, 40px);
margin-bottom: 12px;
display: block;
}
.stat-value {
font-size: clamp(24px, 6vw, 32px);
font-weight: 700;
color: var(--success-green);
margin: 8px 0;
}
.stat-label {
font-size: clamp(14px, 3.5vw, 16px);
color: var(--text-secondary);
font-weight: 500;
}
/* Transactions section */
.transactions-card {
background: rgba(255, 255, 255, 0.95);
border-radius: clamp(12px, 3vw, 16px);
padding: clamp(20px, 5vw, 24px);
border: 1px solid rgba(16, 185, 129, 0.1);
backdrop-filter: blur(20px);
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.1);
}
.transactions-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: clamp(20px, 5vw, 24px);
padding-bottom: 16px;
border-bottom: 2px solid var(--border-light);
}
.transactions-title {
font-size: clamp(20px, 5vw, 24px);
font-weight: 700;
color: var(--text-primary);
margin: 0;
flex: 1;
}
.transaction-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: clamp(16px, 4vw, 20px) 0;
border-bottom: 1px solid var(--border-light);
transition: all 0.2s ease;
}
.transaction-item:last-child {
border-bottom: none;
}
.transaction-item:hover {
background: rgba(16, 185, 129, 0.02);
border-radius: 8px;
margin: 0 -12px;
padding-left: 12px;
padding-right: 12px;
}
.transaction-info {
flex: 1;
}
.transaction-description {
font-weight: 600;
color: var(--text-primary);
font-size: clamp(14px, 3.5vw, 16px);
margin-bottom: 4px;
}
.transaction-date {
font-size: clamp(12px, 3vw, 14px);
color: var(--text-secondary);
}
.transaction-amount {
font-weight: 700;
font-size: clamp(16px, 4vw, 18px);
padding: 4px 12px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 4px;
}
.transaction-amount.positive {
color: var(--success-green);
background: rgba(5, 150, 105, 0.1);
}
.transaction-amount.negative {
color: var(--error-red);
background: rgba(220, 38, 38, 0.1);
}
.no-transactions {
text-align: center;
padding: clamp(32px, 8vw, 48px);
color: var(--text-secondary);
}
.no-transactions-icon {
font-size: clamp(48px, 12vw, 64px);
margin-bottom: 16px;
opacity: 0.5;
}
.no-transactions-text {
font-size: clamp(16px, 4vw, 18px);
margin: 0;
}
/* Verify payment button */
.verify-payment-btn {
background: rgba(255, 255, 255, 0.15);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.verify-payment-btn:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-1px);
}
/* Verify modal */
.verify-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.verify-modal-content {
background: white;
border-radius: 16px;
padding: 24px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.verify-modal-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 16px;
color: var(--text-primary);
}
.verify-input {
width: 100%;
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
margin: 8px 0;
font-family: 'Courier New', monospace;
}
.verify-buttons {
display: flex;
gap: 12px;
margin-top: 20px;
}
.verify-btn {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.verify-btn.primary {
background: var(--primary-blue);
color: white;
}
.verify-btn.primary:hover {
background: var(--primary-dark);
}
.verify-btn.secondary {
background: #f1f5f9;
color: var(--text-primary);
}
.verify-btn.secondary:hover {
background: #e2e8f0;
}
.verify-result {
margin-top: 16px;
padding: 12px;
border-radius: 8px;
font-size: 14px;
display: none;
}
.verify-result.success {
background: #dcfce7;
color: #16a34a;
border: 1px solid #bbf7d0;
}
.verify-result.error {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: 1fr;
}
.transaction-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.transaction-amount {
align-self: flex-end;
}
}
</style>
{% endblock %}
{% block content %}
<div class="wallet-page">
<!-- Hero Section -->
<div class="wallet-hero">
<h1>💰 Your Wallet</h1>
<div class="balance-label">Current Balance</div>
<div class="current-balance">{{ current_balance|floatformat:2 }} AED</div>
<a href="{% url 'core:wallet_topup' %}" class="topup-btn">
💳 Top Up Wallet
</a>
<div style="margin-top: 16px;">
<button class="verify-payment-btn" onclick="showVerifyModal()">
🔍 Verify Payment
</button>
</div>
</div>
<!-- Statistics -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">💸</div>
<div class="stat-value">{{ total_spent|floatformat:2 }} AED</div>
<div class="stat-label">Total Spent</div>
</div>
<div class="stat-card">
<div class="stat-icon">💵</div>
<div class="stat-value">{{ total_topped_up|floatformat:2 }} AED</div>
<div class="stat-label">Total Topped Up</div>
</div>
<div class="stat-card">
<div class="stat-icon">📊</div>
<div class="stat-value">{{ transactions|length }}</div>
<div class="stat-label">Total Transactions</div>
</div>
</div>
<!-- Recent Transactions -->
<div class="transactions-card">
<div class="transactions-header">
<h2 class="transactions-title">📋 Recent Transactions</h2>
</div>
{% if transactions %}
{% for transaction in transactions %}
<div class="transaction-item">
<div class="transaction-info">
<div class="transaction-description">{{ transaction.description }}</div>
<div class="transaction-date">{{ transaction.created_at|date:"M d, Y H:i" }}</div>
</div>
<div class="transaction-amount {% if transaction.type == 'top_up' %}positive{% else %}negative{% endif %}">
{% if transaction.type == 'top_up' %}
<span>↗️</span> +{{ transaction.amount|floatformat:2 }} AED
{% else %}
<span>↙️</span> -{{ transaction.amount|floatformat:2 }} AED
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="no-transactions">
<div class="no-transactions-icon">📭</div>
<p class="no-transactions-text">No transactions yet. Start using AI agents to see your transaction history!</p>
</div>
{% endif %}
</div>
</div>
<!-- Verify Payment Modal -->
<div class="verify-modal" id="verifyModal">
<div class="verify-modal-content">
<h3 class="verify-modal-title">🔍 Verify Payment</h3>
<p style="margin-bottom: 16px; color: #666; font-size: 14px;">
If you completed a payment but your wallet balance wasn't updated, enter the Stripe session ID below to verify it manually.
</p>
<label for="sessionIdInput" style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-primary);">
Stripe Session ID:
</label>
<input type="text" id="sessionIdInput" class="verify-input" placeholder="cs_test_..." />
<p style="margin: 12px 0; font-size: 13px; color: #666;">
💡 You can find the session ID in your browser URL after completing payment, or in your email receipt.
</p>
<div class="verify-buttons">
<button class="verify-btn secondary" onclick="hideVerifyModal()">Cancel</button>
<button class="verify-btn primary" onclick="verifyPayment()">Verify Payment</button>
</div>
<div class="verify-result" id="verifyResult"></div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function showVerifyModal() {
document.getElementById('verifyModal').style.display = 'flex';
document.getElementById('sessionIdInput').focus();
}
function hideVerifyModal() {
document.getElementById('verifyModal').style.display = 'none';
document.getElementById('sessionIdInput').value = '';
document.getElementById('verifyResult').style.display = 'none';
}
function verifyPayment() {
const sessionId = document.getElementById('sessionIdInput').value.trim();
const resultDiv = document.getElementById('verifyResult');
if (!sessionId) {
showResult('Please enter a session ID', 'error');
return;
}
if (!sessionId.startsWith('cs_')) {
showResult('Invalid session ID format. It should start with "cs_"', 'error');
return;
}
// Show loading
showResult('🔍 Verifying payment...', 'success');
// Get CSRF token
const csrfToken = getCsrfToken();
fetch('/wallet/verify-payment/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ session_id: sessionId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (data.processed) {
showResult(`✅ Payment verified! ${data.amount} AED has been added to your wallet.`, 'success');
// Refresh page after 2 seconds
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
showResult(` Payment already processed. ${data.message}`, 'success');
}
} else {
showResult(`❌ Verification failed: ${data.error}`, 'error');
}
})
.catch(error => {
showResult(`❌ Error: ${error.message}`, 'error');
});
}
function showResult(message, type) {
const resultDiv = document.getElementById('verifyResult');
resultDiv.textContent = message;
resultDiv.className = `verify-result ${type}`;
resultDiv.style.display = 'block';
}
function getCsrfToken() {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'csrftoken') {
return value;
}
}
return '';
}
// Close modal when clicking outside
document.getElementById('verifyModal').addEventListener('click', function(e) {
if (e.target === this) {
hideVerifyModal();
}
});
// Handle Enter key in input
document.getElementById('sessionIdInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
verifyPayment();
}
});
</script>
{% endblock %}