Skip to content

Commit

Permalink
Create checkout page and stripe systems (#85)
Browse files Browse the repository at this point in the history
not actually tho, since i have to refactor the repo a lot
  • Loading branch information
Hassunaama authored Dec 17, 2024
2 parents 6ef4270 + a193589 commit 7822ccd
Show file tree
Hide file tree
Showing 16 changed files with 2,348 additions and 1,204 deletions.
3 changes: 0 additions & 3 deletions .eslintrc.json

This file was deleted.

13 changes: 13 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [...compat.extends("next/core-web-vitals")];
2,915 changes: 1,766 additions & 1,149 deletions package-lock.json

Large diffs are not rendered by default.

43 changes: 24 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,41 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 25572",
"dev": "next dev --turbopack -p 25572",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@auth/prisma-adapter": "^2.4.1",
"@prisma/client": "^5.18.0",
"@stripe/react-stripe-js": "^2.8.0",
"@stripe/stripe-js": "^4.3.0",
"framer-motion": "^11.3.21",
"next": "14.2.11",
"next-auth": "^5.0.0-beta.20",
"next-intl": "^3.17.6",
"prisma": "^5.19.1",
"react": "^18",
"react-dom": "^18",
"skinview3d": "^3.0.1",
"stripe": "^16.8.0",
"@auth/prisma-adapter": "^2.7.4",
"@prisma/client": "^6.0.1",
"@stripe/react-stripe-js": "^3.1.1",
"@stripe/stripe-js": "^5.3.0",
"framer-motion": "^11.15.0",
"next": "15.1.0",
"next-auth": "^5.0.0-beta.25",
"next-intl": "^3.26.1",
"prisma": "^6.0.1",
"react": "19.0.0",
"react-dom": "19.0.0",
"skinview3d": "^3.1.0",
"stripe": "^17.4.0",
"vanilla-tilt": "^1.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/scrollreveal": "^0.0.11",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.2",
"eslint": "^9",
"eslint-config-next": "15.1.0",
"postcss": "^8",
"tailwindcss": "^3.4.6",
"typescript": "^5"
},
"overrides": {
"@types/react": "19.0.1",
"@types/react-dom": "19.0.2"
}
}
6 changes: 2 additions & 4 deletions src/app/api/data/misc/minecraftSkin/[username]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export async function GET(
request: Request,
{ params }: { params: { username: string } }
) {
export async function GET(request: Request, props: { params: Promise<{ username: string }> }) {
const params = await props.params;
const username = params.username
// Helper function to fetch the Mojang profile ID based on the username
async function getProfileId(username: string): Promise<string | null> {
Expand Down
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>
);
}
3 changes: 2 additions & 1 deletion src/app/billing/products/[product]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default function Product({ params }: { params: { product: string } }) {
export default async function Product(props: { params: Promise<{ product: string }> }) {
const params = await props.params;
return (
<div>{params.product}</div>
)
Expand Down
31 changes: 16 additions & 15 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ import AxsoterLogo from '@/components/other/AxsoterLogo';
import { auth } from "@/auth"
import { redirect } from 'next/navigation';

export default async function LoginPage({searchParams}: {searchParams: { [key: string]: string | undefined }}) {
const session = await auth()
export default async function LoginPage(props: {searchParams: Promise<{ [key: string]: string | undefined }>}) {
const searchParams = await props.searchParams;
const session = await auth()

if (!session) return (
<main className="grow min-h-screen">
<div className="h-screen my-auto flex items-center justify-center flex-col">
<div className="flex items-center font-semibold text-lg py-4 gap-x-2">
<AxsoterLogo />
</div>
<div className="max-w-lg w-full bg-defaultBg rounded-2xl px-6 py-5 border-2 border-dotted border-axsoterBlue">
<LoginForm queryParams={searchParams} />
</div>
</div>
</main>
);
if (!session) return (
<main className="grow min-h-screen">
<div className="h-screen my-auto flex items-center justify-center flex-col">
<div className="flex items-center font-semibold text-lg py-4 gap-x-2">
<AxsoterLogo />
</div>
<div className="max-w-lg w-full bg-defaultBg rounded-2xl px-6 py-5 border-2 border-dotted border-axsoterBlue">
<LoginForm queryParams={searchParams} />
</div>
</div>
</main>
);

redirect("/");
redirect("/");
}
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>
);
}
Loading

0 comments on commit 7822ccd

Please sign in to comment.