Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
35 changes: 35 additions & 0 deletions src/app/dashboard/checkout-success-refresh.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'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 () => {
await refreshAuthkitSession();
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