From 36b49370674c80e087bc3a7f6a219222271ec44d Mon Sep 17 00:00:00 2001 From: 30178597 Date: Mon, 17 Feb 2025 19:39:59 -0700 Subject: [PATCH 01/10] v1.00 rtbefore brew install stripe/stripe-cli/stripe --- package.json | 1 + src/api/webhook/route.ts | 26 ++++++ yarn.lock | 178 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 src/api/webhook/route.ts diff --git a/package.json b/package.json index 7cd1b20..e20635c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "react-scroll": "^1.9.0", "react-tsparticles": "^2.9.3", "react-visibility-sensor": "^5.1.1", + "stripe": "^17.6.0", "styled-components": "^6.1.13", "tsparticles": "^2.9.3" }, diff --git a/src/api/webhook/route.ts b/src/api/webhook/route.ts new file mode 100644 index 0000000..0af5411 --- /dev/null +++ b/src/api/webhook/route.ts @@ -0,0 +1,26 @@ +import Stripe from "stripe"; +import { NextResponse } from "next/server"; + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); +export async function POST(req: Request, res: Response) { + const payload = await req.text(); + const response = await JSON.parse(payload); + + const sig = req.headers.get("stripe-signature"); + + const dateTime = new Date(response?.created * 1000).toLocaleDateString(); + const timeString = new Date(response?.created * 1000).toLocaleDateString(); + + try { + let event = stripe.webhooks.constructEvent( + payload, + sig!, + process.env.STRIPE_WEBHOOK_SECRET!, + ); + console.log("event", event.type); + } catch (error) { + console.log(error); + } + + return NextResponse.json({ success: true }); +} diff --git a/yarn.lock b/yarn.lock index fe18c5e..84db458 100644 --- a/yarn.lock +++ b/yarn.lock @@ -832,6 +832,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>=8.1.0": + version: 22.13.4 + resolution: "@types/node@npm:22.13.4" + dependencies: + undici-types: "npm:~6.20.0" + checksum: 10c0/3a234fa7766a3efc382cf81f66f474c26cdab2f54f43f757634c81c0444eb2160c2dabbde9741e4983078a318a88515b65416b5f1ab5478548579d7b3ead1d95 + languageName: node + linkType: hard + "@types/node@npm:^20": version: 20.16.11 resolution: "@types/node@npm:20.16.11" @@ -1589,6 +1598,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938 + languageName: node + linkType: hard + "call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" @@ -1602,6 +1621,16 @@ __metadata: languageName: node linkType: hard +"call-bound@npm:^1.0.2": + version: 1.0.3 + resolution: "call-bound@npm:1.0.3" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + get-intrinsic: "npm:^1.2.6" + checksum: 10c0/45257b8e7621067304b30dbd638e856cac913d31e8e00a80d6cf172911acd057846572d0b256b45e652d515db6601e2974a1b1a040e91b4fc36fb3dd86fa69cf + languageName: node + linkType: hard + "callsites@npm:^3.0.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -2060,6 +2089,17 @@ __metadata: languageName: node linkType: hard +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -2207,6 +2247,13 @@ __metadata: languageName: node linkType: hard +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c + languageName: node + linkType: hard + "es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" @@ -2971,6 +3018,34 @@ __metadata: languageName: node linkType: hard +"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6": + version: 1.2.7 + resolution: "get-intrinsic@npm:1.2.7" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + function-bind: "npm:^1.1.2" + get-proto: "npm:^1.0.0" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.1.0" + checksum: 10c0/b475dec9f8bff6f7422f51ff4b7b8d0b68e6776ee83a753c1d627e3008c3442090992788038b37eff72e93e43dceed8c1acbdf2d6751672687ec22127933080d + languageName: node + linkType: hard + +"get-proto@npm:^1.0.0": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: "npm:^1.0.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c + languageName: node + linkType: hard + "get-stream@npm:^8.0.1": version: 8.0.1 resolution: "get-stream@npm:8.0.1" @@ -3110,6 +3185,13 @@ __metadata: languageName: node linkType: hard +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead + languageName: node + linkType: hard + "graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -3168,6 +3250,13 @@ __metadata: languageName: node linkType: hard +"has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e + languageName: node + linkType: hard + "has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.2": version: 1.0.2 resolution: "has-tostringtag@npm:1.0.2" @@ -3954,6 +4043,13 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -4314,6 +4410,13 @@ __metadata: languageName: node linkType: hard +"object-inspect@npm:^1.13.3": + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692 + languageName: node + linkType: hard + "object-is@npm:^1.1.5": version: 1.1.6 resolution: "object-is@npm:1.1.6" @@ -4808,6 +4911,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.11.0": + version: 6.14.0 + resolution: "qs@npm:6.14.0" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10c0/8ea5d91bf34f440598ee389d4a7d95820e3b837d3fd9f433871f7924801becaa0cd3b3b4628d49a7784d06a8aea9bc4554d2b6d8d584e2d221dc06238a42909c + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -5155,6 +5267,7 @@ __metadata: react-scroll: "npm:^1.9.0" react-tsparticles: "npm:^2.9.3" react-visibility-sensor: "npm:^5.1.1" + stripe: "npm:^17.6.0" styled-components: "npm:^6.1.13" tailwindcss: "npm:^3.4.4" tsparticles: "npm:^2.9.3" @@ -5277,6 +5390,41 @@ __metadata: languageName: node linkType: hard +"side-channel-list@npm:^1.0.0": + version: 1.0.0 + resolution: "side-channel-list@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + checksum: 10c0/644f4ac893456c9490ff388bf78aea9d333d5e5bfc64cfb84be8f04bf31ddc111a8d4b83b85d7e7e8a7b845bc185a9ad02c052d20e086983cf59f0be517d9b3d + languageName: node + linkType: hard + +"side-channel-map@npm:^1.0.1": + version: 1.0.1 + resolution: "side-channel-map@npm:1.0.1" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672 + languageName: node + linkType: hard + +"side-channel-weakmap@npm:^1.0.2": + version: 1.0.2 + resolution: "side-channel-weakmap@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + side-channel-map: "npm:^1.0.1" + checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185 + languageName: node + linkType: hard + "side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": version: 1.0.6 resolution: "side-channel@npm:1.0.6" @@ -5289,6 +5437,19 @@ __metadata: languageName: node linkType: hard +"side-channel@npm:^1.1.0": + version: 1.1.0 + resolution: "side-channel@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + side-channel-list: "npm:^1.0.0" + side-channel-map: "npm:^1.0.1" + side-channel-weakmap: "npm:^1.0.2" + checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -5550,6 +5711,16 @@ __metadata: languageName: node linkType: hard +"stripe@npm:^17.6.0": + version: 17.6.0 + resolution: "stripe@npm:17.6.0" + dependencies: + "@types/node": "npm:>=8.1.0" + qs: "npm:^6.11.0" + checksum: 10c0/78b5570fffeae5e197392d5ab506a4713f1ee7316df4359d5cc7628a31d86e6b29c464684b5de46420ef52d3a39c6a27e3e715c88d0f2968a48104a68285e8a7 + languageName: node + linkType: hard + "styled-components@npm:^6.1.13": version: 6.1.13 resolution: "styled-components@npm:6.1.13" @@ -6329,6 +6500,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.20.0": + version: 6.20.0 + resolution: "undici-types@npm:6.20.0" + checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" From 45603d7de1de9e238eb362874d273b218cacd9e6 Mon Sep 17 00:00:00 2001 From: 30178597 Date: Mon, 17 Feb 2025 23:20:27 -0700 Subject: [PATCH 02/10] v1.01 POST 200 success --- src/api/webhook/route.ts | 26 -------------- src/pages/api/test.ts | 8 +++++ src/pages/api/webhook.ts | 75 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 26 deletions(-) delete mode 100644 src/api/webhook/route.ts create mode 100644 src/pages/api/test.ts create mode 100644 src/pages/api/webhook.ts diff --git a/src/api/webhook/route.ts b/src/api/webhook/route.ts deleted file mode 100644 index 0af5411..0000000 --- a/src/api/webhook/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Stripe from "stripe"; -import { NextResponse } from "next/server"; - -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); -export async function POST(req: Request, res: Response) { - const payload = await req.text(); - const response = await JSON.parse(payload); - - const sig = req.headers.get("stripe-signature"); - - const dateTime = new Date(response?.created * 1000).toLocaleDateString(); - const timeString = new Date(response?.created * 1000).toLocaleDateString(); - - try { - let event = stripe.webhooks.constructEvent( - payload, - sig!, - process.env.STRIPE_WEBHOOK_SECRET!, - ); - console.log("event", event.type); - } catch (error) { - console.log(error); - } - - return NextResponse.json({ success: true }); -} diff --git a/src/pages/api/test.ts b/src/pages/api/test.ts new file mode 100644 index 0000000..0fb6b7d --- /dev/null +++ b/src/pages/api/test.ts @@ -0,0 +1,8 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + res.status(200).json({ + stripeKey: process.env.STRIPE_SECRET_KEY || "Not found", + webhookKey: process.env.STRIPE_WEBHOOK_SECRET || "Not found", + }); +} diff --git a/src/pages/api/webhook.ts b/src/pages/api/webhook.ts new file mode 100644 index 0000000..e460fe0 --- /dev/null +++ b/src/pages/api/webhook.ts @@ -0,0 +1,75 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import Stripe from "stripe"; + +// Initialize Stripe +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: "2023-10-16", +}); + +// Disable automatic body parsing +export const config = { + api: { + bodyParser: false, // ❌ Prevent Next.js from parsing request body + }, +}; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + console.log("🔍 Incoming Webhook Request:", { method: req.method }); + + if (req.method !== "POST") { + return res.status(405).json({ error: "❌ Method Not Allowed" }); + } + + const sig = req.headers["stripe-signature"]; + if (!sig) { + console.error("❌ Missing Stripe-Signature Header"); + return res.status(400).json({ error: "Missing Stripe-Signature Header" }); + } + + if (!process.env.STRIPE_WEBHOOK_SECRET) { + console.error("❌ STRIPE_WEBHOOK_SECRET is missing in .env.local!"); + return res.status(500).json({ error: "Server Configuration Error" }); + } + + // Read raw body (Stripe requires raw body for verification) + let rawBody = await new Promise((resolve, reject) => { + let data: Buffer[] = []; + req.on("data", (chunk) => data.push(chunk)); + req.on("end", () => resolve(Buffer.concat(data))); + req.on("error", (err) => reject(err)); + }); + + let event; + try { + event = stripe.webhooks.constructEvent( + rawBody, // Pass raw body as Buffer + sig, + process.env.STRIPE_WEBHOOK_SECRET!, + ); + } catch (err: any) { + console.error("❌ Webhook Signature Verification Failed:", err.message); + return res.status(400).json({ error: err.message }); + } + + console.log("✅ Webhook Event Verified:", { type: event.type }); + + // ✅ Handle different Stripe event types explicitly + switch (event.type) { + case "payment_intent.succeeded": + console.log("✅ Payment Successful:", event.data.object); + break; + case "payment_intent.payment_failed": + console.log("❌ Payment Failed:", event.data.object); + break; + case "charge.refunded": + console.log("🔄 Charge Refunded:", event.data.object); + break; + default: + console.log("â„šī¸ Unhandled Event Type:", event.type); + } + + return res.status(200).json({ status: "✅ Success", received: true }); +} From 5e9e306a38d076826cb2c90893de8339801687f3 Mon Sep 17 00:00:00 2001 From: 30178597 Date: Tue, 18 Feb 2025 12:46:22 -0700 Subject: [PATCH 03/10] v1.02 yarn build test on vercel --- src/pages/api/webhook.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pages/api/webhook.ts b/src/pages/api/webhook.ts index e460fe0..2adc949 100644 --- a/src/pages/api/webhook.ts +++ b/src/pages/api/webhook.ts @@ -2,9 +2,7 @@ import { NextApiRequest, NextApiResponse } from "next"; import Stripe from "stripe"; // Initialize Stripe -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: "2023-10-16", -}); +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); // Disable automatic body parsing export const config = { @@ -35,8 +33,8 @@ export default async function handler( } // Read raw body (Stripe requires raw body for verification) - let rawBody = await new Promise((resolve, reject) => { - let data: Buffer[] = []; + const rawBody = await new Promise((resolve, reject) => { + const data: Buffer[] = []; req.on("data", (chunk) => data.push(chunk)); req.on("end", () => resolve(Buffer.concat(data))); req.on("error", (err) => reject(err)); @@ -49,9 +47,14 @@ export default async function handler( sig, process.env.STRIPE_WEBHOOK_SECRET!, ); - } catch (err: any) { - console.error("❌ Webhook Signature Verification Failed:", err.message); - return res.status(400).json({ error: err.message }); + } catch (err: unknown) { + if (err instanceof Error) { + console.error("❌ Webhook Signature Verification Failed:", err.message); + return res.status(400).json({ error: err.message }); + } else { + console.error("❌ Webhook Signature Verification Failed: Unknown error"); + return res.status(400).json({ error: "Unknown error occurred" }); + } } console.log("✅ Webhook Event Verified:", { type: event.type }); @@ -71,5 +74,5 @@ export default async function handler( console.log("â„šī¸ Unhandled Event Type:", event.type); } - return res.status(200).json({ status: "✅ Success", received: true }); + return res.status(200).json({ received: true, status: "✅ Success" }); } From 0a4ea42c96b4db064ddee5805a1803e9b55c0991 Mon Sep 17 00:00:00 2001 From: 30178597 Date: Tue, 18 Feb 2025 14:55:02 -0700 Subject: [PATCH 04/10] v1.03 api/checkout.ts payment with button? --- src/components/Merch/MerchItems.tsx | 55 ++++++++++++----------- src/components/Merch/MerchPageContent.tsx | 1 - src/components/Merch/PaymentButton.tsx | 36 +++++++++++++++ src/pages/api/checkout.ts | 38 ++++++++++++++++ 4 files changed, 102 insertions(+), 28 deletions(-) create mode 100644 src/components/Merch/PaymentButton.tsx create mode 100644 src/pages/api/checkout.ts diff --git a/src/components/Merch/MerchItems.tsx b/src/components/Merch/MerchItems.tsx index ddea7d3..eeef835 100644 --- a/src/components/Merch/MerchItems.tsx +++ b/src/components/Merch/MerchItems.tsx @@ -1,46 +1,47 @@ import Image from "next/image"; -import Link from "next/link"; +import PaymentButton from "./PaymentButton"; export const MerchItems = ({ clothingImg, - link, price, title, }: { clothingImg: string; - link: string; price: string; title: string; }) => { return ( - -
-
- Merch Image -
+
+
+
+
+ Merch Image +
- {/* Title and Price Section */} -
-

- {title} -

-

${price} CAD

-
+ {/* Title and Price Section */} +
+

+ {title} +

+

+ ${price} CAD +

+
- {/* Button Section */} -
- + {/* Button Section */} +
+ +
- +
); }; + export default MerchItems; diff --git a/src/components/Merch/MerchPageContent.tsx b/src/components/Merch/MerchPageContent.tsx index 3c1b2e9..bb71f77 100644 --- a/src/components/Merch/MerchPageContent.tsx +++ b/src/components/Merch/MerchPageContent.tsx @@ -37,7 +37,6 @@ const MerchPageContent = () => {
diff --git a/src/components/Merch/PaymentButton.tsx b/src/components/Merch/PaymentButton.tsx new file mode 100644 index 0000000..64e29b8 --- /dev/null +++ b/src/components/Merch/PaymentButton.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useState } from "react"; + +export default function PaymentButton() { + const [loading, setLoading] = useState(false); + + const handleCheckout = async () => { + setLoading(true); + try { + const res = await fetch("/api/checkout", { method: "POST" }); + const data = await res.json(); + + if (data.url) { + window.location.href = data.url; // Redirect user to Stripe Checkout + } else { + alert("Failed to create checkout session."); + } + } catch (error) { + console.error("Error:", error); + alert("Something went wrong."); + } finally { + setLoading(false); + } + }; + + return ( + + ); +} diff --git a/src/pages/api/checkout.ts b/src/pages/api/checkout.ts new file mode 100644 index 0000000..eca4adb --- /dev/null +++ b/src/pages/api/checkout.ts @@ -0,0 +1,38 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import Stripe from "stripe"; + +// Initialize Stripe +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== "POST") { + return res.status(405).json({ error: "❌ Method Not Allowed" }); + } + + try { + const session = await stripe.checkout.sessions.create({ + cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`, + line_items: [ + { + price_data: { + currency: "usd", + product_data: { name: "Your Product Name" }, + unit_amount: 100, // Amount in cents (e.g., $1.00) + }, + quantity: 1, + }, + ], + mode: "payment", + payment_method_types: ["card"], + success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success`, + }); + + return res.status(200).json({ url: session.url }); + } catch (error) { + console.error("Error creating checkout session:", error); + return res.status(500).json({ error: "Internal Server Error" }); + } +} From 0468c423f34794597269d9ffb5ae72a2c09ce857 Mon Sep 17 00:00:00 2001 From: 30178597 Date: Tue, 18 Feb 2025 22:43:08 -0700 Subject: [PATCH 05/10] v1.04 passed on vercel deploy w/ test_sk + /sucess findable page --- package.json | 1 + src/pages/api/checkout.ts | 4 +-- src/pages/success/index.tsx | 62 +++++++++++++++++++++++++++++++++++++ yarn.lock | 10 ++++++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/pages/success/index.tsx diff --git a/package.json b/package.json index e20635c..5a21a5f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react": "^18", "react-countup": "^6.5.3", "react-dom": "^18", + "react-icons": "^5.5.0", "react-intersection-observer": "^9.13.1", "react-lazy-load-image-component": "^1.6.2", "react-multi-carousel": "^2.8.5", diff --git a/src/pages/api/checkout.ts b/src/pages/api/checkout.ts index eca4adb..7cf851d 100644 --- a/src/pages/api/checkout.ts +++ b/src/pages/api/checkout.ts @@ -18,9 +18,9 @@ export default async function handler( line_items: [ { price_data: { - currency: "usd", + currency: "cad", product_data: { name: "Your Product Name" }, - unit_amount: 100, // Amount in cents (e.g., $1.00) + unit_amount: 200, // Amount in cents (e.g., $1.00) }, quantity: 1, }, diff --git a/src/pages/success/index.tsx b/src/pages/success/index.tsx new file mode 100644 index 0000000..4cd9128 --- /dev/null +++ b/src/pages/success/index.tsx @@ -0,0 +1,62 @@ +import { motion } from "framer-motion"; +import { FiCheckCircle } from "react-icons/fi"; +import Link from "next/link"; + +// Common transition properties for all animations +const commonTransition = { + duration: 1, + ease: "easeOut", +}; + +const SuccessPage = () => { + return ( +
+ {/* Animated Icon Intro */} + + + + + {/* Success Message */} + + Payment Successful! + + + {/* Personalized Thank-You Message */} + + Thank you for supporting TechStart UCalgary. Your transaction has been + completed. + + + {/* Return to Merch Button */} + + + + + +
+ ); +}; + +export default SuccessPage; diff --git a/yarn.lock b/yarn.lock index 84db458..8ce564d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4950,6 +4950,15 @@ __metadata: languageName: node linkType: hard +"react-icons@npm:^5.5.0": + version: 5.5.0 + resolution: "react-icons@npm:5.5.0" + peerDependencies: + react: "*" + checksum: 10c0/a24309bfc993c19cbcbfc928157e53a137851822779977b9588f6dd41ffc4d11ebc98b447f4039b0d309a858f0a42980f6bfb4477fb19f9f2d1bc2e190fcf79c + languageName: node + linkType: hard + "react-intersection-observer@npm:^9.13.1": version: 9.13.1 resolution: "react-intersection-observer@npm:9.13.1" @@ -5261,6 +5270,7 @@ __metadata: react: "npm:^18" react-countup: "npm:^6.5.3" react-dom: "npm:^18" + react-icons: "npm:^5.5.0" react-intersection-observer: "npm:^9.13.1" react-lazy-load-image-component: "npm:^1.6.2" react-multi-carousel: "npm:^2.8.5" From 6575544eb9a88f599d9bd544b0fe016c9dbc78c6 Mon Sep 17 00:00:00 2001 From: 30178597 Date: Wed, 19 Feb 2025 14:37:53 -0700 Subject: [PATCH 06/10] v1.05 per product diffrent price --- src/components/Merch/MerchItems.tsx | 4 +++- src/components/Merch/MerchPageContent.tsx | 1 + src/components/Merch/PaymentButton.tsx | 10 ++++++++-- src/lib/data/MerchData.ts | 3 +++ src/pages/api/checkout.ts | 20 +++++++++----------- src/pages/success/index.tsx | 16 ++++++++-------- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/components/Merch/MerchItems.tsx b/src/components/Merch/MerchItems.tsx index eeef835..16c753e 100644 --- a/src/components/Merch/MerchItems.tsx +++ b/src/components/Merch/MerchItems.tsx @@ -4,10 +4,12 @@ import PaymentButton from "./PaymentButton"; export const MerchItems = ({ clothingImg, price, + priceId, title, }: { clothingImg: string; price: string; + priceId: string; title: string; }) => { return ( @@ -36,7 +38,7 @@ export const MerchItems = ({ {/* Button Section */}
- +
diff --git a/src/components/Merch/MerchPageContent.tsx b/src/components/Merch/MerchPageContent.tsx index bb71f77..5850338 100644 --- a/src/components/Merch/MerchPageContent.tsx +++ b/src/components/Merch/MerchPageContent.tsx @@ -38,6 +38,7 @@ const MerchPageContent = () => {
diff --git a/src/components/Merch/PaymentButton.tsx b/src/components/Merch/PaymentButton.tsx index 64e29b8..b6f32e2 100644 --- a/src/components/Merch/PaymentButton.tsx +++ b/src/components/Merch/PaymentButton.tsx @@ -2,13 +2,19 @@ import { useState } from "react"; -export default function PaymentButton() { +export default function PaymentButton({ priceId }: { priceId: string }) { const [loading, setLoading] = useState(false); const handleCheckout = async () => { setLoading(true); try { - const res = await fetch("/api/checkout", { method: "POST" }); + const res = await fetch("/api/checkout", { + body: JSON.stringify({ priceId }), // Send the priceId to the API + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); const data = await res.json(); if (data.url) { diff --git a/src/lib/data/MerchData.ts b/src/lib/data/MerchData.ts index 908a970..f25ae43 100644 --- a/src/lib/data/MerchData.ts +++ b/src/lib/data/MerchData.ts @@ -4,6 +4,7 @@ type MerchItem = { link: string; price: string; title: string; + priceId: string; }; export const MerchItemsData: MerchItem[] = [ @@ -12,6 +13,7 @@ export const MerchItemsData: MerchItem[] = [ id: 0, link: "https://docs.google.com/forms/d/e/1FAIpQLSfpXS4hisen7IBvMGZnrfYWH600W_vpJwW0-b7blsA-D5Dq2w/viewform", price: "29.95", + priceId: "price_1QuK3UIJ8lAYncJXcSjd0Hkl", title: "Classic Crew Neck", }, { @@ -19,6 +21,7 @@ export const MerchItemsData: MerchItem[] = [ id: 1, link: "https://docs.google.com/forms/d/e/1FAIpQLSfpXS4hisen7IBvMGZnrfYWH600W_vpJwW0-b7blsA-D5Dq2w/viewform", price: "49.99", + priceId: "price_1QuK3UIJ8lAYncJXcSjd0Hkl", title: "Crew Neck with Custom Sleeve Print", }, ]; diff --git a/src/pages/api/checkout.ts b/src/pages/api/checkout.ts index 7cf851d..9e65307 100644 --- a/src/pages/api/checkout.ts +++ b/src/pages/api/checkout.ts @@ -13,18 +13,16 @@ export default async function handler( } try { + // Extract the priceId from the request body + const { priceId } = req.body; + if (!priceId) { + return res.status(400).json({ error: "Price ID is required" }); + } + + // Create a Stripe Checkout Session using the provided price ID const session = await stripe.checkout.sessions.create({ - cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`, - line_items: [ - { - price_data: { - currency: "cad", - product_data: { name: "Your Product Name" }, - unit_amount: 200, // Amount in cents (e.g., $1.00) - }, - quantity: 1, - }, - ], + cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/merch`, + line_items: [{ price: priceId, quantity: 1 }], mode: "payment", payment_method_types: ["card"], success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success`, diff --git a/src/pages/success/index.tsx b/src/pages/success/index.tsx index 4cd9128..5da21a5 100644 --- a/src/pages/success/index.tsx +++ b/src/pages/success/index.tsx @@ -13,30 +13,30 @@ const SuccessPage = () => {
{/* Animated Icon Intro */} {/* Success Message */} Payment Successful! {/* Personalized Thank-You Message */} Thank you for supporting TechStart UCalgary. Your transaction has been completed. @@ -44,10 +44,10 @@ const SuccessPage = () => { {/* Return to Merch Button */}
diff --git a/src/components/Merch/MerchPageContent.tsx b/src/components/Merch/MerchPageContent.tsx index 5850338..f1f4c20 100644 --- a/src/components/Merch/MerchPageContent.tsx +++ b/src/components/Merch/MerchPageContent.tsx @@ -1,13 +1,52 @@ +"use client"; + +import { useState, useEffect } from "react"; import { motion } from "framer-motion"; import { NewlineText } from "../../utility/Helpers"; import { merchPageLottieOptions } from "../../utility/LottieOptions"; import { MerchItems } from "./MerchItems"; -import { MerchItemsData } from "../../lib/data/MerchData"; import dynamic from "next/dynamic"; +import Image from "next/image"; const Lottie = dynamic(() => import("lottie-react"), { ssr: false }); const MerchPageContent = () => { + const [merchItems, setMerchItems] = useState< + { + id: string; + price: string; + priceId: string; + title: string; + clothingImg: string; + }[] + >([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // ✅ Fetch merch data when the component loads + useEffect(() => { + async function fetchMerchData() { + try { + const response = await fetch("/api/getProducts"); + + const data = await response.json(); + if (!response.ok) + throw new Error(data.error || "Failed to load merch data"); + + setMerchItems(data.prices); + } catch (err) { + setError((err as Error).message); + } finally { + setLoading(false); + } + } + + fetchMerchData(); + }, []); // Runs only once when component mounts + + if (loading) return

Loading merch...

; + if (error) return

Error: {error}

; + return (
{
- {NewlineText("Our Merch")} - + - {/* Merch Items Grid - Max 3 per row */} + {/* Merch Items Grid */}
- {MerchItemsData.map((merchItem) => ( + {merchItems.map((merchItem, index) => ( +
+ {/* Product Image */} + {merchItem.title + + {/* Product Details */} +

+ ID: {merchItem.id} +

+

+ Price: ${merchItem.price || "N/A"} +

+

+ Price ID: {merchItem.priceId || "N/A"} +

+

+ Title: {merchItem.title || "Untitled Product"} +

+

+ Index: {index} +

+
+ ))} + + {merchItems.map((merchItem) => (
diff --git a/src/pages/api/getProducts.ts b/src/pages/api/getProducts.ts new file mode 100644 index 0000000..70b5ecb --- /dev/null +++ b/src/pages/api/getProducts.ts @@ -0,0 +1,66 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import Stripe from "stripe"; + +if (!process.env.STRIPE_SECRET_KEY) { + throw new Error("STRIPE_SECRET_KEY is not set in environment variables."); +} + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: "2025-01-27.acacia", // Ensure consistent API versioning +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== "GET") { + return res + .setHeader("Cache-Control", "no-store") + .status(405) + .json({ error: "Method Not Allowed" }); + } + + try { + res.setHeader("Cache-Control", "no-store, max-age=0, must-revalidate"); // Ensures fresh response + + // Fetch prices from Stripe + const prices = await stripe.prices.list({ limit: 5 }); + + // Fetch product details for each price entry + const productDetails = await Promise.all( + prices.data.map(async (price) => { + try { + const product = await stripe.products.retrieve( + price.product as string, + ); + + return { + amount: Number(price.unit_amount || 0) / 100, // Convert cents to dollars + clothingImg: product.images?.[0] || "/default-image.png", // Default fallback + currency: price.currency, + id: price.id, + priceId: price.id, + productId: product.id, + title: product.name || "Unknown Product", + }; + } catch (err) { + console.error(`Error fetching product ${price.product}:`, err); + return { + amount: "unit_amount_decimal", + clothingImg: "/default-image.png", + currency: price.currency, + id: price.id, + priceId: price.id, + productId: price.product, + title: "Error Fetching Product", + }; + } + }), + ); + + return res.status(200).json({ prices: productDetails }); + } catch (error) { + console.error("Stripe API error:", error); + return res.status(500).json({ error: "Internal Server Error" }); + } +} From 15520cb8d910ed0e4fe1e0a770c32f22a8d4180d Mon Sep 17 00:00:00 2001 From: 30178597 Date: Thu, 13 Mar 2025 15:50:37 -0600 Subject: [PATCH 10/10] v1.08 custom field and get(). API query, so i did {active ? ... : null}. --- package.json | 2 +- src/components/Merch/MerchItems.tsx | 7 ++- src/components/Merch/MerchPageContent.tsx | 66 +++++++---------------- src/pages/api/getProducts.ts | 17 +++--- yarn.lock | 10 ++-- 5 files changed, 38 insertions(+), 64 deletions(-) diff --git a/package.json b/package.json index 5a21a5f..c7740f2 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "react-scroll": "^1.9.0", "react-tsparticles": "^2.9.3", "react-visibility-sensor": "^5.1.1", - "stripe": "^17.6.0", + "stripe": "^17.7.0", "styled-components": "^6.1.13", "tsparticles": "^2.9.3" }, diff --git a/src/components/Merch/MerchItems.tsx b/src/components/Merch/MerchItems.tsx index e662e19..c63cdf5 100644 --- a/src/components/Merch/MerchItems.tsx +++ b/src/components/Merch/MerchItems.tsx @@ -8,13 +8,13 @@ export const MerchItems = ({ title, }: { clothingImg: string; - price: string; + price: number; priceId: string; title: string; }) => { return (
-
+

- ${Number(price || 0).toFixed(2)} CAD + {price?.toFixed(2)} CAD

- {/* Button Section */}
diff --git a/src/components/Merch/MerchPageContent.tsx b/src/components/Merch/MerchPageContent.tsx index f1f4c20..2c9596d 100644 --- a/src/components/Merch/MerchPageContent.tsx +++ b/src/components/Merch/MerchPageContent.tsx @@ -6,7 +6,6 @@ import { NewlineText } from "../../utility/Helpers"; import { merchPageLottieOptions } from "../../utility/LottieOptions"; import { MerchItems } from "./MerchItems"; import dynamic from "next/dynamic"; -import Image from "next/image"; const Lottie = dynamic(() => import("lottie-react"), { ssr: false }); @@ -14,10 +13,11 @@ const MerchPageContent = () => { const [merchItems, setMerchItems] = useState< { id: string; - price: string; + price: number; priceId: string; title: string; clothingImg: string; + active: boolean; }[] >([]); const [loading, setLoading] = useState(true); @@ -32,7 +32,6 @@ const MerchPageContent = () => { const data = await response.json(); if (!response.ok) throw new Error(data.error || "Failed to load merch data"); - setMerchItems(data.prices); } catch (err) { setError((err as Error).message); @@ -40,7 +39,6 @@ const MerchPageContent = () => { setLoading(false); } } - fetchMerchData(); }, []); // Runs only once when component mounts @@ -52,7 +50,6 @@ const MerchPageContent = () => { className="flex flex-col items-center justify-center" id="merchPageTop" > - {/* Header Section */}
@@ -61,58 +58,33 @@ const MerchPageContent = () => {
{NewlineText("Our Merch")}
+

+ {JSON.stringify(merchItems)} +

{/* Merch Items Grid */}
- {merchItems.map((merchItem, index) => ( -
- {/* Product Image */} - {merchItem.title - - {/* Product Details */} -

- ID: {merchItem.id} -

-

- Price: ${merchItem.price || "N/A"} -

-

- Price ID: {merchItem.priceId || "N/A"} -

-

- Title: {merchItem.title || "Untitled Product"} -

-

- Index: {index} -

-
- ))} - - {merchItems.map((merchItem) => ( -
- -
- ))} + {merchItems.map( + (merchItem) => + merchItem.active ? ( // Check if `active` is true +
+ +
+ ) : null, // If not active, don't render anything + )}
); diff --git a/src/pages/api/getProducts.ts b/src/pages/api/getProducts.ts index 70b5ecb..413ef19 100644 --- a/src/pages/api/getProducts.ts +++ b/src/pages/api/getProducts.ts @@ -6,9 +6,8 @@ if (!process.env.STRIPE_SECRET_KEY) { } const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { - apiVersion: "2025-01-27.acacia", // Ensure consistent API versioning + apiVersion: "2025-02-24.acacia", }); - export default async function handler( req: NextApiRequest, res: NextApiResponse, @@ -21,10 +20,12 @@ export default async function handler( } try { - res.setHeader("Cache-Control", "no-store, max-age=0, must-revalidate"); // Ensures fresh response + res.setHeader("Cache-Control", "no-store, max-age=0, must-revalidate"); // Fetch prices from Stripe - const prices = await stripe.prices.list({ limit: 5 }); + const prices = await stripe.prices.search({ + query: 'active:"true"', + }); // Fetch product details for each price entry const productDetails = await Promise.all( @@ -35,10 +36,11 @@ export default async function handler( ); return { - amount: Number(price.unit_amount || 0) / 100, // Convert cents to dollars - clothingImg: product.images?.[0] || "/default-image.png", // Default fallback + active: product.active ?? price.active, // Ensure active status is retrieved + clothingImg: product.images?.[0] || "/default-image.png", currency: price.currency, id: price.id, + price: (price.unit_amount ?? 0) / 100, // Convert cents to dollars priceId: price.id, productId: product.id, title: product.name || "Unknown Product", @@ -46,10 +48,11 @@ export default async function handler( } catch (err) { console.error(`Error fetching product ${price.product}:`, err); return { - amount: "unit_amount_decimal", + active: price.active, // Ensure active status is still included clothingImg: "/default-image.png", currency: price.currency, id: price.id, + price: (price.unit_amount ?? 0) / 100, // Fix incorrect unit amount priceId: price.id, productId: price.product, title: "Error Fetching Product", diff --git a/yarn.lock b/yarn.lock index 8ce564d..ff76088 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5277,7 +5277,7 @@ __metadata: react-scroll: "npm:^1.9.0" react-tsparticles: "npm:^2.9.3" react-visibility-sensor: "npm:^5.1.1" - stripe: "npm:^17.6.0" + stripe: "npm:^17.7.0" styled-components: "npm:^6.1.13" tailwindcss: "npm:^3.4.4" tsparticles: "npm:^2.9.3" @@ -5721,13 +5721,13 @@ __metadata: languageName: node linkType: hard -"stripe@npm:^17.6.0": - version: 17.6.0 - resolution: "stripe@npm:17.6.0" +"stripe@npm:^17.7.0": + version: 17.7.0 + resolution: "stripe@npm:17.7.0" dependencies: "@types/node": "npm:>=8.1.0" qs: "npm:^6.11.0" - checksum: 10c0/78b5570fffeae5e197392d5ab506a4713f1ee7316df4359d5cc7628a31d86e6b29c464684b5de46420ef52d3a39c6a27e3e715c88d0f2968a48104a68285e8a7 + checksum: 10c0/df67c6d455bd0dd87140640924c220fa9581fc00c3267d171f407c8d088f946f61e3ae7e88a89e7dd705b10fd5254630fc943222eb6f003390ebafbd391f81b2 languageName: node linkType: hard