Skip to content

Commit

Permalink
stripe wip pre-alpha commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Hassunaama committed Oct 17, 2024
1 parent 6ef4270 commit 6700733
Show file tree
Hide file tree
Showing 7 changed files with 501 additions and 3 deletions.
45 changes: 45 additions & 0 deletions src/app/api/stripe/genPaymentIntent/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NextResponse } from "next/server";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

type Item = {
id: string;
quantity: number;
// this will change
};

const calculateOrderAmount = (items: Item[]): number => {
// wip, prisma soon
const amount = /* placeholder currently */ (10000.00 * 100); // it is in cents (14€)

return amount;
};

export async function POST(req: Request) {
try {
const { items }: { items: Item[] } = await req.json();

const paymentIntent = await stripe.paymentIntents.create({
amount: calculateOrderAmount(items),
currency: "eur",
automatic_payment_methods: {
enabled: false,
},
payment_method_types: ["card", "alipay", "wechat_pay", "paypal", "mobilepay", "klarna"]
});

return NextResponse.json({
clientSecret: paymentIntent.client_secret,
amount: paymentIntent.amount
});

} catch (error) {
console.error("Error creating a payment intent:", error);
return NextResponse.json({
error: "An error occurred while creating the payment intent using Stripe, please try again soon. This is not your problem, but rather a problem with us or Stripe.",
}, {
status: 500,
});
}
}
76 changes: 76 additions & 0 deletions src/app/billing/cart/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use client"

import { useState, useEffect } from "react";
import { loadStripe, StripeElementLocale } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";
import { StripeElementsOptions, Appearance } from "@stripe/stripe-js";

import CheckoutForm from "@/components/blocks/checkout/Form";
import CompletePage from "@/components/blocks/checkout/Complete";
import { useLocale } from "next-intl";

export default function Cart() {
const locale = useLocale() as StripeElementLocale;
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, {
locale: locale,
});
const [clientSecret, setClientSecret] = useState<string | null>(null);
const [amount, setAmount] = useState<string | null>(null);
const [confirmed, setConfirmed] = useState<boolean>(false);

useEffect(() => {
const paymentIntentClientSecret = new URLSearchParams(window.location.search).get("payment_intent_client_secret");
if (paymentIntentClientSecret) {
setConfirmed(true);
}
}, []);

useEffect(() => {
fetch("/api/stripe/genPaymentIntent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items: [{ id: "xl-tshirt" }] }),
})
.then((res) => {
if (!res.ok) {
throw new Error("Failed to create payment intent");
}
return res.json();
})
.then((data) => {
setClientSecret(data.clientSecret);
setAmount(data.amount)
})
.catch((error) => {
console.error("Error fetching payment intent:", error);
});
}, []);

const appearance: Appearance = {
theme: 'flat',
variables: {
colorPrimary: '#393cb9',
colorText: '#ffffff',
colorBackground: '#475569',
borderRadius: '6px',
},
};

// Options for the Elements component
const options: StripeElementsOptions | undefined = clientSecret ? {
clientSecret,
appearance,
} : undefined;

return (
<main>
{clientSecret && (
<>
<Elements options={options} stripe={stripePromise}>
{confirmed ? <CompletePage /> : <CheckoutForm amount={amount} />}
</Elements>
</>
)}
</main>
);
}
2 changes: 1 addition & 1 deletion src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()

export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
//adapter: PrismaAdapter(prisma),
pages: {
signIn: '/login',
signOut: '/api/auth/logout',
Expand Down
88 changes: 88 additions & 0 deletions src/components/blocks/checkout/Complete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useState, useEffect } from "react";
import { useStripe } from "@stripe/react-stripe-js";

const STATUS_CONTENT_MAP: {
[key: string]: {
text: string;
iconColor: string;
};
} = {
succeeded: {
text: "Payment succeeded",
iconColor: "#30B130",
},
processing: {
text: "Your payment is processing.",
iconColor: "#6D6E78",
},
requires_payment_method: {
text: "Your payment was not successful, please try again.",
iconColor: "#DF1B41",
},
default: {
text: "Something went wrong, please try again.",
iconColor: "#DF1B41",
},
};


// WIP, COPIED FROM STRIPE DOCS
export default function CompletePage() {
const stripe = useStripe();
const [status, setStatus] = useState<string>("default");
const [intentId, setIntentId] = useState<string | null>(null);

useEffect(() => {
if (!stripe) return;

const clientSecret = new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);
if (!clientSecret) return;

const retrievePaymentIntent = async () => {
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
if (!paymentIntent) return;

setStatus(paymentIntent.status || "default");
setIntentId(paymentIntent.id || null);
};

retrievePaymentIntent();
}, [stripe]);

const currentStatus = STATUS_CONTENT_MAP[status] || STATUS_CONTENT_MAP["default"];

return (
<div id="payment-status">
<h2 id="status-text">{currentStatus.text}</h2>
{intentId && (
<div id="details-table">
<table>
<tbody>
<tr>
<td className="TableLabel">id</td>
<td id="intent-id" className="TableContent">{intentId}</td>
</tr>
<tr>
<td className="TableLabel">status</td>
<td id="intent-status" className="TableContent">{status}</td>
</tr>
</tbody>
</table>
</div>
)}
{intentId && (
<a
href={`https://dashboard.stripe.com/payments/${intentId}`}
id="view-details"
target="_blank"
rel="noopener noreferrer"
>
Stripe link
</a>
)}
<a id="retry-button" href="/billing/cart">Test another</a>
</div>
);
}
163 changes: 163 additions & 0 deletions src/components/blocks/checkout/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { FormEvent, useState, useEffect } from "react";
import {
PaymentElement,
AddressElement,
useStripe,
useElements
} from "@stripe/react-stripe-js";
import { StripePaymentElementOptions, StripeAddressElementOptions } from "@stripe/stripe-js";
import DefaultButton from "@/components/buttons/Default";
import { Session } from "@/components/buttons/NavLogin";
import ProgressBar, { Step } from "./Steps";

export default function CheckoutForm({amount}: {amount: string | null}) {
const stripe = useStripe();
const elements = useElements();

const [session, setSession] = useState<Session | null>(null);
const [message, setMessage] = useState<string | null | undefined>(null);
const [isLoading, setIsLoading] = useState(false);
const [stepIndex, setStepIndex] = useState(0);

const steps: Omit<Step, 'status'>[] = [
{ label: 'Info', icon: 'document' },
{ label: 'Payment', icon: 'document' },
{ label: 'Review', icon: 'lock' },
{ label: 'Finish', icon: 'lock' },
];

const dynamicSteps: Step[] = steps.map((step, index) => ({
...step,
status:
index < stepIndex ? 'completed' : index === stepIndex ? 'active' : 'inactive',
}));

useEffect(() => {
const fetchData = async () => {
setIsLoading(true);

try {
const res = await fetch('/api/auth/session');
const json = await res.json();
setSession(json);
} catch (error) {
console.error('Error fetching session:', error);
} finally {
setIsLoading(false);
}
};

fetchData();
}, []);

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
console.log("test")
e.preventDefault();

if (!stripe || !elements) {
// Stripe.js hasn't yet loaded.
// Make sure to disable form submission until Stripe.js has loaded.
return;
}

setIsLoading(true);

const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: "http://localhost:25572/billing/cart",
},
});

if (error) {
if (error.type === "card_error" || error.type === "validation_error") {
setMessage(error.message);
} else {
setMessage("An unexpected error occurred.");
}
} else {
setMessage("Payment confirmation was successful.");
}

setIsLoading(false);
};

const paymentElementOptions: StripePaymentElementOptions = {
layout: "tabs",
};
const addressElementOptions: StripeAddressElementOptions = {
mode: "billing",
defaultValues: {
name: session?.user.name
}
}

return (
<>
<form id="payment-form" onSubmit={handleSubmit} className="flex flex-col lg:flex-row gap-4">
<div className="flex-grow flex flex-col gap-4">
<div className="bg-defaultBg rounded-2xl px-6 py-5 border-2 border-dotted border-axsoterBlue">
<div className="mb-4">
<ProgressBar steps={dynamicSteps} />
</div>

<AddressElement options={addressElementOptions} className={stepIndex === 0 ? "block" : "hidden"} />
<PaymentElement options={paymentElementOptions} className={stepIndex === 1 ? "block" : "hidden"} />

</div>
<div className="bg-defaultBg rounded-2xl px-6 py-5 border-2 border-dotted border-axsoterBlue">
tänne ne tuotteet
</div>
</div>
<div className="bg-defaultBg rounded-2xl px-6 py-5 border-2 border-dotted border-axsoterBlue lg:relative lg:w-[22rem] lg:h-[36rem] flex-shrink">
<h2 className="text-xl font-bold">Overview:</h2>
<div className="lg:absolute lg:bottom-5 lg:right-5 text-right flex flex-col">
<div className="ml-auto flex flex-row gap-4">
<button
onClick={() => setStepIndex((prev) => Math.max(prev - 1, 0))}
type="button"
className={stepIndex === 0 ? "hidden" : "inline-block"}
>
<DefaultButton>
Go back
</DefaultButton>
</button>
{
stepIndex === 2 ? (
<button
disabled={isLoading || !stripe || !elements}
id="submit"
type="submit"
className="w-fit h-fit ml-auto"
>
<DefaultButton>
{isLoading ? "Loading..." : "Pay now"}
</DefaultButton>
</button>
) : (
<button
disabled={isLoading || !stripe || !elements}
type="button"
onClick={(e) => {
setIsLoading(true);
e.preventDefault();
setStepIndex((prev) => Math.min(prev + 1, steps.length - 1));
setIsLoading(false);
}}
className="w-fit h-fit ml-auto"
>
<DefaultButton>
{isLoading ? "Loading..." : "Continue"}
</DefaultButton>
</button>
)
}
</div>
</div>
</div>
</form>
{/* Show any error or success messages */}
{message && <div id="payment-message">{message}</div>}
</>
);
}
Loading

0 comments on commit 6700733

Please sign in to comment.