Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
- Billing
- Webhook syncing to Convex

> Entitlement sync is handled by WorkOS (via the [Stripe integration](https://workos.com/docs/authkit/add-ons/stripe)) — the app's only responsibility is [refreshing the session](https://workos.com/docs/user-management/entitlements) after checkout so new entitlements appear in the access token immediately. No app-side Stripe webhook is required for entitlements.

## Getting started

### Prerequisites
Expand Down
28 changes: 5 additions & 23 deletions scripts/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,28 +275,7 @@ async function setupConvex() {
}
}

async function setupStripeWebhook(stripeApiKey: string, webhookUrl: string) {
console.log(`\n${chalk.bold('Setting up Stripe webhooks')}`);

const stripe = new Stripe(stripeApiKey);

try {
const endpoint = await stripe.webhookEndpoints.create({
enabled_events: ['checkout.session.completed'],
url: webhookUrl,
});

console.log(chalk.green('Stripe webhook created successfully'));

console.log('\nAdding Stripe endpoint signing secret as deployment variable in Convex');
await execAsync(`npx convex env set STRIPE_WEBHOOK_SECRET ${endpoint.secret}`);
console.log(chalk.green('Stripe endpoint signing secret set as deployment variable in Convex'));
} catch (error) {
console.log(chalk.red('Failed to create Stripe webhook'));
console.log(error);
process.exit(1);
}

async function setStripeApiKeyInConvex(stripeApiKey: string) {
try {
console.log('\nAdding Stripe API key as deployment variable in Convex');
await execAsync(`npx convex env set STRIPE_API_KEY ${stripeApiKey}`);
Expand Down Expand Up @@ -378,7 +357,10 @@ async function main() {
const convexUrl = env.match(/NEXT_PUBLIC_CONVEX_URL=(.*)/)?.[1];
const webhookUrl = convexUrl?.replace('.cloud', '.site');

await setupStripeWebhook(STRIPE_API_KEY, webhookUrl + '/stripe-webhook');
// Entitlement sync is handled by WorkOS via the Stripe (Entitlements)
// integration — the app does NOT need its own Stripe webhook. We only need
// the Stripe API key available to Convex (used by convex/stripe.ts).
await setStripeApiKeyInConvex(STRIPE_API_KEY);
await setupWorkOSWebhook(WORKOS_API_KEY, webhookUrl + '/workos-webhook');

console.log('\n🎉 Setup completed successfully!');
Expand Down
7 changes: 6 additions & 1 deletion src/app/api/subscribe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export const POST = async (req: NextRequest) => {
stripeCustomerId: customer.id,
});

// Entitlements are provisioned onto the organization's access token by
// WorkOS automatically (via the Stripe Entitlements integration) once the
// Stripe customer ID is set on the org above. No app-side Stripe webhook is
// required. We tag the success URL so /dashboard can refresh the session and
// surface the new entitlements immediately instead of on the next login.
const session = await stripe.checkout.sessions.create({
customer: customer.id,
billing_address_collection: 'auto',
Expand All @@ -59,7 +64,7 @@ export const POST = async (req: NextRequest) => {
},
],
mode: 'subscription',
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard`,
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?checkout=success`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
});

Expand Down
41 changes: 41 additions & 0 deletions src/app/dashboard/checkout-success-refresh.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { refreshAuthkitSession } from '@/actions/refreshAuthkitSession';

// After a successful Stripe checkout, refresh the AuthKit session so the
// entitlements WorkOS just provisioned appear in the access token now, rather
// than on the user's next login/natural refresh. `refreshAuthkitSession` sets
// cookies, so it must run from a client-triggered server action rather than
// during the dashboard's server render.
export function CheckoutSuccessRefresh() {
const router = useRouter();

useEffect(() => {
let cancelled = false;

const refresh = async () => {
try {
await refreshAuthkitSession();
} catch {
// If the refresh fails, still drop the marker below so we don't retry on
// every page load. The entitlements will appear on the next natural
// session refresh.
}
if (cancelled) return;

// Drop the ?checkout=success marker and re-render with fresh entitlements.
router.replace('/dashboard');
router.refresh();
};
Comment thread
greptile-apps[bot] marked this conversation as resolved.

refresh();

return () => {
cancelled = true;
};
}, [router]);

return null;
}
10 changes: 9 additions & 1 deletion src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@ import { Box, Flex, Text, Heading } from '@radix-ui/themes';
import { withAuth } from '@workos-inc/authkit-nextjs';
import { redirect } from 'next/navigation';
import { DashboardContainer } from '../components/layout/dashboard-container';
import { CheckoutSuccessRefresh } from './checkout-success-refresh';

export default async function DashboardPage() {
export default async function DashboardPage({
searchParams,
}: {
searchParams: Promise<{ checkout?: string }>;
}) {
const session = await withAuth({ ensureSignedIn: true });

// This view is restricted to admins
if (session.role !== 'admin') {
return redirect('/product');
}

const { checkout } = await searchParams;

return (
<Flex direction="column" gap="3" width="100%">
{checkout === 'success' && <CheckoutSuccessRefresh />}
<Box>
<Heading>Dashboard</Heading>
</Box>
Expand Down
3 changes: 1 addition & 2 deletions src/app/router/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { workos } from '../api/workos';
import { NextRequest } from 'next/server';

export const GET = async (request: NextRequest) => {
const { session } = await authkit(request);
let { session } = await authkit(request);

if (!session || !session.user) {
return redirect('/pricing');
Expand All @@ -19,7 +19,6 @@ export const GET = async (request: NextRequest) => {
});

if (oms.data.length > 0) {
// @ts-expect-error will be fixed in the next version of @workos-inc/authkit-nextjs
session = await refreshSession({
organizationId: oms.data[0].organizationId,
ensureSignedIn: true,
Expand Down