Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add stripe #97

Merged
merged 2 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ NEXT_PUBLIC_GITHUB_SECRET='your github secret ID' ## required for next-auth

NEXTAUTH_SECRET='your next-auth secret' ## required for next-auth - generate one here: https://generate-secret.vercel.app/32
NEXTAUTH_URL='http://localhost:3000' ## Only required for localhost

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY='your stripe publishable key' ## required for stripe
STRIPE_SECRET_KEY='your stripe secret key' ## required for stripe
STRIPE_WEBHOOK_SECRET_KEY='your webhook secret key' ## required for stripe
STRIPE_SUBSCRIPTION_PRICE_ID='your subscription price id' ## required for stripe
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- 📘 Typescript
- 🎨 TailwindCSS - Class sorting, merging and linting
- 🛠️ Shadcn/ui - Customizable UI components
- 💵 Stripe - Payment handler
- 🔒 Next-auth - Easy authentication library for Next.js (GitHub provider)
- 🛡️ Prisma - ORM for node.js
- 📋 React-hook-form - Manage your forms easy and efficient
Expand Down
54 changes: 37 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@stripe/stripe-js": "^3.4.0",
"@t3-oss/env-nextjs": "^0.10.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand All @@ -48,6 +49,7 @@
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.51.3",
"stripe": "^15.6.0",
"tailwind-merge": "^2.3.0",
"zod": "^3.23.5"
},
Expand Down
16 changes: 9 additions & 7 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ model Session {
}

model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
stripeCustomerId String? @unique
isActive Boolean @default(false)
accounts Account[]
sessions Session[]
}

model VerificationToken {
Expand Down
31 changes: 31 additions & 0 deletions src/app/api/auth/[...nextauth]/auth-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import GitHubProvider from 'next-auth/providers/github';

import { env } from '@/env.mjs';
import prisma from '@/lib/prisma';
import { stripeServer } from '@/lib/stripe';

export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
Expand All @@ -13,4 +14,34 @@ export const authOptions: NextAuthOptions = {
clientSecret: env.NEXT_PUBLIC_GITHUB_SECRET || '',
}),
],
callbacks: {
async session({ session, user }) {
if (!session.user) return session;

session.user.id = user.id;
session.user.stripeCustomerId = user.stripeCustomerId;
session.user.isActive = user.isActive;

return session;
},
},
events: {
createUser: async ({ user }) => {
if (!user.email || !user.name) return;

await stripeServer.customers
.create({
email: user.email,
name: user.name,
})
.then(async (customer) => {
return prisma.user.update({
where: { id: user.id },
data: {
stripeCustomerId: customer.id,
},
});
});
},
},
};
37 changes: 37 additions & 0 deletions src/app/api/stripe/checkout-session/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';

import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options';
import { env } from '@/env.mjs';
import { stripeServer } from '@/lib/stripe';

export const GET = async () => {
const session = await getServerSession(authOptions);

if (!session?.user) {
return NextResponse.json(
{
error: {
code: 'no-access',
message: 'You are not signed in.',
},
},
{ status: 401 }
);
}

const checkoutSession = await stripeServer.checkout.sessions.create({
mode: 'subscription',
customer: session.user.stripeCustomerId,
line_items: [
{
price: env.STRIPE_SUBSCRIPTION_PRICE_ID,
quantity: 1,
},
],
success_url: `${env.NEXT_PUBLIC_SITE_URL}?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: env.NEXT_PUBLIC_SITE_URL,
});

return NextResponse.json({ session: checkoutSession }, { status: 200 });
};
61 changes: 61 additions & 0 deletions src/app/api/stripe/webhook/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';

import { env } from '@/env.mjs';
import prisma from '@/lib/prisma';
import { stripeServer } from '@/lib/stripe';

const webhookHandler = async (req: NextRequest) => {
try {
const buf = await req.text();
const sig = req.headers.get('stripe-signature')!;

let event: Stripe.Event;

try {
event = stripeServer.webhooks.constructEvent(
buf,
sig,
env.STRIPE_WEBHOOK_SECRET_KEY
);
} catch (err) {
return NextResponse.json(
{
error: {
message: 'Webhook Error',
},
},
{ status: 400 }
);
}

const subscription = event.data.object as Stripe.Subscription;

switch (event.type) {
case 'customer.subscription.created':
await prisma.user.update({
where: {
stripeCustomerId: subscription.customer as string,
},
data: {
isActive: true,
},
});
break;
default:
break;
}
return NextResponse.json({ received: true });
} catch {
return NextResponse.json(
{
error: {
message: 'Method Not Allowed',
},
},
{ status: 405 }
).headers.set('Allow', 'POST');
}
};

export { webhookHandler as POST };
Loading
Loading