-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6ef4270
commit 6700733
Showing
7 changed files
with
501 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>} | ||
</> | ||
); | ||
} |
Oops, something went wrong.