diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 2c72dab..a966641 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,10 @@ This project adheres to [Semantic Versioning](https://semver.org/). +## 1.2.0 + +- Added stripe checkouts + ## 1.1.0 - Automated releases diff --git a/app/api/checkout/route.ts b/app/api/checkout/route.ts new file mode 100644 index 0000000..fcb7f82 --- /dev/null +++ b/app/api/checkout/route.ts @@ -0,0 +1,52 @@ +import { NextResponse } from "next/server"; +import { Stripe } from "stripe"; + +export async function GET() { + return NextResponse.json({ message: "success" }, { status: 200 }); +} +export async function POST(request: Request) { + const stripe = new Stripe(process.env.STRIPE_API_SECRET as string, { + apiVersion: "2022-11-15", + }); + + const data = await request.json(); + const price_id = data.price_id; + + if (!price_id) { + return NextResponse.json( + "create-checkout: please provide a valid price id", + { status: 422 }, + ); + } + + let event; + try { + event = await stripe.checkout.sessions.create({ + success_url: process.env.STRIPE_SUCCESS_URL as string, + cancel_url: process.env.STRIPE_CANCEL_URL as string, + mode: "subscription", + line_items: [ + { + price: price_id, + quantity: 1, + }, + ], + }); + return NextResponse.json( + { message: "success", checkout_url: event.url }, + { status: 200 }, + ); + } catch (error: any) { + return NextResponse.json( + { + message: `create-checkout: ${ + error?.message || "unexpected error" + }`, + }, + { + status: error?.status || 512, + statusText: error || "unexpected error", + }, + ); + } +} diff --git a/components/pricing-card/checkout-button.tsx b/components/pricing-card/checkout-button.tsx new file mode 100644 index 0000000..ca6be57 --- /dev/null +++ b/components/pricing-card/checkout-button.tsx @@ -0,0 +1,42 @@ +"use client"; + +import React, { useState } from "react"; +import { Button, ButtonProps } from "../ui/button"; +import LoadingDots from "../ui/loading-dots"; + +export default function CheckoutButton({ children, ...rest }: ButtonProps) { + const [isLoading, setLoadingState] = useState(false); + + const handleCheckout = async () => { + setLoadingState(true); + const response = await fetch("/api/checkout", { + method: "POST", + body: JSON.stringify({ + price_id: + process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_SUBSCRIPTION_HOBBY, + }), + }); + + if (!response.ok) { + throw new Error( + `client(${response.status}): ${ + response.statusText || "unexpected error" + }`, + ); + } + + const data = await response.json(); + const checkout_url = data?.checkout_url; + if (!checkout_url) + throw new Error("client: no checkout_url found, please try again"); + + setLoadingState(false); + window.location = checkout_url; + }; + + return ( + + ); +} diff --git a/components/pricing-card/pricing-card.tsx b/components/pricing-card/pricing-card.tsx index 7366d0f..ca3580a 100644 --- a/components/pricing-card/pricing-card.tsx +++ b/components/pricing-card/pricing-card.tsx @@ -1,7 +1,5 @@ import { Check } from "lucide-react"; - import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -11,6 +9,7 @@ import { CardTitle, } from "@/components/ui/card"; import { TypographyP } from "../ui/typography"; +import CheckoutButton from "./checkout-button"; export enum PricingTiers { FREE, @@ -71,12 +70,12 @@ export default function PricingCard({ *No credit cards required )} - + ); diff --git a/middleware.ts b/middleware.ts index c515f3e..b2e56f2 100644 --- a/middleware.ts +++ b/middleware.ts @@ -5,10 +5,11 @@ import { authMiddleware } from "@clerk/nextjs"; // See https://clerk.com/docs/nextjs/middleware for more information about configuring your middleware export default authMiddleware({ publicRoutes: ["/", "/(github|twitter|linkedin)"], + ignoredRoutes: ["/(api|trpc)(.*)"], clockSkewInMs: 100_000, clockSkewInSeconds: 100, }); export const config = { - matcher: ["/((?!.*\\..*|_next).*)", "/(dashboard)(.*)", "/(api|trpc)(.*)"], + matcher: ["/((?!.*\\..*|_next).*)", "/(dashboard)(.*)"], }; diff --git a/package-lock.json b/package-lock.json index df4ffb3..4550cce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "next-starter", - "version": "1.0.0", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "next-starter", - "version": "1.0.0", + "version": "1.2.0", "dependencies": { "@clerk/nextjs": "^4.23.1", "@prisma/client": "^5.0.0", @@ -30,6 +30,7 @@ "postcss": "8.4.27", "react": "18.2.0", "react-dom": "18.2.0", + "stripe": "^12.14.0", "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.6", @@ -4591,6 +4592,20 @@ "node": ">=6.0.0" } }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5156,6 +5171,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "12.14.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-12.14.0.tgz", + "integrity": "sha512-WrDlYH1p5jliY7uzSU5nLDY7OCIeRe6FkC0hhScpTGwMthP/Muk38WXGeggjDHKeXAGCs43jUheZ7Ud/NEAJdg==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -8722,6 +8749,14 @@ "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==" }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9082,6 +9117,15 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, + "stripe": { + "version": "12.14.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-12.14.0.tgz", + "integrity": "sha512-WrDlYH1p5jliY7uzSU5nLDY7OCIeRe6FkC0hhScpTGwMthP/Muk38WXGeggjDHKeXAGCs43jUheZ7Ud/NEAJdg==", + "requires": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + } + }, "styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", diff --git a/package.json b/package.json index c400b98..1eaa3d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next-starter", - "version": "1.1.0", + "version": "1.2.0", "private": true, "scripts": { "dev": "cross-env NODE_ENV=development next dev -p 3000", @@ -38,6 +38,7 @@ "postcss": "8.4.27", "react": "18.2.0", "react-dom": "18.2.0", + "stripe": "^12.14.0", "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.6", @@ -45,7 +46,7 @@ }, "devDependencies": { "cross-env": "^7.0.3", - "prisma": "^5.0.0", - "husky": "^8.0.0" + "husky": "^8.0.0", + "prisma": "^5.0.0" } }