diff --git a/nextjs/generation-based-subscription/bun.lock b/nextjs/generation-based-subscription/bun.lock index 7892d19..2f60d6b 100644 --- a/nextjs/generation-based-subscription/bun.lock +++ b/nextjs/generation-based-subscription/bun.lock @@ -1,11 +1,10 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "nextjs-starter", "dependencies": { - "@flowglad/nextjs": "0.15.0", + "@flowglad/nextjs": "0.16.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", @@ -143,15 +142,15 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@flowglad/nextjs": ["@flowglad/nextjs@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/react": "0.15.0", "@flowglad/server": "0.15.0", "@flowglad/shared": "0.15.0", "@vercel/mcp-adapter": "0.11.1", "zod": "4.1.5" }, "peerDependencies": { "next": "^14.0.3 || ^15.0.0 || ^16.0.0", "react": "^19.0.0 || ^18.0.0" } }, "sha512-5CDd1SkH+dwt16QzvLgZVH5ydbO0uu4AXMkZ5hOBGfONmsvGZ6r0bS3dtQQJtHPBkMGZVqwm529JsFHbTD6hdQ=="], + "@flowglad/nextjs": ["@flowglad/nextjs@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/react": "0.16.2", "@flowglad/server": "0.16.2", "@flowglad/shared": "0.16.2", "@vercel/mcp-adapter": "0.11.1", "zod": "4.1.5" }, "peerDependencies": { "better-auth": "^1.3.17", "next": "^14.0.3 || ^15.0.0 || ^16.0.0", "react": "^19.0.0 || ^18.0.0" }, "optionalPeers": ["better-auth"] }, "sha512-FtdSFV3uD4rnjfI5iA3wqawc6weTUTq1afd/RLnzx6RP44qXg0v2cZzVLZuNHiPiWY9SB3qTnmgD1evL8RVYaw=="], "@flowglad/node": ["@flowglad/node@0.24.0", "", {}, "sha512-P5UhUaYNAGuCT9hLTQC34o+az4hsfMSuSGpYzg9bPQIrRnTkWvJfmyOmE+cGSlaGZCdLNnEwxCwDQr+lVeOyRw=="], - "@flowglad/react": ["@flowglad/react@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.15.0", "@tanstack/react-query": "5.66.0", "clsx": "2.1.1", "date-fns": "4.1.0", "tailwind-merge": "3.0.2", "zod": "4.1.5" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0" } }, "sha512-/CkqMI0dpyezn4enQ1F2m0XzoyHZFXRQ0pB3EAcSMj1683/aRgnDaqbqiaT3mt4ieEf3/D8Cxnm4xOs+xfPf0g=="], + "@flowglad/react": ["@flowglad/react@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.2", "@tanstack/react-query": "5.66.0", "clsx": "2.1.1", "date-fns": "4.1.0", "tailwind-merge": "3.0.2", "zod": "4.1.5" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0" } }, "sha512-BiSf2rTeeLDlNpviooPLMwCgEHCwUplO3l7uqey78HyZiTlrnbDMmh6xY4lVVB6CWIvptjbVg9sPissihW/XCQ=="], - "@flowglad/server": ["@flowglad/server@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.15.0", "zod": "4.1.5" } }, "sha512-XyFRnnx0wl8qOsv+c5kj3+pDxVSGAsoC1YivHTCrGPWGXPCHGhGNsH0XgXxV5BjxjjRuYjb57OkV553WUn/eWQ=="], + "@flowglad/server": ["@flowglad/server@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.2", "nanoid": "^3.3.11", "zod": "4.1.5" }, "peerDependencies": { "better-auth": "^1.3.17", "express": "^4.0.0" }, "optionalPeers": ["better-auth", "express"] }, "sha512-C0mKYL4AVCzy8rqB38TpG3oiMrec3CgHG0zgh8UWnkpYW8A8qN0qSLHHkUmN5Oncvzr3YSJZ4MpmBcWpD7xnjA=="], - "@flowglad/shared": ["@flowglad/shared@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "date-fns": "4.1.0", "zod": "4.1.5" } }, "sha512-6Go6C4JSdloYUcfc98FvVosElhgFegz1tRGW0CFuaf6LPAr442hW/f71HUKouRKrTUD1YMhjoZvPJD+QA7neag=="], + "@flowglad/shared": ["@flowglad/shared@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "date-fns": "4.1.0", "zod": "4.1.5" } }, "sha512-kdNJ7IwVeCxtqpO6HldK7s5Cil9K0r4OFU6s/1ShM922EqU98OrLHqTkXeyXCX/7EW8U+XmDrFRxfINI2KYc4g=="], "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], diff --git a/nextjs/generation-based-subscription/drizzle/0000_dizzy_micromax.sql b/nextjs/generation-based-subscription/drizzle/0000_open_maria_hill.sql similarity index 100% rename from nextjs/generation-based-subscription/drizzle/0000_dizzy_micromax.sql rename to nextjs/generation-based-subscription/drizzle/0000_open_maria_hill.sql diff --git a/nextjs/generation-based-subscription/drizzle/meta/0000_snapshot.json b/nextjs/generation-based-subscription/drizzle/meta/0000_snapshot.json index ff8ad97..94f3dcc 100644 --- a/nextjs/generation-based-subscription/drizzle/meta/0000_snapshot.json +++ b/nextjs/generation-based-subscription/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "e1c4a87d-52d4-4112-8d76-80aa1b288c9e", + "id": "66597033-970b-4b5a-b9fc-d2f242c71df4", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -104,15 +104,7 @@ } }, "compositePrimaryKeys": {}, - "uniqueConstraints": { - "accounts_provider_id_account_id_unique": { - "name": "accounts_provider_id_account_id_unique", - "columns": [ - "provider_id", - "account_id" - ] - } - }, + "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false diff --git a/nextjs/generation-based-subscription/drizzle/meta/_journal.json b/nextjs/generation-based-subscription/drizzle/meta/_journal.json index d40f988..4d0d342 100644 --- a/nextjs/generation-based-subscription/drizzle/meta/_journal.json +++ b/nextjs/generation-based-subscription/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1762366685322, - "tag": "0000_dizzy_micromax", + "when": 1768190947104, + "tag": "0000_open_maria_hill", "breakpoints": true } ] diff --git a/nextjs/generation-based-subscription/package.json b/nextjs/generation-based-subscription/package.json index c54c5bf..b46f6c5 100644 --- a/nextjs/generation-based-subscription/package.json +++ b/nextjs/generation-based-subscription/package.json @@ -20,7 +20,7 @@ "db:studio": "drizzle-kit studio --config=drizzle.config.ts" }, "dependencies": { - "@flowglad/nextjs": "0.15.0", + "@flowglad/nextjs": "0.16.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", diff --git a/nextjs/generation-based-subscription/src/app/api/usage-events/route.ts b/nextjs/generation-based-subscription/src/app/api/usage-events/route.ts deleted file mode 100644 index ce2ee2d..0000000 --- a/nextjs/generation-based-subscription/src/app/api/usage-events/route.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { flowglad } from '@/lib/flowglad'; -import { findUsagePriceByMeterSlug } from '@/lib/billing-helpers'; -import { auth } from '@/lib/auth'; -import { headers } from 'next/headers'; -import { NextResponse } from 'next/server'; -import { z } from 'zod'; - -/** - * POST /api/usage-events - * Creates a usage event for the current customer - * - * Body: { - * usageMeterSlug: string; // e.g., 'fast_generations' - * amount: number; // e.g., 1 - * transactionId?: string; // Optional: for idempotency - * } - */ -const createUsageEventSchema = z.object({ - usageMeterSlug: z.string().min(1, 'usageMeterSlug is required'), - amount: z - .number() - .int('amount must be an integer') - .positive('amount must be a positive integer'), - transactionId: z.string().optional(), -}); - -export async function POST(request: Request) { - try { - const body = await request.json(); - const parseResult = createUsageEventSchema.safeParse(body); - - if (!parseResult.success) { - return NextResponse.json( - { - error: 'Invalid request body', - details: parseResult.error.issues, - }, - { status: 400 } - ); - } - - const { - usageMeterSlug, - amount: amountNumber, - transactionId, - } = parseResult.data; - - // Generate transaction ID if not provided - const finalTransactionId = - transactionId || - `usage_${Date.now()}_${Math.random().toString(36).substring(7)}`; - - // Get customer ID from session - const session = await auth.api.getSession({ headers: await headers() }); - const userId = session?.user?.id; - if (!userId) { - return NextResponse.json( - { error: 'User not found' }, - { status: 401 } - ); - } - - // Get billing information to extract required IDs - const flowgladServer = flowglad(userId); - const billing = await flowgladServer.getBilling(); - - if (!billing.customer) { - return NextResponse.json( - { error: 'Customer not found' }, - { status: 404 } - ); - } - - // Find the current subscription - // By default, each customer can only have one active subscription at a time, - // so accessing the first currentSubscriptions is sufficient. - // Multiple subscriptions per customer can be enabled in dashboard > settings - const currentSubscription = billing.currentSubscriptions?.[0]; - if (!currentSubscription) { - return NextResponse.json( - { error: 'No active subscription found' }, - { status: 404 } - ); - } - - const subscriptionId = currentSubscription.id; - - const usagePrice = findUsagePriceByMeterSlug( - usageMeterSlug, - billing.pricingModel - ); - - if (!usagePrice) { - return NextResponse.json( - { - error: `Usage price not found for meter: ${usageMeterSlug}. Please ensure a usage price product exists for this meter in your pricing model.`, - }, - { status: 404 } - ); - } - - const priceSlug = usagePrice.slug; - - if (!priceSlug) { - return NextResponse.json( - { - error: `Usage price found but missing priceSlug for meter: ${usageMeterSlug}`, - }, - { status: 500 } - ); - } - - // Create usage event with all required IDs - // Note: customerId is automatically resolved from the session by FlowgladServer - // usageMeterId is automatically resolved from priceSlug by Flowglad - const usageEvent = await flowgladServer.createUsageEvent({ - subscriptionId, - priceSlug, - amount: amountNumber, - transactionId: finalTransactionId, - }); - - return NextResponse.json({ - success: true, - usageEvent, - }); - } catch (error) { - return NextResponse.json( - { - error: - error instanceof Error - ? error.message - : 'Failed to create usage event', - }, - { status: 500 } - ); - } -} diff --git a/nextjs/generation-based-subscription/src/app/home-client.tsx b/nextjs/generation-based-subscription/src/app/home-client.tsx index 4361c29..92446f5 100644 --- a/nextjs/generation-based-subscription/src/app/home-client.tsx +++ b/nextjs/generation-based-subscription/src/app/home-client.tsx @@ -165,26 +165,24 @@ export function HomeClient() { setGenerateError(null); try { - // Generate a unique transaction ID for idempotency - const transactionId = `fast_image_${Date.now()}_${Math.random().toString(36).substring(7)}`; + if (!billing.createUsageEvent) { + throw new Error('createUsageEvent is not available'); + } + // Random amount between 3-5 const amount = Math.floor(Math.random() * 3) + 3; - const response = await fetch('/api/usage-events', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - usageMeterSlug: 'fast_generations', - amount, - transactionId, - }), + const result = await billing.createUsageEvent({ + usageMeterSlug: 'fast_generations', + amount, }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to create usage event'); + if ('error' in result) { + const errorMsg = result.error.json?.error ?? result.error.json?.message; + throw new Error( + (typeof errorMsg === 'string' ? errorMsg : null) || + 'Failed to create usage event' + ); } // Cycle through mock images @@ -217,26 +215,20 @@ export function HomeClient() { setHdVideoError(null); try { - // Generate a unique transaction ID for idempotency - const transactionId = `hd_video_${Date.now()}_${Math.random().toString(36).substring(7)}`; // Random amount between 1-3 minutes const amount = Math.floor(Math.random() * 3) + 1; - const response = await fetch('/api/usage-events', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - usageMeterSlug: 'hd_video_minutes', - amount, - transactionId, - }), + const result = await billing.createUsageEvent({ + usageMeterSlug: 'hd_video_minutes', + amount, }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to create usage event'); + if ('error' in result) { + const errorMsg = result.error.json?.error ?? result.error.json?.message; + throw new Error( + (typeof errorMsg === 'string' ? errorMsg : null) || + 'Failed to create usage event' + ); } // Cycle through mock video GIFs diff --git a/nextjs/pay-as-you-go/bun.lock b/nextjs/pay-as-you-go/bun.lock index 37b7c69..e066e56 100644 --- a/nextjs/pay-as-you-go/bun.lock +++ b/nextjs/pay-as-you-go/bun.lock @@ -4,7 +4,7 @@ "": { "name": "nextjs-starter", "dependencies": { - "@flowglad/nextjs": "0.15.0", + "@flowglad/nextjs": "0.16.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-scroll-area": "^1.2.10", @@ -143,15 +143,15 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@flowglad/nextjs": ["@flowglad/nextjs@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/react": "0.15.0", "@flowglad/server": "0.15.0", "@flowglad/shared": "0.15.0", "@vercel/mcp-adapter": "0.11.1", "zod": "4.1.5" }, "peerDependencies": { "next": "^14.0.3 || ^15.0.0 || ^16.0.0", "react": "^19.0.0 || ^18.0.0" } }, "sha512-5CDd1SkH+dwt16QzvLgZVH5ydbO0uu4AXMkZ5hOBGfONmsvGZ6r0bS3dtQQJtHPBkMGZVqwm529JsFHbTD6hdQ=="], + "@flowglad/nextjs": ["@flowglad/nextjs@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/react": "0.16.2", "@flowglad/server": "0.16.2", "@flowglad/shared": "0.16.2", "@vercel/mcp-adapter": "0.11.1", "zod": "4.1.5" }, "peerDependencies": { "better-auth": "^1.3.17", "next": "^14.0.3 || ^15.0.0 || ^16.0.0", "react": "^19.0.0 || ^18.0.0" }, "optionalPeers": ["better-auth"] }, "sha512-FtdSFV3uD4rnjfI5iA3wqawc6weTUTq1afd/RLnzx6RP44qXg0v2cZzVLZuNHiPiWY9SB3qTnmgD1evL8RVYaw=="], "@flowglad/node": ["@flowglad/node@0.24.0", "", {}, "sha512-P5UhUaYNAGuCT9hLTQC34o+az4hsfMSuSGpYzg9bPQIrRnTkWvJfmyOmE+cGSlaGZCdLNnEwxCwDQr+lVeOyRw=="], - "@flowglad/react": ["@flowglad/react@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.15.0", "@tanstack/react-query": "5.66.0", "clsx": "2.1.1", "date-fns": "4.1.0", "tailwind-merge": "3.0.2", "zod": "4.1.5" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0" } }, "sha512-/CkqMI0dpyezn4enQ1F2m0XzoyHZFXRQ0pB3EAcSMj1683/aRgnDaqbqiaT3mt4ieEf3/D8Cxnm4xOs+xfPf0g=="], + "@flowglad/react": ["@flowglad/react@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.2", "@tanstack/react-query": "5.66.0", "clsx": "2.1.1", "date-fns": "4.1.0", "tailwind-merge": "3.0.2", "zod": "4.1.5" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0" } }, "sha512-BiSf2rTeeLDlNpviooPLMwCgEHCwUplO3l7uqey78HyZiTlrnbDMmh6xY4lVVB6CWIvptjbVg9sPissihW/XCQ=="], - "@flowglad/server": ["@flowglad/server@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.15.0", "zod": "4.1.5" } }, "sha512-XyFRnnx0wl8qOsv+c5kj3+pDxVSGAsoC1YivHTCrGPWGXPCHGhGNsH0XgXxV5BjxjjRuYjb57OkV553WUn/eWQ=="], + "@flowglad/server": ["@flowglad/server@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.2", "nanoid": "^3.3.11", "zod": "4.1.5" }, "peerDependencies": { "better-auth": "^1.3.17", "express": "^4.0.0" }, "optionalPeers": ["better-auth", "express"] }, "sha512-C0mKYL4AVCzy8rqB38TpG3oiMrec3CgHG0zgh8UWnkpYW8A8qN0qSLHHkUmN5Oncvzr3YSJZ4MpmBcWpD7xnjA=="], - "@flowglad/shared": ["@flowglad/shared@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "date-fns": "4.1.0", "zod": "4.1.5" } }, "sha512-6Go6C4JSdloYUcfc98FvVosElhgFegz1tRGW0CFuaf6LPAr442hW/f71HUKouRKrTUD1YMhjoZvPJD+QA7neag=="], + "@flowglad/shared": ["@flowglad/shared@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "date-fns": "4.1.0", "zod": "4.1.5" } }, "sha512-kdNJ7IwVeCxtqpO6HldK7s5Cil9K0r4OFU6s/1ShM922EqU98OrLHqTkXeyXCX/7EW8U+XmDrFRxfINI2KYc4g=="], "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], diff --git a/nextjs/pay-as-you-go/package.json b/nextjs/pay-as-you-go/package.json index c19ea30..7e6b794 100644 --- a/nextjs/pay-as-you-go/package.json +++ b/nextjs/pay-as-you-go/package.json @@ -20,7 +20,7 @@ "db:studio": "drizzle-kit studio --config=drizzle.config.ts" }, "dependencies": { - "@flowglad/nextjs": "0.15.0", + "@flowglad/nextjs": "0.16.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-scroll-area": "^1.2.10", diff --git a/nextjs/pay-as-you-go/src/app/api/usage-events/route.ts b/nextjs/pay-as-you-go/src/app/api/usage-events/route.ts deleted file mode 100644 index ce2ee2d..0000000 --- a/nextjs/pay-as-you-go/src/app/api/usage-events/route.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { flowglad } from '@/lib/flowglad'; -import { findUsagePriceByMeterSlug } from '@/lib/billing-helpers'; -import { auth } from '@/lib/auth'; -import { headers } from 'next/headers'; -import { NextResponse } from 'next/server'; -import { z } from 'zod'; - -/** - * POST /api/usage-events - * Creates a usage event for the current customer - * - * Body: { - * usageMeterSlug: string; // e.g., 'fast_generations' - * amount: number; // e.g., 1 - * transactionId?: string; // Optional: for idempotency - * } - */ -const createUsageEventSchema = z.object({ - usageMeterSlug: z.string().min(1, 'usageMeterSlug is required'), - amount: z - .number() - .int('amount must be an integer') - .positive('amount must be a positive integer'), - transactionId: z.string().optional(), -}); - -export async function POST(request: Request) { - try { - const body = await request.json(); - const parseResult = createUsageEventSchema.safeParse(body); - - if (!parseResult.success) { - return NextResponse.json( - { - error: 'Invalid request body', - details: parseResult.error.issues, - }, - { status: 400 } - ); - } - - const { - usageMeterSlug, - amount: amountNumber, - transactionId, - } = parseResult.data; - - // Generate transaction ID if not provided - const finalTransactionId = - transactionId || - `usage_${Date.now()}_${Math.random().toString(36).substring(7)}`; - - // Get customer ID from session - const session = await auth.api.getSession({ headers: await headers() }); - const userId = session?.user?.id; - if (!userId) { - return NextResponse.json( - { error: 'User not found' }, - { status: 401 } - ); - } - - // Get billing information to extract required IDs - const flowgladServer = flowglad(userId); - const billing = await flowgladServer.getBilling(); - - if (!billing.customer) { - return NextResponse.json( - { error: 'Customer not found' }, - { status: 404 } - ); - } - - // Find the current subscription - // By default, each customer can only have one active subscription at a time, - // so accessing the first currentSubscriptions is sufficient. - // Multiple subscriptions per customer can be enabled in dashboard > settings - const currentSubscription = billing.currentSubscriptions?.[0]; - if (!currentSubscription) { - return NextResponse.json( - { error: 'No active subscription found' }, - { status: 404 } - ); - } - - const subscriptionId = currentSubscription.id; - - const usagePrice = findUsagePriceByMeterSlug( - usageMeterSlug, - billing.pricingModel - ); - - if (!usagePrice) { - return NextResponse.json( - { - error: `Usage price not found for meter: ${usageMeterSlug}. Please ensure a usage price product exists for this meter in your pricing model.`, - }, - { status: 404 } - ); - } - - const priceSlug = usagePrice.slug; - - if (!priceSlug) { - return NextResponse.json( - { - error: `Usage price found but missing priceSlug for meter: ${usageMeterSlug}`, - }, - { status: 500 } - ); - } - - // Create usage event with all required IDs - // Note: customerId is automatically resolved from the session by FlowgladServer - // usageMeterId is automatically resolved from priceSlug by Flowglad - const usageEvent = await flowgladServer.createUsageEvent({ - subscriptionId, - priceSlug, - amount: amountNumber, - transactionId: finalTransactionId, - }); - - return NextResponse.json({ - success: true, - usageEvent, - }); - } catch (error) { - return NextResponse.json( - { - error: - error instanceof Error - ? error.message - : 'Failed to create usage event', - }, - { status: 500 } - ); - } -} diff --git a/nextjs/pay-as-you-go/src/app/home-client.tsx b/nextjs/pay-as-you-go/src/app/home-client.tsx index a31e46a..63d5efb 100644 --- a/nextjs/pay-as-you-go/src/app/home-client.tsx +++ b/nextjs/pay-as-you-go/src/app/home-client.tsx @@ -109,24 +109,21 @@ export function HomeClient() { setMessageInput(null); try { - // Generate a unique transaction ID for idempotency - const transactionId = `message_${Date.now()}_${Math.random().toString(36).substring(7)}`; + if (!billing.createUsageEvent) { + throw new Error('createUsageEvent is not available'); + } - const response = await fetch('/api/usage-events', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - usageMeterSlug: 'message_credits', - amount: 1, - transactionId, - }), + const result = await billing.createUsageEvent({ + usageMeterSlug: 'message_credits', + amount: 1, }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to create usage event'); + if ('error' in result) { + const errorMsg = result.error.json?.error ?? result.error.json?.message; + throw new Error( + (typeof errorMsg === 'string' ? errorMsg : null) || + 'Failed to create usage event' + ); } // Cycle through mock messages diff --git a/nextjs/tiered-usage-gated-subscription/bun.lock b/nextjs/tiered-usage-gated-subscription/bun.lock index 32d6f8d..bf3c806 100644 --- a/nextjs/tiered-usage-gated-subscription/bun.lock +++ b/nextjs/tiered-usage-gated-subscription/bun.lock @@ -1,11 +1,10 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "nextjs-starter", "dependencies": { - "@flowglad/nextjs": "0.15.0", + "@flowglad/nextjs": "0.16.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", @@ -145,15 +144,15 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@flowglad/nextjs": ["@flowglad/nextjs@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/react": "0.15.0", "@flowglad/server": "0.15.0", "@flowglad/shared": "0.15.0", "@vercel/mcp-adapter": "0.11.1", "zod": "4.1.5" }, "peerDependencies": { "next": "^14.0.3 || ^15.0.0 || ^16.0.0", "react": "^19.0.0 || ^18.0.0" } }, "sha512-5CDd1SkH+dwt16QzvLgZVH5ydbO0uu4AXMkZ5hOBGfONmsvGZ6r0bS3dtQQJtHPBkMGZVqwm529JsFHbTD6hdQ=="], + "@flowglad/nextjs": ["@flowglad/nextjs@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/react": "0.16.2", "@flowglad/server": "0.16.2", "@flowglad/shared": "0.16.2", "@vercel/mcp-adapter": "0.11.1", "zod": "4.1.5" }, "peerDependencies": { "better-auth": "^1.3.17", "next": "^14.0.3 || ^15.0.0 || ^16.0.0", "react": "^19.0.0 || ^18.0.0" }, "optionalPeers": ["better-auth"] }, "sha512-FtdSFV3uD4rnjfI5iA3wqawc6weTUTq1afd/RLnzx6RP44qXg0v2cZzVLZuNHiPiWY9SB3qTnmgD1evL8RVYaw=="], "@flowglad/node": ["@flowglad/node@0.24.0", "", {}, "sha512-P5UhUaYNAGuCT9hLTQC34o+az4hsfMSuSGpYzg9bPQIrRnTkWvJfmyOmE+cGSlaGZCdLNnEwxCwDQr+lVeOyRw=="], - "@flowglad/react": ["@flowglad/react@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.15.0", "@tanstack/react-query": "5.66.0", "clsx": "2.1.1", "date-fns": "4.1.0", "tailwind-merge": "3.0.2", "zod": "4.1.5" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0" } }, "sha512-/CkqMI0dpyezn4enQ1F2m0XzoyHZFXRQ0pB3EAcSMj1683/aRgnDaqbqiaT3mt4ieEf3/D8Cxnm4xOs+xfPf0g=="], + "@flowglad/react": ["@flowglad/react@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.2", "@tanstack/react-query": "5.66.0", "clsx": "2.1.1", "date-fns": "4.1.0", "tailwind-merge": "3.0.2", "zod": "4.1.5" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0" } }, "sha512-BiSf2rTeeLDlNpviooPLMwCgEHCwUplO3l7uqey78HyZiTlrnbDMmh6xY4lVVB6CWIvptjbVg9sPissihW/XCQ=="], - "@flowglad/server": ["@flowglad/server@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.15.0", "zod": "4.1.5" } }, "sha512-XyFRnnx0wl8qOsv+c5kj3+pDxVSGAsoC1YivHTCrGPWGXPCHGhGNsH0XgXxV5BjxjjRuYjb57OkV553WUn/eWQ=="], + "@flowglad/server": ["@flowglad/server@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.2", "nanoid": "^3.3.11", "zod": "4.1.5" }, "peerDependencies": { "better-auth": "^1.3.17", "express": "^4.0.0" }, "optionalPeers": ["better-auth", "express"] }, "sha512-C0mKYL4AVCzy8rqB38TpG3oiMrec3CgHG0zgh8UWnkpYW8A8qN0qSLHHkUmN5Oncvzr3YSJZ4MpmBcWpD7xnjA=="], - "@flowglad/shared": ["@flowglad/shared@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "date-fns": "4.1.0", "zod": "4.1.5" } }, "sha512-6Go6C4JSdloYUcfc98FvVosElhgFegz1tRGW0CFuaf6LPAr442hW/f71HUKouRKrTUD1YMhjoZvPJD+QA7neag=="], + "@flowglad/shared": ["@flowglad/shared@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "date-fns": "4.1.0", "zod": "4.1.5" } }, "sha512-kdNJ7IwVeCxtqpO6HldK7s5Cil9K0r4OFU6s/1ShM922EqU98OrLHqTkXeyXCX/7EW8U+XmDrFRxfINI2KYc4g=="], "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], diff --git a/nextjs/tiered-usage-gated-subscription/drizzle/0000_dizzy_micromax.sql b/nextjs/tiered-usage-gated-subscription/drizzle/0000_pale_starhawk.sql similarity index 100% rename from nextjs/tiered-usage-gated-subscription/drizzle/0000_dizzy_micromax.sql rename to nextjs/tiered-usage-gated-subscription/drizzle/0000_pale_starhawk.sql diff --git a/nextjs/tiered-usage-gated-subscription/drizzle/meta/0000_snapshot.json b/nextjs/tiered-usage-gated-subscription/drizzle/meta/0000_snapshot.json index ff8ad97..c5ff660 100644 --- a/nextjs/tiered-usage-gated-subscription/drizzle/meta/0000_snapshot.json +++ b/nextjs/tiered-usage-gated-subscription/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "e1c4a87d-52d4-4112-8d76-80aa1b288c9e", + "id": "2023db0c-250d-40ca-adda-6dc03ddfb85f", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -104,15 +104,7 @@ } }, "compositePrimaryKeys": {}, - "uniqueConstraints": { - "accounts_provider_id_account_id_unique": { - "name": "accounts_provider_id_account_id_unique", - "columns": [ - "provider_id", - "account_id" - ] - } - }, + "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false diff --git a/nextjs/tiered-usage-gated-subscription/drizzle/meta/_journal.json b/nextjs/tiered-usage-gated-subscription/drizzle/meta/_journal.json index d40f988..89f1fd3 100644 --- a/nextjs/tiered-usage-gated-subscription/drizzle/meta/_journal.json +++ b/nextjs/tiered-usage-gated-subscription/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1762366685322, - "tag": "0000_dizzy_micromax", + "when": 1768191510969, + "tag": "0000_pale_starhawk", "breakpoints": true } ] diff --git a/nextjs/tiered-usage-gated-subscription/package.json b/nextjs/tiered-usage-gated-subscription/package.json index 2621d9d..85fcd06 100644 --- a/nextjs/tiered-usage-gated-subscription/package.json +++ b/nextjs/tiered-usage-gated-subscription/package.json @@ -20,7 +20,7 @@ "db:studio": "drizzle-kit studio --config=drizzle.config.ts" }, "dependencies": { - "@flowglad/nextjs": "0.15.0", + "@flowglad/nextjs": "0.16.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", diff --git a/nextjs/tiered-usage-gated-subscription/src/app/api/usage-events/route.ts b/nextjs/tiered-usage-gated-subscription/src/app/api/usage-events/route.ts deleted file mode 100644 index d75b03a..0000000 --- a/nextjs/tiered-usage-gated-subscription/src/app/api/usage-events/route.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { flowglad } from '@/lib/flowglad'; -import { - findUsagePriceBySlug, - findUsageMeterBySlug, -} from '@/lib/billing-helpers'; -import { auth } from '@/lib/auth'; -import { headers } from 'next/headers'; -import { NextResponse } from 'next/server'; -import { z } from 'zod'; - -/** - * POST /api/usage-events - * Creates a usage event for the current customer - * - * Body: { - * priceSlug: string; // e.g., 'plus_o3_overage' or 'pro_o3_tracking' - * usageMeterSlug: string; // e.g., 'o3_messages' - used for validation - * amount: number; // e.g., 1 - * transactionId?: string; // Optional: for idempotency - * } - */ -const createUsageEventSchema = z.object({ - priceSlug: z.string().min(1, 'priceSlug is required'), - usageMeterSlug: z.string().min(1, 'usageMeterSlug is required'), - amount: z - .number() - .int('amount must be an integer') - .positive('amount must be a positive integer'), - transactionId: z.string().optional(), -}); - -export async function POST(request: Request) { - try { - const body = await request.json(); - const parseResult = createUsageEventSchema.safeParse(body); - - if (!parseResult.success) { - return NextResponse.json( - { - error: 'Invalid request body', - details: parseResult.error.issues, - }, - { status: 400 } - ); - } - - const { - priceSlug, - usageMeterSlug, - amount: amountNumber, - transactionId, - } = parseResult.data; - - // Generate transaction ID if not provided - const finalTransactionId = - transactionId || - `usage_${Date.now()}_${Math.random().toString(36).substring(7)}`; - - // Get customer ID from session - const session = await auth.api.getSession({ headers: await headers() }); - const userId = session?.user?.id; - if (!userId) { - return NextResponse.json( - { error: 'User not found' }, - { status: 401 } - ); - } - - // Get billing information to extract required IDs - const flowgladServer = flowglad(userId); - const billing = await flowgladServer.getBilling(); - - if (!billing.customer) { - return NextResponse.json( - { error: 'Customer not found' }, - { status: 404 } - ); - } - - // Find the current subscription - // By default, each customer can only have one active subscription at a time, - // so accessing the first currentSubscriptions is sufficient. - // Multiple subscriptions per customer can be enabled in dashboard > settings - const currentSubscription = billing.currentSubscriptions?.[0]; - if (!currentSubscription) { - return NextResponse.json( - { error: 'No active subscription found' }, - { status: 404 } - ); - } - - const subscriptionId = currentSubscription.id; - - // Find the usage meter by slug for validation - const usageMeter = findUsageMeterBySlug( - usageMeterSlug, - billing.pricingModel - ); - if (!usageMeter) { - return NextResponse.json( - { - error: `Usage meter not found: ${usageMeterSlug}`, - }, - { status: 404 } - ); - } - - // Find the usage price directly by slug - const usagePrice = findUsagePriceBySlug(priceSlug, billing.pricingModel); - - if (!usagePrice) { - return NextResponse.json( - { - error: `Usage price not found: ${priceSlug}`, - }, - { status: 404 } - ); - } - - if (usagePrice.type !== 'usage') { - return NextResponse.json( - { - error: `Price ${priceSlug} is not a usage price`, - }, - { status: 400 } - ); - } - - if (!usagePrice.usageMeterId) { - return NextResponse.json( - { - error: `Price ${priceSlug} does not have a usage meter associated`, - }, - { status: 400 } - ); - } - - // Validate that the price's usage meter matches the provided usage meter slug - if (usagePrice.usageMeterId !== usageMeter.id) { - return NextResponse.json( - { - error: `Price ${priceSlug} is associated with a different usage meter than ${usageMeterSlug}`, - }, - { status: 400 } - ); - } - - // Note: customerId is automatically resolved from the session by FlowgladServer - const usageEvent = await flowgladServer.createUsageEvent({ - subscriptionId, - priceSlug, - amount: amountNumber, - transactionId: finalTransactionId, - }); - - return NextResponse.json({ - success: true, - usageEvent, - }); - } catch (error) { - return NextResponse.json( - { - error: - error instanceof Error - ? error.message - : 'Failed to create usage event', - }, - { status: 500 } - ); - } -} diff --git a/nextjs/tiered-usage-gated-subscription/src/app/home-client.tsx b/nextjs/tiered-usage-gated-subscription/src/app/home-client.tsx index 9b11248..4fc2dcc 100644 --- a/nextjs/tiered-usage-gated-subscription/src/app/home-client.tsx +++ b/nextjs/tiered-usage-gated-subscription/src/app/home-client.tsx @@ -245,25 +245,21 @@ export function HomeClient() { try { // Create usage event if always required OR if model is limited (has usage meter) if (alwaysCreateUsageEvent || !isUnlimited) { - const transactionId = `${transactionIdPrefix}_${Date.now()}_${Math.random().toString(36).substring(7)}`; - const amount = 1; - - const response = await fetch('/api/usage-events', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - priceSlug, - usageMeterSlug, - amount, - transactionId, - }), + if (!billing.createUsageEvent) { + throw new Error('createUsageEvent is not available'); + } + + const result = await billing.createUsageEvent({ + usageMeterSlug, }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to create usage event'); + if ('error' in result) { + const errorMsg = + result.error.json?.error ?? result.error.json?.message; + throw new Error( + (typeof errorMsg === 'string' ? errorMsg : null) || + 'Failed to create usage event' + ); } if (billing.reload) { diff --git a/nextjs/usage-limit-subscription/bun.lock b/nextjs/usage-limit-subscription/bun.lock index 204a3fe..9b2a40e 100644 --- a/nextjs/usage-limit-subscription/bun.lock +++ b/nextjs/usage-limit-subscription/bun.lock @@ -4,7 +4,7 @@ "": { "name": "nextjs-starter", "dependencies": { - "@flowglad/nextjs": "0.15.0", + "@flowglad/nextjs": "0.16.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", @@ -145,15 +145,15 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@flowglad/nextjs": ["@flowglad/nextjs@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/react": "0.15.0", "@flowglad/server": "0.15.0", "@flowglad/shared": "0.15.0", "@vercel/mcp-adapter": "0.11.1", "zod": "4.1.5" }, "peerDependencies": { "next": "^14.0.3 || ^15.0.0 || ^16.0.0", "react": "^19.0.0 || ^18.0.0" } }, "sha512-5CDd1SkH+dwt16QzvLgZVH5ydbO0uu4AXMkZ5hOBGfONmsvGZ6r0bS3dtQQJtHPBkMGZVqwm529JsFHbTD6hdQ=="], + "@flowglad/nextjs": ["@flowglad/nextjs@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/react": "0.16.2", "@flowglad/server": "0.16.2", "@flowglad/shared": "0.16.2", "@vercel/mcp-adapter": "0.11.1", "zod": "4.1.5" }, "peerDependencies": { "better-auth": "^1.3.17", "next": "^14.0.3 || ^15.0.0 || ^16.0.0", "react": "^19.0.0 || ^18.0.0" }, "optionalPeers": ["better-auth"] }, "sha512-FtdSFV3uD4rnjfI5iA3wqawc6weTUTq1afd/RLnzx6RP44qXg0v2cZzVLZuNHiPiWY9SB3qTnmgD1evL8RVYaw=="], "@flowglad/node": ["@flowglad/node@0.24.0", "", {}, "sha512-P5UhUaYNAGuCT9hLTQC34o+az4hsfMSuSGpYzg9bPQIrRnTkWvJfmyOmE+cGSlaGZCdLNnEwxCwDQr+lVeOyRw=="], - "@flowglad/react": ["@flowglad/react@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.15.0", "@tanstack/react-query": "5.66.0", "clsx": "2.1.1", "date-fns": "4.1.0", "tailwind-merge": "3.0.2", "zod": "4.1.5" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0" } }, "sha512-/CkqMI0dpyezn4enQ1F2m0XzoyHZFXRQ0pB3EAcSMj1683/aRgnDaqbqiaT3mt4ieEf3/D8Cxnm4xOs+xfPf0g=="], + "@flowglad/react": ["@flowglad/react@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.2", "@tanstack/react-query": "5.66.0", "clsx": "2.1.1", "date-fns": "4.1.0", "tailwind-merge": "3.0.2", "zod": "4.1.5" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0" } }, "sha512-BiSf2rTeeLDlNpviooPLMwCgEHCwUplO3l7uqey78HyZiTlrnbDMmh6xY4lVVB6CWIvptjbVg9sPissihW/XCQ=="], - "@flowglad/server": ["@flowglad/server@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.15.0", "zod": "4.1.5" } }, "sha512-XyFRnnx0wl8qOsv+c5kj3+pDxVSGAsoC1YivHTCrGPWGXPCHGhGNsH0XgXxV5BjxjjRuYjb57OkV553WUn/eWQ=="], + "@flowglad/server": ["@flowglad/server@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.2", "nanoid": "^3.3.11", "zod": "4.1.5" }, "peerDependencies": { "better-auth": "^1.3.17", "express": "^4.0.0" }, "optionalPeers": ["better-auth", "express"] }, "sha512-C0mKYL4AVCzy8rqB38TpG3oiMrec3CgHG0zgh8UWnkpYW8A8qN0qSLHHkUmN5Oncvzr3YSJZ4MpmBcWpD7xnjA=="], - "@flowglad/shared": ["@flowglad/shared@0.15.0", "", { "dependencies": { "@flowglad/node": "0.24.0", "date-fns": "4.1.0", "zod": "4.1.5" } }, "sha512-6Go6C4JSdloYUcfc98FvVosElhgFegz1tRGW0CFuaf6LPAr442hW/f71HUKouRKrTUD1YMhjoZvPJD+QA7neag=="], + "@flowglad/shared": ["@flowglad/shared@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "date-fns": "4.1.0", "zod": "4.1.5" } }, "sha512-kdNJ7IwVeCxtqpO6HldK7s5Cil9K0r4OFU6s/1ShM922EqU98OrLHqTkXeyXCX/7EW8U+XmDrFRxfINI2KYc4g=="], "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], diff --git a/nextjs/usage-limit-subscription/package.json b/nextjs/usage-limit-subscription/package.json index 10a101e..1a88114 100644 --- a/nextjs/usage-limit-subscription/package.json +++ b/nextjs/usage-limit-subscription/package.json @@ -22,7 +22,7 @@ "unlink:packages": "yalc remove --all && bun install" }, "dependencies": { - "@flowglad/nextjs": "0.15.0", + "@flowglad/nextjs": "0.16.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", diff --git a/nextjs/usage-limit-subscription/src/app/api/usage-events/route.ts b/nextjs/usage-limit-subscription/src/app/api/usage-events/route.ts deleted file mode 100644 index 3f345a3..0000000 --- a/nextjs/usage-limit-subscription/src/app/api/usage-events/route.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { flowglad } from '@/lib/flowglad'; -import { findUsagePriceByMeterSlug } from '@/lib/billing-helpers'; -import { auth } from '@/lib/auth'; -import { headers } from 'next/headers'; -import { NextResponse } from 'next/server'; -import { z } from 'zod'; - -/** - * POST /api/usage-events - * Creates a usage event for the current customer - * - * Body: { - * usageMeterSlug: string; // e.g., 'fast_premium_requests' - * amount: number; // e.g., 1 - * transactionId?: string; // Optional: for idempotency - * } - */ -const createUsageEventSchema = z.object({ - usageMeterSlug: z.string().min(1, 'usageMeterSlug is required'), - amount: z - .number() - .int('amount must be an integer') - .positive('amount must be a positive integer'), - transactionId: z.string().optional(), -}); - -export async function POST(request: Request) { - try { - const body = await request.json(); - const parseResult = createUsageEventSchema.safeParse(body); - - if (!parseResult.success) { - return NextResponse.json( - { - error: 'Invalid request body', - details: parseResult.error.issues, - }, - { status: 400 } - ); - } - - const { - usageMeterSlug, - amount: amountNumber, - transactionId, - } = parseResult.data; - - // Generate transaction ID if not provided - const finalTransactionId = - transactionId || - `usage_${Date.now()}_${Math.random().toString(36).substring(7)}`; - - // Get customer ID from session - const session = await auth.api.getSession({ headers: await headers() }); - const userId = session?.user?.id; - if (!userId) { - return NextResponse.json( - { error: 'User not found' }, - { status: 401 } - ); - } - - // Get billing information to extract required IDs - const flowgladServer = flowglad(userId); - const billing = await flowgladServer.getBilling(); - - if (!billing.customer) { - return NextResponse.json( - { error: 'Customer not found' }, - { status: 404 } - ); - } - - // Find the current subscription - // By default, each customer can only have one active subscription at a time, - // so accessing the first currentSubscriptions is sufficient. - // Multiple subscriptions per customer can be enabled in dashboard > settings - const currentSubscription = billing.currentSubscriptions?.[0]; - if (!currentSubscription) { - return NextResponse.json( - { error: 'No active subscription found' }, - { status: 404 } - ); - } - - const subscriptionId = currentSubscription.id; - - // Find the usage price by searching through the catalog using the meter slug - const usagePrice = findUsagePriceByMeterSlug( - usageMeterSlug, - billing.pricingModel - ); - - if (!usagePrice) { - return NextResponse.json( - { - error: `Usage price not found for meter: ${usageMeterSlug}. Please ensure a usage price product exists for this meter in your pricing model.`, - }, - { status: 404 } - ); - } - - const priceId = usagePrice.id; - const usageMeterId = usagePrice.usageMeterId; - - if (!usageMeterId) { - return NextResponse.json( - { - error: `Usage price found but missing usageMeterId for meter: ${usageMeterSlug}`, - }, - { status: 500 } - ); - } - - // Create usage event with all required IDs - // Note: customerId is automatically resolved from the session by FlowgladServer - const usageEvent = await flowgladServer.createUsageEvent({ - subscriptionId, - priceId, - amount: amountNumber, - transactionId: finalTransactionId, - }); - - return NextResponse.json({ - success: true, - usageEvent, - }); - } catch (error) { - return NextResponse.json( - { - error: - error instanceof Error - ? error.message - : 'Failed to create usage event', - }, - { status: 500 } - ); - } -} diff --git a/nextjs/usage-limit-subscription/src/app/home-client.tsx b/nextjs/usage-limit-subscription/src/app/home-client.tsx index 5e04de5..6435884 100644 --- a/nextjs/usage-limit-subscription/src/app/home-client.tsx +++ b/nextjs/usage-limit-subscription/src/app/home-client.tsx @@ -144,26 +144,19 @@ export function HomeClient() { setRequestError(null); try { - // Generate a unique transaction ID for idempotency - const transactionId = `fast_request_${Date.now()}_${Math.random().toString(36).substring(7)}`; - // Use 1 request per fast premium request - const amount = 1; - - const response = await fetch('/api/usage-events', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - usageMeterSlug: 'fast_premium_requests', - amount, - transactionId, - }), + if (!billing.createUsageEvent) { + throw new Error('createUsageEvent is not available'); + } + const result = await billing.createUsageEvent({ + usageMeterSlug: 'fast_premium_requests', }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to create usage event'); + if ('error' in result) { + const errorMsg = result.error.json?.error ?? result.error.json?.message; + throw new Error( + (typeof errorMsg === 'string' ? errorMsg : null) || + 'Failed to create usage event' + ); } // Reload billing data to update usage balances diff --git a/tanstack-start/generation-based-subscription/bun.lock b/tanstack-start/generation-based-subscription/bun.lock index fdae961..5a71629 100644 --- a/tanstack-start/generation-based-subscription/bun.lock +++ b/tanstack-start/generation-based-subscription/bun.lock @@ -4,9 +4,9 @@ "": { "name": "generation-based-subscription", "dependencies": { - "@flowglad/react": "^0.16.1", - "@flowglad/server": "^0.16.1", - "@flowglad/shared": "^0.16.1", + "@flowglad/react": "^0.16.2", + "@flowglad/server": "^0.16.2", + "@flowglad/shared": "^0.16.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", @@ -229,11 +229,11 @@ "@flowglad/node": ["@flowglad/node@0.24.0", "", {}, "sha512-P5UhUaYNAGuCT9hLTQC34o+az4hsfMSuSGpYzg9bPQIrRnTkWvJfmyOmE+cGSlaGZCdLNnEwxCwDQr+lVeOyRw=="], - "@flowglad/react": ["@flowglad/react@0.16.1", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.1", "@tanstack/react-query": "5.66.0", "clsx": "2.1.1", "date-fns": "4.1.0", "tailwind-merge": "3.0.2", "zod": "4.1.5" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0" } }, "sha512-RmWKdwsB1tFWEH5EWw9WTlkSQx0NzZjySTGg255ABJ5LlZVWPQC3Odpx+500ymCTnE4b05S/cxibX3GwXBarbg=="], + "@flowglad/react": ["@flowglad/react@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.2", "@tanstack/react-query": "5.66.0", "clsx": "2.1.1", "date-fns": "4.1.0", "tailwind-merge": "3.0.2", "zod": "4.1.5" }, "peerDependencies": { "react": "^19.0.0 || ^18.0.0" } }, "sha512-BiSf2rTeeLDlNpviooPLMwCgEHCwUplO3l7uqey78HyZiTlrnbDMmh6xY4lVVB6CWIvptjbVg9sPissihW/XCQ=="], - "@flowglad/server": ["@flowglad/server@0.16.1", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.1", "zod": "4.1.5" }, "peerDependencies": { "better-auth": "^1.3.17", "express": "^4.0.0" }, "optionalPeers": ["better-auth", "express"] }, "sha512-HqGD+R9kcpCpVDSuwc4VtJ6juFrKcJJ+wEmTAHJSkEHRRiO37cxjljlD803Jru6nNdbj78W6oX+7rVJnA1+KuA=="], + "@flowglad/server": ["@flowglad/server@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "@flowglad/shared": "0.16.2", "nanoid": "^3.3.11", "zod": "4.1.5" }, "peerDependencies": { "better-auth": "^1.3.17", "express": "^4.0.0" }, "optionalPeers": ["better-auth", "express"] }, "sha512-C0mKYL4AVCzy8rqB38TpG3oiMrec3CgHG0zgh8UWnkpYW8A8qN0qSLHHkUmN5Oncvzr3YSJZ4MpmBcWpD7xnjA=="], - "@flowglad/shared": ["@flowglad/shared@0.16.1", "", { "dependencies": { "@flowglad/node": "0.24.0", "date-fns": "4.1.0", "zod": "4.1.5" } }, "sha512-3CBRUsys3GUaVvbpyQqEw6kSQmm22seZu1AcTD29G46FAJXRR+31M4al1AGoYI+CWRKxYooq3NdvVsHg8N1yhw=="], + "@flowglad/shared": ["@flowglad/shared@0.16.2", "", { "dependencies": { "@flowglad/node": "0.24.0", "date-fns": "4.1.0", "zod": "4.1.5" } }, "sha512-kdNJ7IwVeCxtqpO6HldK7s5Cil9K0r4OFU6s/1ShM922EqU98OrLHqTkXeyXCX/7EW8U+XmDrFRxfINI2KYc4g=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], diff --git a/tanstack-start/generation-based-subscription/package.json b/tanstack-start/generation-based-subscription/package.json index 4591c9b..54bfc34 100644 --- a/tanstack-start/generation-based-subscription/package.json +++ b/tanstack-start/generation-based-subscription/package.json @@ -17,9 +17,9 @@ "db:studio": "drizzle-kit studio" }, "dependencies": { - "@flowglad/react": "^0.16.1", - "@flowglad/server": "^0.16.1", - "@flowglad/shared": "^0.16.1", + "@flowglad/react": "^0.16.2", + "@flowglad/server": "^0.16.2", + "@flowglad/shared": "^0.16.2", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", diff --git a/tanstack-start/generation-based-subscription/src/routeTree.gen.ts b/tanstack-start/generation-based-subscription/src/routeTree.gen.ts index 075c511..def8988 100644 --- a/tanstack-start/generation-based-subscription/src/routeTree.gen.ts +++ b/tanstack-start/generation-based-subscription/src/routeTree.gen.ts @@ -13,7 +13,6 @@ import { Route as SignUpRouteImport } from './routes/sign-up' import { Route as SignInRouteImport } from './routes/sign-in' import { Route as PricingRouteImport } from './routes/pricing' import { Route as IndexRouteImport } from './routes/index' -import { Route as ApiUsageEventsRouteImport } from './routes/api/usage-events' import { Route as ApiFlowgladSplatRouteImport } from './routes/api/flowglad/$' import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$' @@ -37,11 +36,6 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) -const ApiUsageEventsRoute = ApiUsageEventsRouteImport.update({ - id: '/api/usage-events', - path: '/api/usage-events', - getParentRoute: () => rootRouteImport, -} as any) const ApiFlowgladSplatRoute = ApiFlowgladSplatRouteImport.update({ id: '/api/flowglad/$', path: '/api/flowglad/$', @@ -58,7 +52,6 @@ export interface FileRoutesByFullPath { '/pricing': typeof PricingRoute '/sign-in': typeof SignInRoute '/sign-up': typeof SignUpRoute - '/api/usage-events': typeof ApiUsageEventsRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/flowglad/$': typeof ApiFlowgladSplatRoute } @@ -67,7 +60,6 @@ export interface FileRoutesByTo { '/pricing': typeof PricingRoute '/sign-in': typeof SignInRoute '/sign-up': typeof SignUpRoute - '/api/usage-events': typeof ApiUsageEventsRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/flowglad/$': typeof ApiFlowgladSplatRoute } @@ -77,7 +69,6 @@ export interface FileRoutesById { '/pricing': typeof PricingRoute '/sign-in': typeof SignInRoute '/sign-up': typeof SignUpRoute - '/api/usage-events': typeof ApiUsageEventsRoute '/api/auth/$': typeof ApiAuthSplatRoute '/api/flowglad/$': typeof ApiFlowgladSplatRoute } @@ -88,7 +79,6 @@ export interface FileRouteTypes { | '/pricing' | '/sign-in' | '/sign-up' - | '/api/usage-events' | '/api/auth/$' | '/api/flowglad/$' fileRoutesByTo: FileRoutesByTo @@ -97,7 +87,6 @@ export interface FileRouteTypes { | '/pricing' | '/sign-in' | '/sign-up' - | '/api/usage-events' | '/api/auth/$' | '/api/flowglad/$' id: @@ -106,7 +95,6 @@ export interface FileRouteTypes { | '/pricing' | '/sign-in' | '/sign-up' - | '/api/usage-events' | '/api/auth/$' | '/api/flowglad/$' fileRoutesById: FileRoutesById @@ -116,7 +104,6 @@ export interface RootRouteChildren { PricingRoute: typeof PricingRoute SignInRoute: typeof SignInRoute SignUpRoute: typeof SignUpRoute - ApiUsageEventsRoute: typeof ApiUsageEventsRoute ApiAuthSplatRoute: typeof ApiAuthSplatRoute ApiFlowgladSplatRoute: typeof ApiFlowgladSplatRoute } @@ -151,13 +138,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - '/api/usage-events': { - id: '/api/usage-events' - path: '/api/usage-events' - fullPath: '/api/usage-events' - preLoaderRoute: typeof ApiUsageEventsRouteImport - parentRoute: typeof rootRouteImport - } '/api/flowglad/$': { id: '/api/flowglad/$' path: '/api/flowglad/$' @@ -180,7 +160,6 @@ const rootRouteChildren: RootRouteChildren = { PricingRoute: PricingRoute, SignInRoute: SignInRoute, SignUpRoute: SignUpRoute, - ApiUsageEventsRoute: ApiUsageEventsRoute, ApiAuthSplatRoute: ApiAuthSplatRoute, ApiFlowgladSplatRoute: ApiFlowgladSplatRoute, } diff --git a/tanstack-start/generation-based-subscription/src/routes/api/usage-events.ts b/tanstack-start/generation-based-subscription/src/routes/api/usage-events.ts deleted file mode 100644 index 6755e33..0000000 --- a/tanstack-start/generation-based-subscription/src/routes/api/usage-events.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { z } from 'zod' -import { flowglad, getSessionFromRequest } from '../../lib/flowglad' -import { findUsagePriceByMeterSlug } from '../../lib/billing-helpers' - -/** - * POST /api/usage-events - * Creates a usage event for the current customer - * - * Body: { - * usageMeterSlug: string; // e.g., 'fast_generations' - * amount: number; // e.g., 1 - * transactionId?: string; // Optional: for idempotency - * } - */ -const createUsageEventSchema = z.object({ - usageMeterSlug: z.string().min(1, 'usageMeterSlug is required'), - amount: z - .number() - .int('amount must be an integer') - .positive('amount must be a positive integer'), - transactionId: z.string().optional(), -}) - -export const Route = createFileRoute('/api/usage-events')({ - server: { - handlers: { - POST: async ({ request }) => { - try { - const body = await request.json() - const parseResult = createUsageEventSchema.safeParse(body) - - if (!parseResult.success) { - return Response.json( - { - error: 'Invalid request body', - details: parseResult.error.issues, - }, - { status: 400 }, - ) - } - - const { - usageMeterSlug, - amount: amountNumber, - transactionId, - } = parseResult.data - - // Generate transaction ID if not provided - const finalTransactionId = - transactionId || - `usage_${Date.now()}_${Math.random().toString(36).substring(7)}` - - // Get customer ID from session - const session = await getSessionFromRequest() - const userId = session?.user.id - - if (!userId) { - return Response.json({ error: 'User not found' }, { status: 401 }) - } - - // Get billing information to extract required IDs - const flowgladServer = flowglad(userId) - const billing = await flowgladServer.getBilling() - - if (!billing.customer) { - return Response.json( - { error: 'Customer not found' }, - { status: 404 }, - ) - } - - // Find the current subscription - // By default, each customer can only have one active subscription at a time, - // so accessing the first currentSubscriptions is sufficient. - // Multiple subscriptions per customer can be enabled in dashboard > settings - const currentSubscription = billing.currentSubscriptions?.[0] - if (!currentSubscription) { - return Response.json( - { error: 'No active subscription found' }, - { status: 404 }, - ) - } - - const subscriptionId = currentSubscription.id - - const usagePrice = findUsagePriceByMeterSlug( - usageMeterSlug, - billing.pricingModel, - ) - - if (!usagePrice) { - return Response.json( - { - error: `Usage price not found for meter: ${usageMeterSlug}. Please ensure a usage price product exists for this meter in your pricing model.`, - }, - { status: 404 }, - ) - } - - const priceSlug = usagePrice.slug - - if (!priceSlug) { - return Response.json( - { - error: `Usage price found but missing priceSlug for meter: ${usageMeterSlug}`, - }, - { status: 500 }, - ) - } - - // Create usage event with all required IDs - // Note: customerId is automatically resolved from the session by FlowgladServer - // usageMeterId is automatically resolved from priceSlug by Flowglad - const usageEvent = await flowgladServer.createUsageEvent({ - subscriptionId, - priceSlug, - amount: amountNumber, - transactionId: finalTransactionId, - }) - - return Response.json({ - success: true, - usageEvent, - }) - } catch (error) { - return Response.json( - { - error: - error instanceof Error - ? error.message - : 'Failed to create usage event', - }, - { status: 500 }, - ) - } - }, - }, - }, -}) diff --git a/tanstack-start/generation-based-subscription/src/routes/index.tsx b/tanstack-start/generation-based-subscription/src/routes/index.tsx index 278498a..e077575 100644 --- a/tanstack-start/generation-based-subscription/src/routes/index.tsx +++ b/tanstack-start/generation-based-subscription/src/routes/index.tsx @@ -18,7 +18,7 @@ export const Route = createFileRoute('/')({ component: Dashboard, server: { middleware: [authMiddleware], - } + }, }) // Mock images to cycle through @@ -64,7 +64,7 @@ function Dashboard() { // Refetch billing data when user ID changes to prevent showing previous user's data useEffect(() => { - const currentUserId = session?.user.id + const currentUserId = session?.user?.id // Only refetch if user ID actually changed and billing is loaded if ( currentUserId && @@ -78,7 +78,7 @@ function Dashboard() { // Update ref even if we don't reload (e.g., on initial mount) previousUserIdRef.current = currentUserId } - }, [session?.user.id, billing]) + }, [session?.user?.id, billing]) // Check if user is on free plan and redirect to pricing page useEffect(() => { @@ -175,26 +175,23 @@ function Dashboard() { setGenerateError(null) try { - // Generate a unique transaction ID for idempotency - const transactionId = `fast_image_${Date.now()}_${Math.random().toString(36).substring(7)}` + if (!billing.createUsageEvent) { + throw new Error('createUsageEvent is not available') + } // Random amount between 3-5 const amount = Math.floor(Math.random() * 3) + 3 - const response = await fetch('/api/usage-events', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - usageMeterSlug: 'fast_generations', - amount, - transactionId, - }), + const result = await billing.createUsageEvent({ + usageMeterSlug: 'fast_generations', + amount, }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || 'Failed to create usage event') + if ('error' in result) { + const errorMsg = result.error.json?.error ?? result.error.json?.message + throw new Error( + (typeof errorMsg === 'string' ? errorMsg : null) || + 'Failed to create usage event', + ) } // Cycle through mock images @@ -227,26 +224,20 @@ function Dashboard() { setHdVideoError(null) try { - // Generate a unique transaction ID for idempotency - const transactionId = `hd_video_${Date.now()}_${Math.random().toString(36).substring(7)}` // Random amount between 1-3 minutes const amount = Math.floor(Math.random() * 3) + 1 - const response = await fetch('/api/usage-events', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - usageMeterSlug: 'hd_video_minutes', - amount, - transactionId, - }), + const result = await billing.createUsageEvent({ + usageMeterSlug: 'hd_video_minutes', + amount, }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || 'Failed to create usage event') + if ('error' in result) { + const errorMsg = result.error.json?.error ?? result.error.json?.message + throw new Error( + (typeof errorMsg === 'string' ? errorMsg : null) || + 'Failed to create usage event', + ) } // Cycle through mock video GIFs