-
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
Showing
45 changed files
with
883 additions
and
99 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 |
---|---|---|
@@ -1,11 +1,20 @@ | ||
CLERK_SECRET_KEY="" | ||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" | ||
# client | ||
NEXT_PUBLIC_APP_URL="http://localhost:3000" | ||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_" | ||
NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in" | ||
NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up" | ||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_" | ||
|
||
OPENAI_API_KEY="" | ||
FIRECRAWL_API_KEY="" | ||
RESEND_API_KEY="" | ||
TRIGGER_SECRET_KEY="" | ||
POSTGRES_PRISMA_URL="" | ||
POSTGRES_URL_NON_POOLING="" | ||
# server | ||
CLERK_SECRET_KEY="sk_" | ||
CLERK_WEBHOOK_SECRET="whsec_" | ||
FIRECRAWL_API_KEY="fc-" | ||
OPENAI_API_KEY="sk-" | ||
POSTGRES_PRISMA_URL="postgresql://" | ||
POSTGRES_URL_NON_POOLING="postgresql://" | ||
RESEND_API_KEY="re_" | ||
STRIPE_METER_ID="mtr_" | ||
STRIPE_PRICE_ID="price_" | ||
STRIPE_SECRET_KEY="sk_test_" | ||
STRIPE_WEBHOOK_SECRET="whsec_" | ||
TRIGGER_SECRET_KEY="tr_" |
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 |
---|---|---|
@@ -1 +1,11 @@ | ||
### Pyngme | ||
|
||
`bun run dev` | ||
|
||
`bun run trigger` | ||
|
||
`lt --port 3000 --subdomain big-orange-machine` | ||
|
||
`stripe listen --forward-to localhost:4242/stripe_webhooks` | ||
|
||
Webhooks are at `/api/clerk` and `/api/stripe` |
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,7 @@ | ||
"use server"; | ||
|
||
import stripe from "@/stripe"; | ||
|
||
export async function fetchSetupIntent(stripeSetupIntentId: string) { | ||
return await stripe.setupIntents.retrieve(stripeSetupIntentId); | ||
} |
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,27 @@ | ||
"use server"; | ||
|
||
import stripe from "@/stripe"; | ||
|
||
export async function fetchUsage( | ||
stripeCustomerId: string, | ||
stripeSubscriptionId: string, | ||
) { | ||
const subscription = | ||
await stripe.subscriptions.retrieve(stripeSubscriptionId); | ||
|
||
const meterEventSummaries = await stripe.billing.meters.listEventSummaries( | ||
process.env.STRIPE_METER_ID as string, | ||
{ | ||
customer: stripeCustomerId, | ||
start_time: subscription.current_period_start, | ||
end_time: subscription.current_period_end, | ||
limit: 100, | ||
}, | ||
); | ||
|
||
const totalUsage = meterEventSummaries.data.reduce((acc, curr) => { | ||
return acc + curr.aggregated_value; | ||
}, 0); | ||
|
||
return totalUsage; | ||
} |
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,54 @@ | ||
import stripe from "@/stripe"; | ||
import type { UserJSON } from "@clerk/nextjs/server"; | ||
import { clerkClient } from "@clerk/nextjs/server"; | ||
import type Stripe from "stripe"; | ||
|
||
export default async function onUserCreate(data: UserJSON) { | ||
const customersWithEmail = await stripe.customers.search({ | ||
query: `email:\'${data.email_addresses[0].email_address}\'`, | ||
}); | ||
console.log("Found customers with email: ", customersWithEmail.data.length); | ||
|
||
if (customersWithEmail.data.length > 0) { | ||
console.error("Customer already exists: ", customersWithEmail.data[0].id); | ||
return; | ||
} | ||
|
||
const customer = await stripe.customers.create({ | ||
name: `${data.first_name} ${data.last_name}`, | ||
email: data.email_addresses[0].email_address, | ||
metadata: { | ||
clerkUserId: data.id, | ||
}, | ||
}); | ||
|
||
console.log("Created customer: ", customer); | ||
|
||
const subscription = await stripe.subscriptions.create({ | ||
customer: customer.id, | ||
items: [ | ||
{ | ||
price: process.env.STRIPE_PRICE_ID as string, | ||
}, | ||
], | ||
expand: ["pending_setup_intent"], | ||
}); | ||
|
||
console.log("Created subscription: ", subscription.id); | ||
const setupIntentId = ( | ||
subscription.pending_setup_intent as Stripe.SetupIntent | ||
).id; | ||
|
||
console.log("Setup intent id: ", setupIntentId); | ||
|
||
const clerk = clerkClient(); | ||
const updatedUser = await clerk.users.updateUserMetadata(data.id, { | ||
publicMetadata: { | ||
stripeCustomerId: customer.id, | ||
stripeSetupIntentId: setupIntentId, | ||
stripeSubscriptionId: subscription.id, | ||
}, | ||
}); | ||
|
||
console.log("Updated user: ", updatedUser); | ||
} |
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,29 @@ | ||
import stripe from "@/stripe"; | ||
|
||
export default async function onUserDelete(userId: string | undefined) { | ||
if (!userId) { | ||
console.error("User ID is undefined, skipping user deletion"); | ||
return; | ||
} | ||
|
||
console.log("Searching for customer on stripe with clerkUserId: ", userId); | ||
const customer = await stripe.customers.search({ | ||
query: `metadata['clerkUserId']:\'${userId}\'`, | ||
}); | ||
console.log("Customer: ", customer); | ||
|
||
if (customer.data.length === 0) { | ||
console.error("Customer not found on stripe, skipping deletion"); | ||
return; | ||
} | ||
|
||
const customerId = customer.data[0].id; | ||
console.log("Stripe customer ID: ", customerId); | ||
|
||
const res = await stripe.customers.del(customerId); | ||
if (res.deleted) { | ||
console.log("Deleted customer: ", customerId); | ||
} else { | ||
console.error("Failed to delete Stripe customer: ", customerId); | ||
} | ||
} |
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,47 @@ | ||
import type { WebhookEvent } from "@clerk/nextjs/server"; | ||
import { revalidatePath } from "next/cache"; | ||
import { headers } from "next/headers"; | ||
import { Webhook } from "svix"; | ||
import onUserCreate from "./on-user-create"; | ||
import onUserDelete from "./on-user-delete"; | ||
|
||
async function validateRequest(request: Request) { | ||
const payloadString = await request.text(); | ||
const headerPayload = headers(); | ||
const webhookSecret = process.env.CLERK_WEBHOOK_SECRET || ""; | ||
|
||
const svixHeaders = { | ||
"svix-id": headerPayload.get("svix-id") as string, | ||
"svix-timestamp": headerPayload.get("svix-timestamp") as string, | ||
"svix-signature": headerPayload.get("svix-signature") as string, | ||
}; | ||
const wh = new Webhook(webhookSecret); | ||
return wh.verify(payloadString, svixHeaders) as WebhookEvent; | ||
} | ||
|
||
export async function POST(request: Request) { | ||
try { | ||
console.log("Inside Clerk Webhook"); | ||
const payload = await validateRequest(request); | ||
console.log("Payload type: ", payload.type); | ||
|
||
switch (payload.type) { | ||
case "user.created": { | ||
await onUserCreate(payload.data); | ||
break; | ||
} | ||
case "user.deleted": { | ||
const userId = payload.data.id; | ||
await onUserDelete(userId); | ||
break; | ||
} | ||
} | ||
|
||
revalidatePath("/"); | ||
|
||
return Response.json({ message: "Received" }); | ||
} catch (e) { | ||
console.error(e); | ||
return Response.error(); | ||
} | ||
} |
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,67 @@ | ||
import { clerkClient } from "@clerk/nextjs/server"; | ||
import { revalidatePath } from "next/cache"; | ||
import { headers } from "next/headers"; | ||
import Stripe from "stripe"; | ||
|
||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string); | ||
|
||
async function validateRequest(request: Request) { | ||
const payloadString = await request.text(); | ||
const headerPayload = headers(); | ||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || ""; | ||
|
||
const signature = headerPayload.get("stripe-signature") as string; | ||
|
||
try { | ||
const event = stripe.webhooks.constructEvent( | ||
payloadString, | ||
signature, | ||
webhookSecret, | ||
); | ||
return event; | ||
} catch (err) { | ||
console.error(err); | ||
throw new Error(`Webhook Error: ${err}`); | ||
} | ||
} | ||
|
||
export async function POST(request: Request) { | ||
try { | ||
console.log("Inside Stripe Webhook"); | ||
const event = await validateRequest(request); | ||
console.log("Event type: ", event.type); | ||
|
||
switch (event.type) { | ||
case "setup_intent.succeeded": { | ||
console.log("Setup intent succeeded"); | ||
const setupIntent = event.data.object as Stripe.SetupIntent; | ||
const customerId = setupIntent.customer as string; | ||
|
||
const customer = await stripe.customers.retrieve(customerId); | ||
if (customer.deleted) { | ||
throw new Error("Customer has been deleted."); | ||
} | ||
|
||
const clerkUserId = customer.metadata.clerkUserId as string | undefined; | ||
if (!clerkUserId) { | ||
throw new Error("Clerk user ID not found on customer."); | ||
} | ||
|
||
const clerk = clerkClient(); | ||
await clerk.users.updateUserMetadata(clerkUserId, { | ||
publicMetadata: { | ||
stripeSetupSucceeded: true, | ||
}, | ||
}); | ||
break; | ||
} | ||
} | ||
|
||
revalidatePath("/"); | ||
|
||
return Response.json({ received: true }); | ||
} catch (e) { | ||
console.error(e); | ||
return new Response("Webhook Error", { status: 400 }); | ||
} | ||
} |
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 |
---|---|---|
@@ -1,30 +1,70 @@ | ||
"use client"; | ||
|
||
import Routes from "@/routes"; | ||
import { UserButton, useClerk } from "@clerk/nextjs"; | ||
import { useClerk, useUser } from "@clerk/nextjs"; | ||
import type { UserResource } from "@clerk/types"; | ||
import Link from "next/link"; | ||
import { useState } from "react"; | ||
import { LoaderIcon } from "react-hot-toast"; | ||
|
||
const UserProfile = () => { | ||
const UserProfile = ({ user }: { user: UserResource }) => { | ||
const [loading, setLoading] = useState(false); | ||
const clerk = useClerk(); | ||
|
||
return clerk.loaded ? ( | ||
<UserButton /> | ||
) : ( | ||
<div className="skeleton w-7 h-7 rounded-full" /> | ||
const showProfile = user?.imageUrl; | ||
|
||
const handleSignOut = () => { | ||
setLoading(true); | ||
clerk.signOut(() => { | ||
setLoading(false); | ||
}); | ||
}; | ||
|
||
return ( | ||
<div className="dropdown dropdown-bottom dropdown-end"> | ||
<div tabIndex={0} role="button" className="avatar"> | ||
<div className="w-10 rounded-full"> | ||
<img src={clerk.user?.imageUrl} alt="User avatar" /> | ||
</div> | ||
</div> | ||
<ul | ||
tabIndex={0} | ||
className="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow" | ||
> | ||
<li onClick={() => clerk.openUserProfile()}> | ||
<p>Profile</p> | ||
</li> | ||
<li> | ||
<p>Billing</p> | ||
</li> | ||
<li onClick={handleSignOut}> | ||
<p>Sign Out {loading ? <LoaderIcon /> : null}</p> | ||
</li> | ||
</ul> | ||
</div> | ||
); | ||
}; | ||
|
||
type AuthProps = { | ||
isSignedIn: boolean; | ||
}; | ||
export default function Auth({ isSignedIn }: AuthProps) { | ||
return isSignedIn ? ( | ||
<UserProfile /> | ||
const AuthLoaded = () => { | ||
const { user } = useUser(); | ||
|
||
return user ? ( | ||
<UserProfile user={user} /> | ||
) : ( | ||
<Link href={Routes.signUp}> | ||
<button type="button" className="btn btn-outline"> | ||
Sign Up | ||
</button> | ||
</Link> | ||
); | ||
}; | ||
|
||
export default function Auth() { | ||
const { isLoaded } = useUser(); | ||
|
||
return isLoaded ? ( | ||
<AuthLoaded /> | ||
) : ( | ||
<div className="skeleton w-10 h-10 rounded-full" /> | ||
); | ||
} |
Oops, something went wrong.