Skip to content
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
23 changes: 13 additions & 10 deletions apps/dashboard/app/(app)/settings/billing/plans/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ export const ChangePlanButton: React.FC<Props> = ({ workspace, newPlan, label })
},
});

const handleClick = () => {
const hasPaymentMethod = !!workspace.stripeCustomerId;
if (!hasPaymentMethod && newPlan === "pro") {
return router.push(`/settings/billing/stripe?new_plan=${newPlan}`);
}

changePlan.mutateAsync({
workspaceId: workspace.id,
plan: newPlan === "free" ? "free" : "pro",
});
};

const isSamePlan = workspace.plan === newPlan;
return (
<Dialog open={open} onOpenChange={setOpen}>
Expand Down Expand Up @@ -90,16 +102,7 @@ export const ChangePlanButton: React.FC<Props> = ({ workspace, newPlan, label })
<Button className="col-span-1" variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
className="col-span-1"
variant="primary"
onClick={() =>
changePlan.mutateAsync({
workspaceId: workspace.id,
plan: newPlan === "free" ? "free" : "pro",
})
}
>
<Button className="col-span-1" variant="primary" onClick={handleClick}>
{changePlan.isLoading ? <Loading /> : "Switch"}
</Button>
</DialogFooter>
Expand Down
16 changes: 14 additions & 2 deletions apps/dashboard/app/(app)/settings/billing/stripe/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import { headers } from "next/headers";
import { redirect } from "next/navigation";
import Stripe from "stripe";

export default async function StripeRedirect() {
type Props = {
searchParams: {
new_plan: "free" | "pro" | undefined;
};
};

export default async function StripeRedirect(props: Props) {
const { new_plan } = props.searchParams;
const tenantId = getTenantId();
if (!tenantId) {
return redirect("/auth/sign-in");
Expand Down Expand Up @@ -53,7 +60,12 @@ export default async function StripeRedirect() {
const baseUrl = process.env.VERCEL_URL ? "https://app.unkey.com" : "http://localhost:3000";

// do not use `new URL(...).searchParams` here, because it will escape the curly braces and stripe will not replace them with the session id
const successUrl = `${baseUrl}/settings/billing/stripe/success?session_id={CHECKOUT_SESSION_ID}`;
let successUrl = `${baseUrl}/settings/billing/stripe/success?session_id={CHECKOUT_SESSION_ID}`;

// if they're coming from the change plan flow, pass along the new plan param
if (new_plan && new_plan !== ws.plan) {
successUrl += `&new_plan=${new_plan}`;
}

const cancelUrl = headers().get("referer") ?? baseUrl;
const session = await stripe.checkout.sessions.create({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import { Code } from "@/components/ui/code";
import { getTenantId } from "@/lib/auth";
import { db, eq, schema } from "@/lib/db";
import { stripeEnv } from "@/lib/env";
import { PostHogClient } from "@/lib/posthog";
import { currentUser } from "@clerk/nextjs";
import { redirect } from "next/navigation";
import Stripe from "stripe";

type Props = {
searchParams: {
session_id: string;
new_plan: "free" | "pro" | undefined;
};
};

export default async function StripeSuccess(props: Props) {
const { session_id, new_plan } = props.searchParams;
const tenantId = getTenantId();
if (!tenantId) {
return redirect("/auth/sign-in");
Expand Down Expand Up @@ -44,14 +47,14 @@ export default async function StripeSuccess(props: Props) {
typescript: true,
});

const session = await stripe.checkout.sessions.retrieve(props.searchParams.session_id);
const session = await stripe.checkout.sessions.retrieve(session_id);
if (!session) {
return (
<EmptyPlaceholder>
<EmptyPlaceholder.Title>Stripe session not found</EmptyPlaceholder.Title>
<EmptyPlaceholder.Description>
The Stripe session <Code>{props.searchParams.session_id}</Code> you are trying to access
does not exist. Please contact support@unkey.dev.
The Stripe session <Code>{session_id}</Code> you are trying to access does not exist.
Please contact support@unkey.dev.
</EmptyPlaceholder.Description>
</EmptyPlaceholder>
);
Expand All @@ -69,14 +72,25 @@ export default async function StripeSuccess(props: Props) {
);
}

const isChangingPlan = new_plan && new_plan !== ws.plan;

await db
.update(schema.workspaces)
.set({
stripeCustomerId: customer.id,
stripeSubscriptionId: session.subscription as string,
trialEnds: null,
...(isChangingPlan ? { plan: new_plan } : {}),
})
.where(eq(schema.workspaces.id, ws.id));

if (isChangingPlan) {
PostHogClient.capture({
distinctId: tenantId,
event: "plan_changed",
properties: { plan: new_plan, workspace: ws.id },
});
}

return redirect("/settings/billing");
}
29 changes: 29 additions & 0 deletions apps/dashboard/lib/posthog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { PostHog } from "posthog-node";

class PostHogClientWrapper {
private static instance: PostHog | null = null;

private constructor() {}

public static getInstance(): PostHog {
if (!PostHogClientWrapper.instance) {
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY || !process.env.NEXT_PUBLIC_POSTHOG_HOST) {
console.warn("PostHog key is missing. Analytics data will not be sent.");
// Return a mock client when the key is not present
PostHogClientWrapper.instance = {
capture: () => {},
// Add other methods from PostHog, implementing them as no-ops
} as unknown as PostHog;
} else {
PostHogClientWrapper.instance = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
flushAt: 1,
flushInterval: 0,
});
}
}
return PostHogClientWrapper.instance;
}
}

export const PostHogClient = PostHogClientWrapper.getInstance();
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"postcss": "8.4.38",
"postcss-focus-visible": "^9.0.1",
"posthog-js": "^1.130.1",
"posthog-node": "^4.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.3",
Expand Down
Loading