Spaces:
Sleeping
Sleeping
| // require('dotenv').config(); | |
| const express = require('express'); | |
| const axios = require('axios'); | |
| const bodyParser = require('body-parser'); | |
| const app = express(); | |
| const PORT = 7860; | |
| // ========================================== | |
| // CONFIGURATION | |
| // ========================================== | |
| // REPLACE THIS WITH YOUR PAYSTACK SECRET KEY | |
| const PAYSTACK_SECRET_KEY = 'sk_test_4aa42b4105d33d191eda94d543a2a82ad3c420c7'; | |
| // const PAYSTACK_SECRET_KEY = 'sk_live_31c44ef2b81a591d771202b7ffcbcaab765e977e'; | |
| // MOCK DATABASE | |
| // In a real app, you would save these details to MongoDB, Postgres, etc. | |
| let mockUserDB = { | |
| email: null, | |
| customer_code: null, | |
| authorization_code: null, // This is the "Token" to charge the card later | |
| last_log: "Server started. Waiting for user input..." | |
| }; | |
| // Middleware | |
| app.use(bodyParser.json()); | |
| app.use(bodyParser.urlencoded({ extended: true })); | |
| // Helper to log to console and "DB" | |
| const log = (msg) => { | |
| const timestamp = new Date().toLocaleTimeString(); | |
| console.log(`[${timestamp}] ${msg}`); | |
| mockUserDB.last_log = `[${timestamp}] ${msg}`; | |
| }; | |
| // ========================================== | |
| // ROUTES | |
| // ========================================== | |
| // 1. Render the Embedded HTML Interface | |
| app.get('/', (req, res) => { | |
| // We check if we have a saved token to adjust the UI state | |
| const isAuthorized = !!mockUserDB.authorization_code; | |
| const html = ` | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Paystack Card Tokenization Demo</title> | |
| <style> | |
| body { font-family: sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #f4f4f9; } | |
| .card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } | |
| h1 { color: #333; } | |
| input { padding: 10px; width: 100%; box-sizing: border-box; margin-bottom: 10px; } | |
| button { padding: 10px 20px; cursor: pointer; border: none; border-radius: 4px; font-weight: bold; } | |
| .btn-auth { background-color: #09a5db; color: white; } | |
| .btn-charge { background-color: #27ae60; color: white; } | |
| .btn-charge:disabled { background-color: #ccc; cursor: not-allowed; } | |
| .logs { background: #333; color: #0f0; padding: 15px; border-radius: 4px; font-family: monospace; height: 150px; overflow-y: scroll; margin-top: 20px; white-space: pre-wrap; } | |
| .status-box { margin-bottom: 20px; padding: 10px; background: #eef; border-left: 5px solid #09a5db; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <h1>💳 Paystack Recurring Charge Demo</h1> | |
| <div class="status-box"> | |
| <strong>Status:</strong> <span id="status-text">${isAuthorized ? '✅ Card Authorized' : '❌ No Card stored'}</span><br> | |
| <strong>Customer Email:</strong> ${mockUserDB.email || 'N/A'}<br> | |
| <strong>Auth Code:</strong> ${mockUserDB.authorization_code || 'N/A'} | |
| </div> | |
| ${!isAuthorized ? ` | |
| <h3>Step 1: Authorize Card</h3> | |
| <p>We will charge a tiny amount (e.g., NGN 50) to authorize the card.</p> | |
| <form action="/initialize" method="POST"> | |
| <input type="email" name="email" placeholder="Enter email address" required value="test@user.com"> | |
| <button type="submit" class="btn-auth">Authorize Card Now</button> | |
| </form> | |
| ` : ` | |
| <h3>Step 2: Charge Saved Card</h3> | |
| <p>The card is tokenized. You can now charge it anytime without user interaction.</p> | |
| <button id="chargeBtn" class="btn-charge" onclick="chargeCard()">Charge NGN 100.00 Now</button> | |
| <br><br> | |
| <small><a href="/reset">Reset Demo</a></small> | |
| `} | |
| <h3>Server Logs</h3> | |
| <div class="logs" id="log-window">${mockUserDB.last_log}</div> | |
| </div> | |
| <script> | |
| // Simple polling to update logs | |
| setInterval(() => { | |
| fetch('/logs').then(res => res.json()).then(data => { | |
| document.getElementById('log-window').innerText = data.log; | |
| }); | |
| }, 2000); | |
| async function chargeCard() { | |
| const btn = document.getElementById('chargeBtn'); | |
| btn.disabled = true; | |
| btn.innerText = "Charging..."; | |
| try { | |
| const res = await fetch('/charge-token', { method: 'POST' }); | |
| const data = await res.json(); | |
| alert(data.message); | |
| } catch (e) { | |
| alert("Error charging card"); | |
| } | |
| btn.disabled = false; | |
| btn.innerText = "Charge NGN 100.00 Now"; | |
| window.location.reload(); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| `; | |
| res.send(html); | |
| }); | |
| // 2. Initialize Transaction (Get Redirect URL) | |
| app.post('/initialize', async (req, res) => { | |
| const { email } = req.body; | |
| // Amount is in Kobo (or lowest currency unit). 5000 kobo = 50 Naira. | |
| // Paystack requires a small amount to verify the card (which can be refunded later). | |
| const amount = 5000; | |
| log(`Initializing transaction for ${email}...`); | |
| try { | |
| const response = await axios.post('https://api.paystack.co/transaction/initialize', { | |
| email: email, | |
| amount: amount, | |
| // Important: This is where Paystack returns the user after payment | |
| callback_url: `https://pepguy-authorize-card.hf.space/callback`, | |
| channels: ['card'] // Force card payment to ensure we get an auth token | |
| }, { | |
| headers: { Authorization: `Bearer ${PAYSTACK_SECRET_KEY}` } | |
| }); | |
| const authUrl = response.data.data.authorization_url; | |
| log(`Redirecting user to Paystack: ${authUrl}`); | |
| // Save email temporarily | |
| mockUserDB.email = email; | |
| // Redirect user to Paystack secure page | |
| res.redirect(authUrl); | |
| } catch (error) { | |
| log(`Error initializing: ${error.response ? error.response.data.message : error.message}`); | |
| res.send("Error initializing transaction. Check console."); | |
| } | |
| }); | |
| // 3. Callback (Handle Return from Paystack) | |
| app.get('/callback', async (req, res) => { | |
| const reference = req.query.reference; // Paystack sends this in the URL | |
| log(`Callback received. Verifying reference: ${reference}...`); | |
| try { | |
| // Verify the transaction to get the Authorization Code | |
| const response = await axios.get(`https://api.paystack.co/transaction/verify/${reference}`, { | |
| headers: { Authorization: `Bearer ${PAYSTACK_SECRET_KEY}` } | |
| }); | |
| const data = response.data.data; | |
| if (data.status === 'success') { | |
| // SUCCESS! We now have the Authorization Code (The Token) | |
| const authCode = data.authorization.authorization_code; | |
| const customerCode = data.customer.customer_code; | |
| const cardType = data.authorization.card_type; | |
| const last4 = data.authorization.last4; | |
| // Save to "Database" | |
| mockUserDB.authorization_code = authCode; | |
| mockUserDB.customer_code = customerCode; | |
| mockUserDB.email = data.customer.email; | |
| log(`SUCCESS! Card authorized. Type: ${cardType} *${last4}. Auth Code: ${authCode}`); | |
| } else { | |
| log(`Transaction failed or incomplete.`); | |
| } | |
| } catch (error) { | |
| log(`Verification failed: ${error.message}`); | |
| } | |
| // Redirect back to our main page | |
| res.redirect('/'); | |
| }); | |
| // 4. Charge the Saved Card (Recurring Charge) | |
| app.post('/charge-token', async (req, res) => { | |
| if (!mockUserDB.authorization_code) { | |
| return res.status(400).json({ message: "No card authorized yet." }); | |
| } | |
| // Amount to charge (e.g., 100 Naira = 10000 kobo) | |
| const chargeAmount = 10000; | |
| log(`Attempting to charge saved card (Auth: ${mockUserDB.authorization_code}) amount: ${chargeAmount}...`); | |
| try { | |
| const response = await axios.post('https://api.paystack.co/transaction/charge_authorization', { | |
| email: mockUserDB.email, | |
| amount: chargeAmount, | |
| authorization_code: mockUserDB.authorization_code | |
| }, { | |
| headers: { Authorization: `Bearer ${PAYSTACK_SECRET_KEY}` } | |
| }); | |
| const data = response.data.data; | |
| if (response.data.status) { | |
| log(`CHARGE SUCCESSFUL! Ref: ${data.reference} - Status: ${data.status}`); | |
| res.json({ message: "Charge Successful! check logs." }); | |
| } else { | |
| log(`Charge Failed: ${response.data.message}`); | |
| res.json({ message: "Charge Failed." }); | |
| } | |
| } catch (error) { | |
| const errMsg = error.response ? error.response.data.message : error.message; | |
| log(`API Error during charge: ${errMsg}`); | |
| res.status(500).json({ message: `Error: ${errMsg}` }); | |
| } | |
| }); | |
| // Utility: Get Logs for Frontend | |
| app.get('/logs', (req, res) => { | |
| res.json({ log: mockUserDB.last_log }); | |
| }); | |
| // Utility: Reset Demo | |
| app.get('/reset', (req, res) => { | |
| mockUserDB = { email: null, customer_code: null, authorization_code: null, last_log: "System Reset." }; | |
| res.redirect('/'); | |
| }); | |
| // Start Server | |
| app.listen(PORT, () => { | |
| console.log(`Server running at http://localhost:${PORT}`); | |
| console.log(`Make sure to set your PAYSTACK_SECRET_KEY in code or .env`); | |
| }); |