Skip to content

Commit

Permalink
cmt
Browse files Browse the repository at this point in the history
  • Loading branch information
JUNIORCO committed Sep 8, 2024
1 parent b2060c7 commit 8668cdf
Show file tree
Hide file tree
Showing 25 changed files with 325 additions and 102 deletions.
17 changes: 15 additions & 2 deletions app/actions/fetch-usage.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
"use server";

import stripe from "@/stripe";
import { createClerkClient } from "@clerk/nextjs/server";

export async function fetchUsage(
clerkUserId: string,
stripeCustomerId: string,
stripeSubscriptionId: string,
) {
const subscription =
await stripe.subscriptions.retrieve(stripeSubscriptionId);

const meterEventSummaries = await stripe.billing.meters.listEventSummaries(
const meterEventSummariesPromise = stripe.billing.meters.listEventSummaries(
process.env.STRIPE_METER_ID as string,
{
customer: stripeCustomerId,
Expand All @@ -19,9 +21,20 @@ export async function fetchUsage(
},
);

const clerkClient = createClerkClient({
secretKey: process.env.CLERK_SECRET_KEY as string,
});
const clerkUserPromise = clerkClient.users.getUser(clerkUserId);

const [meterEventSummaries, clerkUser] = await Promise.all([
meterEventSummariesPromise,
clerkUserPromise,
]);

const totalUsage = meterEventSummaries.data.reduce((acc, curr) => {
return acc + curr.aggregated_value;
}, 0);
const clerkRunCount = clerkUser.publicMetadata.runCount as number;

return totalUsage;
return totalUsage + clerkRunCount;
}
2 changes: 2 additions & 0 deletions app/api/clerk/on-user-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ export default async function onUserCreate(data: UserJSON) {
const clerk = clerkClient();
const updatedUser = await clerk.users.updateUserMetadata(data.id, {
publicMetadata: {
runCount: 0,
stripeCustomerId: customer.id,
stripeSetupIntentId: setupIntentId,
stripeSubscriptionId: subscription.id,
stripeSetupSucceeded: false,
},
});

Expand Down
55 changes: 45 additions & 10 deletions app/api/clerk/on-user-delete.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import stripe from "@/stripe";
import { schedules } from "@trigger.dev/sdk/v3";
import prisma from "../../../prisma/prisma";

export default async function onUserDelete(userId: string | undefined) {
if (!userId) {
console.error("User ID is undefined, skipping user deletion");
export default async function onUserDelete(clerkUserId: string | undefined) {
if (!clerkUserId) {
console.error("Clerk User ID is undefined, skipping user deletion");
return;
}

console.log("Searching for customer on stripe with clerkUserId: ", userId);
console.log(
"Searching for customer on stripe with clerkUserId: ",
clerkUserId,
);
const customer = await stripe.customers.search({
query: `metadata['clerkUserId']:\'${userId}\'`,
query: `metadata['clerkUserId']:\'${clerkUserId}\'`,
});
console.log("Customer: ", customer);

Expand All @@ -17,13 +22,43 @@ export default async function onUserDelete(userId: string | undefined) {
return;
}

const customerId = customer.data[0].id;
console.log("Stripe customer ID: ", customerId);
const stripeCustomerId = customer.data[0].id;
console.log("Stripe customer ID: ", stripeCustomerId);

const res = await stripe.customers.del(customerId);
const res = await stripe.customers.del(stripeCustomerId);
if (res.deleted) {
console.log("Deleted customer: ", customerId);
console.log("Deleted customer: ", stripeCustomerId);
} else {
console.error("Failed to delete Stripe customer: ", customerId);
console.error("Failed to delete Stripe customer: ", stripeCustomerId);
}

const userPyngs = await prisma.pyng.findMany({
where: {
OR: [{ clerkUserId }, { stripeCustomerId }],
},
});

for (const pyng of userPyngs) {
if (pyng.triggerScheduleId) {
try {
console.log("Deleting trigger schedule: ", pyng.triggerScheduleId);
const deletedSchedule = await schedules.del(pyng.triggerScheduleId);
await prisma.pyng.delete({
where: {
id: pyng.id,
},
});
console.log("Deleted schedule: ", deletedSchedule);
} catch (e) {
console.error("Failed to delete trigger schedule: ", e);
}
} else {
console.error("Trigger schedule ID not found for pyng: ", pyng.id);
await prisma.pyng.delete({
where: {
id: pyng.id,
},
});
}
}
}
6 changes: 1 addition & 5 deletions app/api/clerk/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
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";
Expand Down Expand Up @@ -31,14 +30,11 @@ export async function POST(request: Request) {
break;
}
case "user.deleted": {
const userId = payload.data.id;
await onUserDelete(userId);
await onUserDelete(payload.data.id);
break;
}
}

revalidatePath("/");

return Response.json({ message: "Received" });
} catch (e) {
console.error(e);
Expand Down
27 changes: 24 additions & 3 deletions app/api/stripe/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { clerkClient } from "@clerk/nextjs/server";
import { revalidatePath } from "next/cache";
import { headers } from "next/headers";
import Stripe from "stripe";

Expand Down Expand Up @@ -32,6 +31,30 @@ export async function POST(request: Request) {
console.log("Event type: ", event.type);

switch (event.type) {
// reset run count
case "invoice.created": {
const invoice = event.data.object as Stripe.Invoice;
const customerId = invoice.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: {
runCount: 0,
},
});
break;
}

case "setup_intent.succeeded": {
console.log("Setup intent succeeded");
const setupIntent = event.data.object as Stripe.SetupIntent;
Expand All @@ -57,8 +80,6 @@ export async function POST(request: Request) {
}
}

revalidatePath("/");

return Response.json({ received: true });
} catch (e) {
console.error(e);
Expand Down
21 changes: 2 additions & 19 deletions app/components/create-pyng/create-pyng-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,17 @@ import type { IFormInput } from "./types";
import WhenInput from "./when-input";

type CreatePyngFormProps = {
userEmail: string | undefined;
clerkUserId: string | undefined;
stripeSubscriptionId: string | undefined;
stripeSetupSucceeded: boolean | undefined;
};

export default function CreatePyngForm({
userEmail,
clerkUserId,
stripeSubscriptionId,
stripeSetupSucceeded,
}: CreatePyngFormProps) {
export default function CreatePyngForm({ clerkUserId }: CreatePyngFormProps) {
const {
control,
handleSubmit,
formState: { isSubmitting },
formState: { isSubmitting: disabled },
reset,
} = useFormContext<IFormInput>();

const disabled = isSubmitting;

const onSubmit: SubmitHandler<IFormInput> = async (data) => {
if (!clerkUserId) {
toast.success(
Expand All @@ -50,13 +40,6 @@ export default function CreatePyngForm({
return;
}

if (!stripeSubscriptionId || !stripeSetupSucceeded) {
toast.error("Please set up billing to create Pyngs.", {
duration: 3500,
});
return;
}

toast.success("Hold on, this takes a few seconds...", {
icon: "⏳",
duration: 3500,
Expand Down
13 changes: 1 addition & 12 deletions app/components/create-pyng/create-pyng.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@ import { useModal } from "../modal/useModal";
import CreatePyngForm from "./create-pyng-form";

export default function CreatePyng({
userEmail,
clerkUserId,
stripeSubscriptionId,
stripeSetupSucceeded,
}: {
userEmail: string | undefined;
clerkUserId: string | undefined;
stripeSubscriptionId: string | undefined;
stripeSetupSucceeded: boolean | undefined;
}) {
const { openModal, closeModal, content } = useModal();

Expand All @@ -29,12 +23,7 @@ export default function CreatePyng({
How it works <MousePointerClick className="w-4 h-4" />
</button>
<Title className="text-center">Pyngme</Title>
<CreatePyngForm
userEmail={userEmail}
clerkUserId={clerkUserId}
stripeSubscriptionId={stripeSubscriptionId}
stripeSetupSucceeded={stripeSetupSucceeded}
/>
<CreatePyngForm clerkUserId={clerkUserId} />
<Modal content={content} closeModal={closeModal} />
</div>
);
Expand Down
20 changes: 12 additions & 8 deletions app/components/header/billing-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import Usage from "./usage";

export default function BillingSection() {
const { user } = useUser();
const userSetupSucceeded = user?.publicMetadata.stripeSetupSucceeded as
const clerkUserId = user?.id;
const stripeSetupSucceeded = user?.publicMetadata.stripeSetupSucceeded as
| boolean
| undefined;
const stripeSubscriptionId = user?.publicMetadata.stripeSubscriptionId as
Expand All @@ -19,12 +20,15 @@ export default function BillingSection() {
| string
| undefined;

return !userSetupSucceeded ? (
<SetupBilling stripeSetupIntentId={stripeSetupIntentId} />
) : (
<Usage
stripeCustomerId={stripeCustomerId}
stripeSubscriptionId={stripeSubscriptionId}
/>
return (
<div className="flex flex-col md:flex-row gap-4">
<SetupBilling stripeSetupIntentId={stripeSetupIntentId} />
<Usage
stripeCustomerId={stripeCustomerId}
stripeSubscriptionId={stripeSubscriptionId}
clerkUserId={clerkUserId}
stripeSetupSucceeded={stripeSetupSucceeded}
/>
</div>
);
}
7 changes: 6 additions & 1 deletion app/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export default function Header({ src, initialTheme }: HeaderProps) {
</div>
</div>
<div className="flex gap-4">
<BillingSection />
<div className="hidden lg:block">
<BillingSection />
</div>
<div className="hidden lg:block">
<ThemeSelector initialTheme={initialTheme} />
</div>
Expand Down Expand Up @@ -89,6 +91,9 @@ export default function Header({ src, initialTheme }: HeaderProps) {
<li className="lg:hidden mt-8 ml-4">
<Auth />
</li>
<li className="lg:hidden mt-4 ml-4">
<BillingSection />
</li>
</ul>
</div>
</header>
Expand Down
7 changes: 7 additions & 0 deletions app/components/header/setup-billing-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ export default function SetupBillingForm() {
<XIcon className="w-4 h-4" />
</button>
</form>
<div>
<h3 className="text-lg font-bold">Setup Billing (Optional)</h3>
<p>
You need to setup billing to use more than 100 Pyngs per month.
Otherwise, this step is completely optional!
</p>
</div>
<form onSubmit={handleSubmit}>
<PaymentElement />
<button
Expand Down
2 changes: 1 addition & 1 deletion app/components/header/setup-billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function SetupBilling({
return setupIntent ? (
<SetupBillingBtn clientSecret={setupIntent.client_secret} />
) : (
<button type="button" className="btn btn-primary btn-disabled">
<button type="button" className="btn btn-secondary btn-disabled">
Loading <LoaderCircle className="w-4 h-4 animate-spin" />
</button>
);
Expand Down
Loading

0 comments on commit 8668cdf

Please sign in to comment.