Skip to content

Commit

Permalink
feat: add stripe (#97)
Browse files Browse the repository at this point in the history
* feat: create UI for subscription functionality

* feat: create subscription functionality by using stripe
  • Loading branch information
Skolaczk authored May 11, 2024
1 parent 6715cca commit 67fdd1e
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 28 deletions.
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

0 comments on commit 67fdd1e

Please sign in to comment.