Skip to content

Commit

Permalink
Add Stripe donation form
Browse files Browse the repository at this point in the history
Add donation form with Stripe integration and success/failure pages.

* **Donation Form**: Add input fields for donation amount, name, and email address in `src/components/forms/donate-form.tsx`. Implement form submission to create a Stripe Checkout session and handle errors.
* **API Route**: Add `src/pages/api/create-checkout-session.ts` to handle Stripe Checkout session creation. Configure Stripe library and define request handler to create a session.
* **Success Page**: Add `src/pages/donate-success.tsx` to display a success message after a successful donation.
* **Failure Page**: Add `src/pages/donate-failure.tsx` to display a failure message after a failed donation.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/Vets-Who-Code/vets-who-code-app?shareId=XXXX-XXXX-XXXX-XXXX).
  • Loading branch information
jeromehardaway committed Sep 8, 2024
1 parent 51372a3 commit f5a5e1c
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 12 deletions.
72 changes: 60 additions & 12 deletions src/components/forms/donate-form.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,70 @@
import { forwardRef } from "react";
import { useState } from "react";
import { useForm, SubmitHandler } from "react-hook-form";
import axios from "axios";
import clsx from "clsx";

type TProps = {
className?: string;
};

const DonateForm = forwardRef<HTMLDivElement, TProps>(({ className }, ref) => {
interface IFormValues {
amount: number;
name: string;
email: string;
}

const DonateForm = ({ className }: TProps) => {
const [serverMessage, setServerMessage] = useState<string>("");
const { register, handleSubmit, formState: { errors }, reset } = useForm<IFormValues>();

const onSubmit: SubmitHandler<IFormValues> = async (data) => {
try {
const response = await axios.post("/api/create-checkout-session", data);
if (response.status === 200) {
const { sessionId } = response.data;
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!);
await stripe?.redirectToCheckout({ sessionId });
} else {
setServerMessage("There was an error. Please try again later.");
}
} catch (error) {
setServerMessage("There was an error. Please try again later.");
}
};

return (
<div className={clsx(className)} ref={ref}>
<iframe
title="#VWC Donorbox"
src="https://donorbox.org/embed/vetswhocode-donation?show_content=true"
name="vwc-donorbox"
seamless
className="tw-overflow-x-hidden tw-min-h-[900px] md:tw-min-h-[700px]"
/>
</div>
<form className={clsx(className)} onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="amount">Donation Amount</label>
<input
id="amount"
type="number"
{...register("amount", { required: "Donation amount is required" })}
/>
{errors.amount && <span>{errors.amount.message}</span>}
</div>
<div>
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
{...register("name", { required: "Name is required" })}
/>
{errors.name && <span>{errors.name.message}</span>}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...register("email", { required: "Email is required" })}
/>
{errors.email && <span>{errors.email.message}</span>}
</div>
<button type="submit">Donate</button>
{serverMessage && <p>{serverMessage}</p>}
</form>
);
});
};

export default DonateForm;
44 changes: 44 additions & 0 deletions src/pages/api/create-checkout-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2020-08-27",
});

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
try {
const { amount, name, email } = req.body;

const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "Donation",
},
unit_amount: amount * 100,
},
quantity: 1,
},
],
mode: "payment",
success_url: `${req.headers.origin}/donate-success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.origin}/donate-failure`,
metadata: {
name,
email,
},
});

res.status(200).json({ sessionId: session.id });
} catch (err) {
res.status(500).json({ error: "Internal Server Error" });
}
} else {
res.setHeader("Allow", "POST");
res.status(405).end("Method Not Allowed");
}
}
18 changes: 18 additions & 0 deletions src/pages/donate-failure.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useRouter } from "next/router";
import Button from "@ui/button";

const DonateFailurePage = () => {
const router = useRouter();

return (
<div className="tw-container tw-py-15 tw-text-center">
<h1 className="tw-text-4xl tw-font-bold tw-mb-5">Donation Failed</h1>
<p className="tw-text-lg tw-mb-10">
Unfortunately, your donation could not be processed. Please try again.
</p>
<Button onClick={() => router.push("/donate")}>Go to Donation Page</Button>
</div>
);
};

export default DonateFailurePage;
18 changes: 18 additions & 0 deletions src/pages/donate-success.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useRouter } from "next/router";
import Button from "@ui/button";

const DonateSuccessPage = () => {
const router = useRouter();

return (
<div className="tw-container tw-py-15 tw-text-center">
<h1 className="tw-text-4xl tw-font-bold tw-mb-5">Thank You!</h1>
<p className="tw-text-lg tw-mb-10">
Your donation was successful. We appreciate your support!
</p>
<Button onClick={() => router.push("/")}>Go to Home</Button>
</div>
);
};

export default DonateSuccessPage;

0 comments on commit f5a5e1c

Please sign in to comment.