From 05547f650a0e3dc998cbb7947c3e176f8a856577 Mon Sep 17 00:00:00 2001 From: Teppy Date: Mon, 10 Feb 2025 20:53:52 -0500 Subject: [PATCH] First version with credit card integration and UI --- foftickets.js | 132 ++++++++++++++++++++++++++---------------- views/camps.ejs | 18 +++--- views/checkout2.ejs | 4 +- views/editcamp.ejs | 4 +- views/error.ejs | 8 ++- views/manytickets.ejs | 4 +- views/oneticket.ejs | 17 +++++- views/pay.ejs | 103 ++++++++++++++++++++++++++++++++ 8 files changed, 225 insertions(+), 65 deletions(-) create mode 100644 views/pay.ejs diff --git a/foftickets.js b/foftickets.js index 01673ae..71ad399 100644 --- a/foftickets.js +++ b/foftickets.js @@ -216,7 +216,7 @@ app.get('/emaillink',(req,res) => { app.get('/camps',requireSuperUser, (req,res) => { const camplist={}; for (const c in camps) { - camplist[c]={ leader:camps[c].leader, issued:0, claimed:0, used:0 }; + camplist[c]={ leader:camps[c].leader, issued:0, claimed:0, used:0, npaid:0, paid:0 }; } for (const t in tickets) { const parts=t.split("-"); @@ -226,6 +226,8 @@ app.get('/camps',requireSuperUser, (req,res) => { camplist[campname].issued+=1; if (tickets[t].owner!="") camplist[campname].claimed+=1; if (tickets[t].status=="u") camplist[campname].used+=1; + if (tickets[t].paid>0) camplist[campname].npaid+=1; + camplist[campname].paid+=tickets[t].paid; } } return res.render("camps",{ username:req.session.username, superuser:req.session.superuser, camps:camplist }); @@ -238,7 +240,7 @@ app.post("/camps",requireSuperUser,(req,res) => { camps[campname]??={ leader:leader, lastid:0 }; for (let i=0; i { edit.tickets[t].owner=tickets[t].owner; edit.tickets[t].offered=tickets[t].offered; edit.tickets[t].status=tickets[t].status; + edit.tickets[t].paid=tickets[t].paid; } } return res.render("editcamp",edit); @@ -282,6 +285,7 @@ app.get('/manytickets', requireLogin, (req,res) => { for (const t in tickets) if (tickets[t].owner==username && tickets[t].status=="i") { edit.tickets[t]={}; edit.tickets[t].offered=tickets[t].offered; + edit.tickets[t].paid=tickets[t].paid; } return res.render("manytickets",edit); }) @@ -301,6 +305,7 @@ app.get('/mytickets',requireLogin, async (req,res)=> { edit.tickets[t]={}; edit.tickets[t].owner=tickets[t].owner; edit.tickets[t].offered=tickets[t].offered; + edit.tickets[t].paid=tickets[t].paid; } } let message=""; @@ -313,25 +318,47 @@ app.get('/mytickets',requireLogin, async (req,res)=> { const hash=hash1.digest("base64").slice(0,6); const useurl=MainURL+'/useticket?t='+theticket+'&h='+hashQR(theticket,username); const dataURL=await QRCode.toDataURL(useurl); - return res.render("oneticket",{ username:username, superuser:req.session.superuser, message:message, ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL, useurl:useurl }); + return res.render("oneticket",{ username:username, superuser:req.session.superuser, message:message, magiclink:GetMagicLink(username), + ticket:theticket, offered:tickets[theticket].offered, paid:tickets[theticket].paid, qrcode:dataURL, useurl:useurl }); } else return res.render("manytickets",edit); }); -app.post('/oneticket',requireLogin, async (req,res)=> { +app.get("/oneticket",requireLogin, async (req,res) => { let username=req.session.username; - let theticket=req.body.ticket; + let ticket=req.query.t; + console.log("Ticket ",tickets); + if (tickets[ticket]) console.log("Onwer ",tickets[ticket].owner); + if (!tickets[ticket] || tickets[ticket].owner!=username) return res.render("error", { username:username, superuser:req.session.superuser, message: "You are not the owner of ticket "+ticket }); + let offered=tickets[ticket].offered; + let message=""; + const hash0=crypto.createHash('sha256'); + const hash1=hash0.update(ticket+QRSalt); + const hash=hash1.digest("base64").slice(0,6); + const useurl=MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username); + const dataURL=await QRCode.toDataURL(MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username)); + return res.render("oneticket", { username:username, superuser:req.session.superuser, message:message, magiclink:GetMagicLink(username), + ticket:ticket, offered:tickets[ticket].offered, paid:tickets[ticket].paid, qrcode:dataURL, useurl:useurl }); +}); + + +app.post('/oneticket',requireLogin, async (req,res) => { // Make sure the ticket is owned by trhe logged in user! + let username=req.session.username; + let ticket=req.body.ticket; + if (!tickets[ticket] || tickets[ticket].owner!=username) return res.render("error", { username:username, superuser:req.session.superuser, message: "You are not the owner of ticket "+ticket }); let offered=req.body.offered; let message=""; - if (tickets[theticket].owner==username && tickets[theticket].status=="i") { - tickets[theticket].offered=offered; // LOG + if (tickets[ticket].owner==username && tickets[ticket].status=="i") { + tickets[ticket].offered=offered; // LOG EmailTickets(offered); } const hash0=crypto.createHash('sha256'); - const hash1=hash0.update(theticket+QRSalt); + const hash1=hash0.update(ticket+QRSalt); const hash=hash1.digest("base64").slice(0,6); - const dataURL=await QRCode.toDataURL(MainURL+'/useticket?t='+theticket+'&h='+hashQR(theticket,username)); - return res.render("oneticket", { username:username, superuser:req.session.superuser, message:message, magiclink:GetMagicLink(username), ticket:theticket, offered:tickets[theticket].offered, qrcode:dataURL }); + const dataURL=await QRCode.toDataURL(MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username)); + const useurl=MainURL+'/useticket?t='+ticket+'&h='+hashQR(ticket,username); + return res.render("oneticket", { username:username, superuser:req.session.superuser, message:message, magiclink:GetMagicLink(username), + ticket:ticket, offered:tickets[ticket].offered, paid:tickets[ticket].paid, qrcode:dataURL, useurl:useurl }); }); @@ -407,10 +434,12 @@ app.get("/useticket",(req,res) => { if (!tickets[ticket]) return res.status(500).send("Ticket "+ticket+" not found."); if (hashQR(ticket,tickets[ticket].owner)!=hash) return res.status(500).send("Ticket "+ticket+" was transferred to "+tickets[ticket].owner); if (tickets[ticket].status!="i") return res.status(500).send("Ticket "+ticket+" has already been used."); - if (settings['enable-email']) { + let paid_message=tickets[ticket].paid; + if (tickets[ticket].paid==0) paid_message="This ticket has not been paid for. Encourage a donation at the gate."; + if (settings['enable-ticket-use']) { tickets[ticket].status="u"; // LOG - return res.send("

Welcome, "+tickets[ticket].owner+" to Falls on Fire! Ticket "+ticket+" has now been used.

"); - } else return res.send("

Your ticket is good, "+tickets[ticket].owner+", but the server is not in Event Mode, so Ticket "+ticket+" is still valid."); + return res.send("

Welcome, "+tickets[ticket].owner+" to Falls on Fire!
Ticket "+ticket+" has now been used.


"+paid_message); + } else return res.send("

Your ticket is good, "+tickets[ticket].owner+"
The server is not in Event Mode, so Ticket "+ticket+" is still valid.
"+paid_message); }) @@ -592,46 +621,51 @@ app.post('/update-setting', requireSuperUser, (req, res) => { res.json({ success: true, message: 'Checkbox state updated successfully' }); }); - -app.get('/checkout', (req, res) => { - // We’ll render a payment form here - res.render('checkout2'); +app.post('/pay0',requireLogin,(req,res) => { + return res.render("pay",{ username:req.session.username, superuser:req.session.superuser, ticket:req.body.ticket, amount:req.body.amount }); }); -app.post('/charge', async (req, res) => { - try { - // Token or Payment Method ID from the client - const paymentMethodId = req.body.paymentMethodId; - // Create a PaymentIntent on the server - const return_url=base_url+'/mytickets'; - const paymentIntent = await stripe.paymentIntents.create({ - amount: 1999, // Amount in cents (e.g., $19.99) - currency: 'usd', - payment_method: paymentMethodId, - confirmation_method: 'automatic', - confirm: true, // Attempt to confirm the payment immediately - return_url: return_url, - }); +app.post('/charge', requireLogin, async (req, res) => { + const ticket=req.body.ticket; + if (tickets[ticket].status=='r' || tickets[ticket].owner!=req.session.username) { + res.json({ error: "Sanity check in /charge" }); + } else try { + // Token or Payment Method ID from the client + const paymentMethodId = req.body.paymentMethodId; - // Check status of payment intent - if (paymentIntent.status === 'requires_action') { - // Additional action is required (e.g. 3D Secure) - res.json({ - requiresAction: true, - paymentIntentClientSecret: paymentIntent.client_secret, - }); - } else if (paymentIntent.status === 'succeeded') { - // Payment is complete - console.log("Returning json success: true"); - res.json({ success: true, redirect_url: return_url }); - } else { - res.json({ error: 'Invalid PaymentIntent status' }); - } - } catch (error) { - console.error('Payment error:', error); - res.json({ error: error.message }); - } + // Create a PaymentIntent on the server + const pennies=Math.round(parseFloat(req.body.amount) * 100); + console.log("Pennies=",pennies); + const return_url=base_url+'/mytickets'; + const paymentIntent = await stripe.paymentIntents.create({ + amount: pennies, // Amount in cents + currency: 'usd', + payment_method: paymentMethodId, + confirmation_method: 'automatic', + confirm: true, // Attempt to confirm the payment immediately + return_url: return_url, + }); + + // Check status of payment intent + if (paymentIntent.status === 'requires_action') { + // Additional action is required (e.g. 3D Secure) + res.json({ + requiresAction: true, + paymentIntentClientSecret: paymentIntent.client_secret, + }); + } else if (paymentIntent.status === 'succeeded') { + // Payment is complete + tickets[ticket].paid=pennies; + console.log("Paid ",pennies," for ticket ",ticket); + res.json({ success: true, redirect_url: return_url }); + } else { + res.json({ error: 'Invalid PaymentIntent status' }); + } + } catch (error) { + console.error('Payment error:', error); + res.json({ error: error.message }); + } }); diff --git a/views/camps.ejs b/views/camps.ejs index 97ad5af..d1638f5 100644 --- a/views/camps.ejs +++ b/views/camps.ejs @@ -10,19 +10,23 @@
- - - + + + - + + + <% for (const c in camps) { %> - - + + - + + + <% } %> diff --git a/views/checkout2.ejs b/views/checkout2.ejs index 1e89069..6f119c4 100644 --- a/views/checkout2.ejs +++ b/views/checkout2.ejs @@ -23,7 +23,7 @@
- + @@ -51,7 +51,7 @@ // 1. Create a PaymentMethod const { paymentMethod, error } = await stripe.createPaymentMethod({ type: "card", - card: cardElement, + card: cardNumberElement, }); if (error) { diff --git a/views/editcamp.ejs b/views/editcamp.ejs index ff248f3..85f47ba 100644 --- a/views/editcamp.ejs +++ b/views/editcamp.ejs @@ -22,14 +22,16 @@ + <% for (const t in tickets) { %> - + + + <% for (const t in tickets) { %> - + + <% } %> diff --git a/views/oneticket.ejs b/views/oneticket.ejs index 7955f5e..460ad23 100644 --- a/views/oneticket.ejs +++ b/views/oneticket.ejs @@ -7,11 +7,22 @@ <%- include('partials/nav') %>
- To use <%=ticket%>, scan this QR Code:
+ To use <%=ticket%>, visit this URL: <%=useurl%>
+ Or scan this QR Code:
QR Code
- Or visit this URL: "<%=useurl%>" + <% if (paid==0) { %> + Tickets are pay-what-you-can, minimum $1, suggested $50.
+ To pay for this ticket by credit card, enter an amount: +
+ + + + + <% } else { %> + This ticket has already been paid for. ($<%=(paid/100).toFixed(2)%>) + <% } %>
- Or transfer <%=ticket%> to:
+ To transfer <%=ticket%>, enter the recipient's email address:
diff --git a/views/pay.ejs b/views/pay.ejs new file mode 100644 index 0000000..bb97387 --- /dev/null +++ b/views/pay.ejs @@ -0,0 +1,103 @@ + + + + Your Ticket + + + + <%- include('partials/nav') %> +
+ +Payment Here!!!<%=amount%> + + + +
+ + +
+ + +
+ + +
+ + + +
+ + + + + + +
CampLeader#IssuedCamp Leader #Issued #Claimed#Used#Used #Paid $Paid
<%=c%><%=camps[c].leader%> <%=camps[c].issued%> <%=camps[c].leader%> <%=camps[c].issued%> <%=camps[c].claimed%> <%=camps[c].used%> <%=camps[c].used%> <%=camps[c].npaid%> <%=(camps[c].paid/100).toFixed(2)%>
Ticket# Owner Offered ToPaid? Status Action
<%=t%><%=t%> <%=tickets[t].paid>0 ? (tickets[t].paid/100).toFixed(2) : "No"%>
Ticket# Offered ToPaid? Action
<%=t%><%=t%> <%=tickets[t].paid>0 ? (tickets[t].paid/100).toFixed(2) : "No"%>