-
Notifications
You must be signed in to change notification settings - Fork 1
Pay As You Go Example #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds a complete "pay-as-you-go" Next.js example: project tooling, Drizzle/Postgres schema and client, BetterAuth server/client, Flowglad per-customer wiring, API routes (auth, session, flowglad proxy, usage-events, health), middleware, providers, UI primitives, pages, and utilities. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User (Browser)
participant App as Next.js UI
participant API as /api/usage-events
participant Auth as BetterAuth
participant Flow as Flowglad
participant DB as PostgreSQL (Drizzle)
Note over U,App: User triggers usage (Generate / Top-up)
U->>App: POST usage request (usageMeterSlug, amount, transactionId)
App->>API: POST /api/usage-events
API->>Auth: auth.api.getSession(headers)
Auth->>DB: query session -> user
DB-->>Auth: user record
Auth-->>API: session with userId
API->>Flow: getBilling(customerExternalId = userId)
Flow->>DB: fetch pricing & subscriptions
DB-->>Flow: billing data
Flow-->>API: billing + subscriptions
API->>API: findUsagePriceByMeterSlug -> priceSlug
API->>Flow: createUsageEvent({ subscriptionId, priceSlug, amount, transactionId })
Flow->>DB: persist usage event
DB-->>Flow: confirm
Flow-->>API: usageEvent
API-->>App: 200 { usageEvent }
App->>U: update UI / refresh billing
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 17
🧹 Nitpick comments (20)
pay-as-you-go/tsconfig.json (1)
3-3: Consider a more modern ECMAScript target.The
ES2017target is conservative for a Next.js 15.5 project in 2025. Modern Next.js projects typically target ES2020 or later, which reduces unnecessary transpilation and produces more efficient code.🔎 Suggested change
- "target": "ES2017", + "target": "ES2020",pay-as-you-go/next.config.js (1)
3-5: Consider removing ESLint bypass during builds.Setting
ignoreDuringBuilds: trueallows ESLint errors to slip into production builds. Unless there's a specific reason to bypass linting, it's better to fix linting issues and keep build-time validation enabled.🔎 Suggested change
- eslint: { - ignoreDuringBuilds: true - },pay-as-you-go/.eslintrc.js (2)
37-42: Consider allowing limited type assertions for third-party library interop.The
assertionStyle: 'never'configuration (line 40) completely bans type assertions (asand<>). While type narrowing is preferred, this can be overly restrictive when working with third-party libraries or complex type scenarios where assertions are the only practical solution.🔎 Suggested relaxation
'@typescript-eslint/consistent-type-assertions': [ 'error', { - assertionStyle: 'never' + assertionStyle: 'as', + objectLiteralTypeAssertions: 'never' } ],This allows
asassertions where truly needed while still preventing risky object literal assertions.
71-72: Explicit return types on all functions may impact developer velocity.Requiring explicit return types (line 71) on every function, including simple callbacks and arrow functions, can significantly slow down development. TypeScript's inference is generally reliable for most cases.
🔎 Suggested alternative
Consider using
explicit-module-boundary-typesonly (which you already have on line 72) and removing or downgradingexplicit-function-return-typeto 'warn', or limit it to exported functions:- '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'error',This maintains type safety at module boundaries while allowing inference for internal functions.
pay-as-you-go/src/app/page.tsx (1)
3-5: Remove unnecessary async keyword.The
Homefunction is declaredasyncbut doesn't perform any asynchronous operations. While Next.js 15 App Router supports async Server Components for data fetching, theasynckeyword here is misleading since noawaitis used.🔎 Proposed fix
-export default async function Home() { +export default function Home() { return <HomeClient />; }pay-as-you-go/.env.example (1)
25-27: Replace empty quoted string with a placeholder value.The
NEXT_PUBLIC_CREDIT_TOPUP_PRICE_IDis set to an empty quoted string"", which could cause confusion. Environment variable examples typically use descriptive placeholder values to guide users.🔎 Suggested improvement
# the price id of the Message Topup product, # can be found in the Flowglad dashboard -NEXT_PUBLIC_CREDIT_TOPUP_PRICE_ID="" +NEXT_PUBLIC_CREDIT_TOPUP_PRICE_ID=price_abc123_your_topup_price_id +This change:
- Provides a clearer placeholder value indicating the expected format
- Adds a blank line at the end per dotenv best practices
pay-as-you-go/drizzle/meta/0000_snapshot.json (1)
110-110: Consider enabling Row-Level Security (RLS) for production.All tables have
isRLSEnabled: false. For a production application with user data, consider enabling PostgreSQL Row-Level Security to enforce data access policies at the database level, providing defense-in-depth against unauthorized access.This is particularly important for:
userstable: Users should only access their own datasessionstable: Sessions should be restricted to their owneraccountstable: Account data should be user-scopedverificationstable: Verification tokens should be protectedAlso applies to: 193-193, 259-259, 310-310
pay-as-you-go/src/app/sign-up/page.tsx (2)
36-41: Addrequiredattribute to the name input for consistency.The email and password inputs have
requiredattributes, but the name input does not. This inconsistency could allow form submission with an empty name, which may cause server-side validation errors or unexpected behavior.🔎 Proposed fix
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" className="w-full rounded border px-3 py-2" + required />
17-30: Consider wrapping the async call in try/finally for robustness.The current pattern relies on
authClient.signUp.emailcallbacks to handle all outcomes. If an unexpected error occurs that bypasses theonErrorcallback, the loading state would still be cleared but no error would be displayed.🔎 Proposed fix
async function onSubmit(e: React.FormEvent) { e.preventDefault(); setLoading(true); setError(null); - await authClient.signUp.email( - { name, email, password, callbackURL: '/' }, - { - onError: (ctx) => setError(ctx.error.message), - onSuccess: () => router.push('/'), - onRequest: () => {}, - } - ); - setLoading(false); + try { + await authClient.signUp.email( + { name, email, password, callbackURL: '/' }, + { + onError: (ctx) => setError(ctx.error.message), + onSuccess: () => router.push('/'), + onRequest: () => {}, + } + ); + } catch (err) { + setError(err instanceof Error ? err.message : 'An unexpected error occurred'); + } finally { + setLoading(false); + } }pay-as-you-go/src/components/navbar.tsx (2)
39-48: Consider usinginvalidateQueriesinstead ofclear.
queryClient.clear()removes all cached queries, which may include data from unrelated parts of the application. If the intent is only to invalidate session-related queries, useinvalidateQuerieswith a specific query key filter instead.🔎 Alternative approach
async function handleSignOut() { - await queryClient.clear(); + await queryClient.invalidateQueries(); // or use specific query keys await authClient.signOut({ fetchOptions: { onSuccess: () => { router.push('/sign-in'); }, }, }); }
111-120: Hardcoded locale may not match user preferences.The date formatting uses
'en-US'locale. Consider usingundefinedto respect the user's browser locale, or leverage a localization strategy if i18n is planned.🔎 Proposed change
const cancellationDate = currentSubscription?.cancelScheduledAt ? new Date(currentSubscription.cancelScheduledAt).toLocaleDateString( - 'en-US', + undefined, // Use browser's default locale { year: 'numeric', month: 'long', day: 'numeric', } ) : null;pay-as-you-go/src/app/sign-in/page.tsx (1)
35-50: Addautocompleteattributes for better UX and password manager support.Adding
autocompleteattributes helps password managers and improves accessibility.🔎 Proposed enhancement
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" className="w-full rounded border px-3 py-2" required + autoComplete="email" /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" className="w-full rounded border px-3 py-2" required + autoComplete="current-password" />pay-as-you-go/drizzle/0000_clammy_ulik.sql (1)
1-50: Consider adding indexes on foreign key columns for query performance.The
accounts.user_idandsessions.user_idcolumns have foreign key constraints but no indexes. PostgreSQL doesn't automatically create indexes on foreign key columns, which can impact performance on JOIN queries and CASCADE deletes.🔎 Suggested index additions
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "accounts_user_id_idx" ON "accounts" ("user_id");--> statement-breakpoint +CREATE INDEX "sessions_user_id_idx" ON "sessions" ("user_id");pay-as-you-go/drizzle.config.ts (1)
11-13: Consider failing explicitly whenDATABASE_URLis missing.The empty string fallback
process.env.DATABASE_URL ?? ''will cause cryptic connection errors during migrations. An explicit check would provide clearer feedback.🔎 Suggested improvement
+const databaseUrl = process.env.DATABASE_URL; +if (!databaseUrl) { + throw new Error('DATABASE_URL environment variable is required'); +} + export default defineConfig({ out: './drizzle', schema: './src/server/db/schema.ts', dialect: 'postgresql', dbCredentials: { - url: process.env.DATABASE_URL ?? '', + url: databaseUrl, }, });pay-as-you-go/src/lib/billing-helpers.ts (3)
24-24: Use strict equality operators.Using loose equality (
==) is not idiomatic in TypeScript. Prefer strict equality (===) for predictable comparisons.- if (!pricingModel?.usageMeters || !purchases || purchases.length == 0) + if (!pricingModel?.usageMeters || !purchases || purchases.length === 0)
32-35: Use strict equality and extract magic number to a constant.Two concerns here:
- Use
!==instead of!=for consistent strict equality.- The hardcoded
100(credits per purchase) should be a named constant for clarity and maintainability.🔎 Proposed fix
+const CREDITS_PER_TOPUP = 100; + export function computeMessageUsageTotal( ... - if (purchase.priceId != topUpPriceId || purchase.status != 'paid') + if (purchase.priceId !== topUpPriceId || purchase.status !== 'paid') continue; - total += purchase.quantity * 100; + total += purchase.quantity * CREDITS_PER_TOPUP;
39-41: Silent error swallowing may hide bugs.Catching all errors and returning
0silently can mask genuine issues. Consider logging the error in development or re-throwing unexpected errors to aid debugging.pay-as-you-go/src/components/ui/carousel.tsx (1)
78-89: Keyboard navigation doesn't adapt to vertical orientation.For vertical carousels, users would expect ArrowUp/ArrowDown keys rather than ArrowLeft/ArrowRight. Consider adapting keyboard handling based on orientation.
🔎 Proposed fix
const handleKeyDown = React.useCallback( (event: React.KeyboardEvent<HTMLDivElement>) => { - if (event.key === 'ArrowLeft') { + const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'; + const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'; + if (event.key === prevKey) { event.preventDefault(); scrollPrev(); - } else if (event.key === 'ArrowRight') { + } else if (event.key === nextKey) { event.preventDefault(); scrollNext(); } }, - [scrollPrev, scrollNext] + [orientation, scrollPrev, scrollNext] );pay-as-you-go/src/server/db/schema.ts (1)
17-28: Consider adding indexes for frequently queried columns.The
sessionsandaccountstables referenceuserIdas a foreign key, andsessions.tokenis likely used for lookups. Adding indexes on these columns can improve query performance as the data grows.// Example using Drizzle indexes (can be added to table definitions or separately) import { index } from 'drizzle-orm/pg-core'; // On sessions table: // index('sessions_user_id_idx').on(sessions.userId) // index('sessions_token_idx').on(sessions.token) // On accounts table: // index('accounts_user_id_idx').on(accounts.userId)Also applies to: 30-50
pay-as-you-go/src/app/api/usage-events/route.ts (1)
127-137: Consider sanitizing error messages in production.Returning
error.messagedirectly may expose internal implementation details. For a production system, consider logging the full error server-side and returning a generic message to clients.🔎 Proposed fix
} catch (error) { + console.error('Usage event creation failed:', error); return NextResponse.json( { - error: - error instanceof Error - ? error.message - : 'Failed to create usage event', + error: 'Failed to create usage event', }, { status: 500 } ); }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (14)
pay-as-you-go/bun.lockis excluded by!**/*.lockpay-as-you-go/public/file.svgis excluded by!**/*.svgpay-as-you-go/public/globe.svgis excluded by!**/*.svgpay-as-you-go/public/images/flowglad.pngis excluded by!**/*.pngpay-as-you-go/public/images/mock-video-3.gifis excluded by!**/*.gifpay-as-you-go/public/images/unsplash-1.jpgis excluded by!**/*.jpgpay-as-you-go/public/images/unsplash-2.jpgis excluded by!**/*.jpgpay-as-you-go/public/images/unsplash-3.jpgis excluded by!**/*.jpgpay-as-you-go/public/images/unsplash-4.jpgis excluded by!**/*.jpgpay-as-you-go/public/images/unsplash-5.jpgis excluded by!**/*.jpgpay-as-you-go/public/next.svgis excluded by!**/*.svgpay-as-you-go/public/vercel.svgis excluded by!**/*.svgpay-as-you-go/public/window.svgis excluded by!**/*.svgpay-as-you-go/src/app/icon.icois excluded by!**/*.ico
📒 Files selected for processing (53)
pay-as-you-go/.env.example(1 hunks)pay-as-you-go/.eslintrc.js(1 hunks)pay-as-you-go/.gitignore(1 hunks)pay-as-you-go/.prettierrc(1 hunks)pay-as-you-go/LICENSE(1 hunks)pay-as-you-go/README.md(1 hunks)pay-as-you-go/bunfig.toml(1 hunks)pay-as-you-go/components.json(1 hunks)pay-as-you-go/drizzle.config.ts(1 hunks)pay-as-you-go/drizzle/0000_clammy_ulik.sql(1 hunks)pay-as-you-go/drizzle/meta/0000_snapshot.json(1 hunks)pay-as-you-go/drizzle/meta/_journal.json(1 hunks)pay-as-you-go/eslint.config.mjs(1 hunks)pay-as-you-go/next-env.d.ts(1 hunks)pay-as-you-go/next.config.js(1 hunks)pay-as-you-go/package.json(1 hunks)pay-as-you-go/postcss.config.mjs(1 hunks)pay-as-you-go/pricing.yaml(1 hunks)pay-as-you-go/src/app/api/auth/[...all]/route.ts(1 hunks)pay-as-you-go/src/app/api/auth/session/route.ts(1 hunks)pay-as-you-go/src/app/api/flowglad/[...path]/route.ts(1 hunks)pay-as-you-go/src/app/api/health/route.ts(1 hunks)pay-as-you-go/src/app/api/usage-events/route.ts(1 hunks)pay-as-you-go/src/app/globals.css(1 hunks)pay-as-you-go/src/app/home-client.tsx(1 hunks)pay-as-you-go/src/app/layout.tsx(1 hunks)pay-as-you-go/src/app/page.tsx(1 hunks)pay-as-you-go/src/app/sign-in/page.tsx(1 hunks)pay-as-you-go/src/app/sign-up/page.tsx(1 hunks)pay-as-you-go/src/components/dashboard-skeleton.tsx(1 hunks)pay-as-you-go/src/components/navbar.tsx(1 hunks)pay-as-you-go/src/components/providers.tsx(1 hunks)pay-as-you-go/src/components/ui/badge.tsx(1 hunks)pay-as-you-go/src/components/ui/button.tsx(1 hunks)pay-as-you-go/src/components/ui/card.tsx(1 hunks)pay-as-you-go/src/components/ui/carousel.tsx(1 hunks)pay-as-you-go/src/components/ui/dropdown-menu.tsx(1 hunks)pay-as-you-go/src/components/ui/input.tsx(1 hunks)pay-as-you-go/src/components/ui/progress.tsx(1 hunks)pay-as-you-go/src/components/ui/scroll-area.tsx(1 hunks)pay-as-you-go/src/components/ui/skeleton.tsx(1 hunks)pay-as-you-go/src/components/ui/switch.tsx(1 hunks)pay-as-you-go/src/components/ui/tooltip.tsx(1 hunks)pay-as-you-go/src/hooks/use-mobile.ts(1 hunks)pay-as-you-go/src/lib/auth-client.ts(1 hunks)pay-as-you-go/src/lib/auth.ts(1 hunks)pay-as-you-go/src/lib/billing-helpers.ts(1 hunks)pay-as-you-go/src/lib/flowglad.ts(1 hunks)pay-as-you-go/src/lib/utils.ts(1 hunks)pay-as-you-go/src/middleware.ts(1 hunks)pay-as-you-go/src/server/db/client.ts(1 hunks)pay-as-you-go/src/server/db/schema.ts(1 hunks)pay-as-you-go/tsconfig.json(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (25)
pay-as-you-go/src/app/api/auth/session/route.ts (2)
pay-as-you-go/src/app/api/health/route.ts (1)
GET(3-5)pay-as-you-go/src/lib/auth.ts (1)
auth(12-27)
pay-as-you-go/src/components/ui/skeleton.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
pay-as-you-go/src/app/sign-up/page.tsx (1)
pay-as-you-go/src/lib/auth-client.ts (1)
authClient(5-8)
pay-as-you-go/src/components/ui/input.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
pay-as-you-go/src/app/api/auth/[...all]/route.ts (1)
pay-as-you-go/src/lib/auth.ts (1)
auth(12-27)
pay-as-you-go/src/app/home-client.tsx (4)
pay-as-you-go/src/lib/auth-client.ts (1)
authClient(5-8)pay-as-you-go/src/components/dashboard-skeleton.tsx (1)
DashboardSkeleton(9-67)pay-as-you-go/src/lib/billing-helpers.ts (1)
computeMessageUsageTotal(18-42)pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
pay-as-you-go/src/components/dashboard-skeleton.tsx (2)
pay-as-you-go/src/components/ui/card.tsx (4)
Card(85-85)CardHeader(86-86)CardTitle(88-88)CardContent(91-91)pay-as-you-go/src/components/ui/skeleton.tsx (1)
Skeleton(13-13)
pay-as-you-go/src/app/api/usage-events/route.ts (3)
pay-as-you-go/src/lib/auth.ts (1)
auth(12-27)pay-as-you-go/src/lib/flowglad.ts (1)
flowglad(5-23)pay-as-you-go/src/lib/billing-helpers.ts (1)
findUsagePriceByMeterSlug(51-74)
pay-as-you-go/src/components/ui/button.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
pay-as-you-go/src/app/api/flowglad/[...path]/route.ts (1)
pay-as-you-go/src/lib/auth.ts (1)
auth(12-27)
pay-as-you-go/src/lib/flowglad.ts (1)
pay-as-you-go/src/lib/auth.ts (1)
auth(12-27)
pay-as-you-go/src/components/ui/progress.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
pay-as-you-go/src/app/sign-in/page.tsx (2)
pay-as-you-go/src/lib/auth-client.ts (1)
authClient(5-8)pay-as-you-go/src/components/ui/button.tsx (1)
Button(61-61)
pay-as-you-go/src/app/layout.tsx (2)
pay-as-you-go/src/components/providers.tsx (2)
ReactQueryProvider(28-36)FlowgladProviderWrapper(38-51)pay-as-you-go/src/components/navbar.tsx (1)
Navbar(23-173)
pay-as-you-go/src/app/page.tsx (1)
pay-as-you-go/src/app/home-client.tsx (1)
HomeClient(26-359)
pay-as-you-go/src/middleware.ts (1)
pay-as-you-go/src/lib/auth.ts (1)
auth(12-27)
pay-as-you-go/src/lib/auth.ts (2)
pay-as-you-go/src/server/db/client.ts (1)
db(24-24)pay-as-you-go/src/server/db/schema.ts (1)
betterAuthSchema(62-67)
pay-as-you-go/src/components/providers.tsx (1)
pay-as-you-go/src/lib/auth-client.ts (1)
authClient(5-8)
pay-as-you-go/src/components/ui/badge.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
pay-as-you-go/src/components/ui/tooltip.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
pay-as-you-go/src/app/api/health/route.ts (1)
pay-as-you-go/src/app/api/auth/session/route.ts (1)
GET(4-15)
pay-as-you-go/src/components/ui/card.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
pay-as-you-go/src/components/ui/switch.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
pay-as-you-go/src/components/ui/carousel.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
pay-as-you-go/src/components/ui/dropdown-menu.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)
🪛 dotenv-linter (4.0.0)
pay-as-you-go/.env.example
[warning] 27-27: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
[warning] 27-27: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
🪛 markdownlint-cli2 (0.18.1)
pay-as-you-go/README.md
103-103: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (46)
pay-as-you-go/LICENSE (1)
1-22: MIT License is appropriate for this example project.The license is complete, correctly formatted, and properly attributes copyright to Flowglad Inc. This is a standard choice for open-source examples.
pay-as-you-go/bunfig.toml (1)
1-2: LGTM!The Bun configuration appropriately disables automatic installation, ensuring explicit control over dependency management.
pay-as-you-go/.prettierrc (1)
1-8: LGTM!The Prettier configuration follows standard conventions and provides consistent formatting rules for the project.
pay-as-you-go/next-env.d.ts (1)
1-6: LGTM!This is a standard Next.js type declaration file that should not be manually edited. The reference to typed routes aligns with Next.js 15.5 capabilities.
pay-as-you-go/postcss.config.mjs (1)
1-7: LGTM!The PostCSS configuration correctly integrates the modern Tailwind CSS plugin.
pay-as-you-go/tsconfig.json (1)
11-14: LGTM!Excellent strict type-checking flags that enhance type safety, including
exactOptionalPropertyTypes,noUncheckedIndexedAccess, andnoImplicitOverride.pay-as-you-go/next.config.js (1)
6-13: LGTM!The image optimization configuration appropriately restricts remote sources to
media.giphy.com, providing security through explicit allowlisting.pay-as-you-go/.gitignore (1)
1-98: LGTM! Comprehensive ignore patterns.The .gitignore covers all necessary patterns including dependencies, build outputs, environment files, IDE artifacts, OS files, logs, and caches. The explicit enforcement of Bun as the package manager (lines 95-98) is clearly documented.
pay-as-you-go/src/app/globals.css (1)
1-122: LGTM! Modern theming setup with oklch color space.The CSS configuration properly integrates Tailwind with custom CSS variables for theming. The use of oklch color space (lines 48-113) provides better perceptual uniformity across light and dark modes. The custom dark mode variant (line 4) and theme inline block (lines 6-44) are well-structured.
pay-as-you-go/.eslintrc.js (1)
1-144: Overall: Strict but maintainable ESLint configuration.The configuration enforces high code quality standards with comprehensive TypeScript rules, import ordering, and consistency requirements. While some rules are quite strict (assertions ban, explicit return types), they reflect a deliberate choice for maximum type safety. The test file overrides (lines 134-143) appropriately relax rules where needed.
pay-as-you-go/src/app/api/health/route.ts (1)
1-5: LGTM! Clean health check endpoint.The health check route is correctly implemented following Next.js 15 App Router conventions. The synchronous GET handler appropriately returns a simple status payload.
pay-as-you-go/drizzle/meta/_journal.json (1)
1-13: LGTM! Standard Drizzle migration journal.This is an auto-generated Drizzle Kit journal file recording the initial migration (tag "0000_clammy_ulik"). The structure is correct for Drizzle version 7 with PostgreSQL dialect.
pay-as-you-go/src/app/api/auth/session/route.ts (1)
4-15: Session endpoint implementation is functional.The route correctly uses BetterAuth's session API and follows Next.js App Router conventions. With the addition of error logging (see previous comment), this implementation will be production-ready.
pay-as-you-go/src/lib/utils.ts (1)
4-6: LGTM!The
cnutility correctly combinesclsxfor conditional class composition withtwMergefor Tailwind CSS class deduplication. This is the standard pattern used by shadcn/ui components.pay-as-you-go/pricing.yaml (2)
1-11: Verify the planisDefaultflag.The plan-level
isDefaultis set tofalse(line 1), while the Free Plan product is marked asdefault: true(line 17). In a pay-as-you-go model, users typically start on the default plan automatically. Confirm whetherisDefault: falseat the plan level is intentional or if it should betrueto auto-enroll new users.
51-71: Verify pricing calculation matches intended rate.The Message Topup product has a
unitPrice: 10000(=$100.00 in cents) and grants 100 message credits via the100_messagesfeature. This results in $1.00 per credit, which matches the PR description. However, ensure this pricing aligns with your business model and that theNEXT_PUBLIC_CREDIT_TOPUP_PRICE_IDenvironment variable references this price's actual ID from your Flowglad dashboard.pay-as-you-go/src/components/ui/skeleton.tsx (1)
3-11: LGTM!The Skeleton component follows best practices with proper prop spreading, className merging via
cn, and uses thedata-slotattribute for potential styling hooks. The implementation is clean and reusable.pay-as-you-go/drizzle/meta/0000_snapshot.json (1)
71-76: Verify password hashing in application code.The
accountstable includes apasswordcolumn stored astext. Ensure that passwords are properly hashed (e.g., using bcrypt, argon2, or scrypt) before being stored in this column. Storing plaintext passwords would be a critical security vulnerability.Since this is a schema snapshot and the actual hashing should occur in application code, verify that BetterAuth or your authentication logic properly hashes passwords before database insertion.
pay-as-you-go/src/app/api/auth/[...all]/route.ts (1)
1-4: LGTM!The authentication route properly uses BetterAuth's Next.js adapter to expose GET and POST handlers. This follows the recommended pattern for integrating BetterAuth with Next.js App Router.
pay-as-you-go/components.json (1)
1-22: LGTM!The shadcn/ui configuration is properly set up with:
- React Server Components enabled (
rsc: true)- TypeScript support (
tsx: true)- CSS variables for theming (
cssVariables: true)- Clear path aliases matching project structure
- Lucide icons integration
This configuration supports the UI component library introduced in this PR.
pay-as-you-go/src/lib/auth-client.ts (1)
5-8: LGTM!The auth client configuration correctly uses the
NEXT_PUBLIC_BASE_URLenvironment variable with an empty string fallback. The empty string default enables relative URLs, allowing the application to work across different environments (localhost, staging, production) without hardcoding the base URL.pay-as-you-go/src/server/db/client.ts (1)
1-24: LGTM!The database client setup follows the standard pattern for Next.js applications with Drizzle ORM. The global singleton pattern correctly prevents connection pool exhaustion during hot module replacement in development, while production instances create fresh pools as expected. The fail-fast validation of
DATABASE_URLis appropriate.pay-as-you-go/src/components/ui/button.tsx (1)
1-61: LGTM!This is a well-implemented Button component following the shadcn/ui pattern. The CVA configuration provides comprehensive variant and size options, and the
asChildprop correctly enables composition via Radix Slot. The TypeScript typing is accurate.pay-as-you-go/src/components/ui/progress.tsx (1)
1-32: LGTM!The Progress component correctly wraps the Radix UI primitive with appropriate styling and transform-based animation. The fallback
value || 0properly handles undefined/null cases.pay-as-you-go/src/app/layout.tsx (1)
26-41: LGTM!The root layout correctly composes the provider hierarchy with ReactQueryProvider wrapping FlowgladProviderWrapper, which aligns with dependency requirements. The font configuration and metadata export follow Next.js 15 conventions.
Minor note: The
asynckeyword on line 26 is currently unused but harmless.pay-as-you-go/src/components/dashboard-skeleton.tsx (1)
9-66: LGTM!The skeleton structure effectively mirrors the dashboard layout with appropriate placeholders for usage meters, credit top-ups, content area, and input section. The comments documenting each section are helpful for maintainability.
pay-as-you-go/src/components/ui/input.tsx (1)
1-21: LGTM!The Input component follows the shadcn/ui pattern with comprehensive styling for various states (focus, disabled, aria-invalid, file inputs, dark mode). The implementation is clean and properly typed.
pay-as-you-go/src/components/navbar.tsx (1)
122-171: Good implementation of the navigation dropdown with billing integration.The component correctly handles multiple states (loading, error, cancellation pending) and provides appropriate user feedback with tooltips and disabled states.
pay-as-you-go/src/app/sign-in/page.tsx (1)
16-29: Form submission logic is correct.The handler properly prevents default, manages loading state, and handles both success and error paths. The
setLoading(false)at line 28 executes regardless of outcome, which is correct since the auth callbacks run synchronously within the await.pay-as-you-go/src/app/api/flowglad/[...path]/route.ts (1)
6-17: Route handler correctly integrates Flowglad with BetterAuth session.The implementation properly retrieves the authenticated user ID from the session and passes it to Flowglad. The error handling at line 14 will result in an appropriate failure response for unauthenticated requests.
Note: The
reqparameter ingetCustomerExternalIdappears unused. If it's not required by the type signature, consider removing it or prefixing with underscore (_req) to indicate intentional non-use.pay-as-you-go/src/components/providers.tsx (2)
7-26: Query client setup follows recommended TanStack Query patterns.The SSR-aware singleton pattern and 30-second
staleTimedefault are appropriate for a Next.js application to avoid refetching immediately on client hydration.
38-51: Good reactive session integration with Flowglad.Using
authClient.useSession()ensuresloadBillingupdates reactively when the session state changes, which handles cases where the session becomes available after initial render.pay-as-you-go/src/lib/auth.ts (1)
7-27: LGTM!The BetterAuth configuration is well-structured:
- Proper environment variable validation at module load
- Clean Drizzle adapter integration with explicit schema mapping
- The
@ts-expect-errorcomment appropriately documents the type incompatibility withexactOptionalPropertyTypespay-as-you-go/src/components/ui/switch.tsx (1)
8-34: LGTM!Well-implemented Switch component following shadcn/ui conventions with proper Radix UI integration, ref forwarding, and accessibility support via the underlying primitive.
pay-as-you-go/src/lib/billing-helpers.ts (1)
51-73: LGTM!The
findUsagePriceByMeterSlugfunction is well-structured with proper null checks and clean logic for mapping meter slugs to prices.pay-as-you-go/src/components/ui/scroll-area.tsx (1)
1-58: LGTM!Clean ScrollArea implementation following shadcn/ui patterns with proper Radix primitive wrapping, orientation support, and consistent styling conventions.
pay-as-you-go/src/components/ui/carousel.tsx (2)
91-106: LGTM!Proper effect cleanup for event listeners prevents memory leaks. The
setApieffect and event subscription patterns are correctly implemented.
175-233: LGTM!Navigation buttons have proper accessibility with
sr-onlytext, disabled states, and clean positioning logic for both orientations.pay-as-you-go/src/components/ui/badge.tsx (1)
1-46: LGTM!Well-structured Badge component following shadcn/ui conventions with proper variant support, asChild pattern, and accessibility styling.
pay-as-you-go/src/server/db/schema.ts (1)
3-15: LGTM!The
userstable schema is well-defined with proper constraints, unique email, and consistent timestamp handling with timezone support.pay-as-you-go/src/components/ui/tooltip.tsx (1)
1-61: LGTM!Well-structured Radix UI tooltip wrapper following the shadcn/ui pattern. The components correctly forward props, use data-slot attributes for styling hooks, and implement appropriate animation classes for open/close states.
pay-as-you-go/src/app/api/usage-events/route.ts (1)
42-121: LGTM!The billing flow is well-structured with proper null checks at each step. Good use of early returns with appropriate HTTP status codes, and the comments explaining the single subscription assumption are helpful.
pay-as-you-go/src/components/ui/card.tsx (1)
1-92: LGTM!Clean implementation of the Card component library following shadcn/ui conventions. Components properly forward props, use data-slot attributes for styling hooks, and compose classNames correctly via the
cnutility.pay-as-you-go/src/app/home-client.tsx (2)
288-312: LGTM on message rendering.Using array indices as keys is acceptable here since messages are append-only and not reordered. The auto-scroll anchor pattern is clean.
107-110: Empty string input would bypass validation.
messageInputisstring | null, so an empty string""is truthy and passes the!messageInputcheck, potentially allowing submission of empty messages.🔎 Proposed fix
const handleGenerateMessage = async () => { - if (!messageInput || messageGenerationsRemaining === 0) { + if (!messageInput?.trim() || messageGenerationsRemaining === 0) { return; }Likely an incorrect or invalid review comment.
pay-as-you-go/src/components/ui/dropdown-menu.tsx (1)
1-257: LGTM!Comprehensive Radix UI dropdown menu wrapper implementation following shadcn/ui conventions. Components properly handle variants (default/destructive), inset options, and include all necessary sub-menu support with appropriate icons.
| CREATE TABLE "verifications" ( | ||
| "id" text PRIMARY KEY NOT NULL, | ||
| "identifier" text NOT NULL, | ||
| "value" text NOT NULL, | ||
| "expires_at" timestamp with time zone NOT NULL, | ||
| "created_at" timestamp with time zone DEFAULT now(), | ||
| "updated_at" timestamp with time zone DEFAULT now() | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent nullability for created_at/updated_at in verifications table.
The verifications table allows NULL for created_at and updated_at (lines 45-46), while all other tables enforce NOT NULL on these timestamp columns. This inconsistency may cause issues if code assumes these fields are always present.
🔎 Suggested fix
CREATE TABLE "verifications" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
- "created_at" timestamp with time zone DEFAULT now(),
- "updated_at" timestamp with time zone DEFAULT now()
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CREATE TABLE "verifications" ( | |
| "id" text PRIMARY KEY NOT NULL, | |
| "identifier" text NOT NULL, | |
| "value" text NOT NULL, | |
| "expires_at" timestamp with time zone NOT NULL, | |
| "created_at" timestamp with time zone DEFAULT now(), | |
| "updated_at" timestamp with time zone DEFAULT now() | |
| ); | |
| CREATE TABLE "verifications" ( | |
| "id" text PRIMARY KEY NOT NULL, | |
| "identifier" text NOT NULL, | |
| "value" text NOT NULL, | |
| "expires_at" timestamp with time zone NOT NULL, | |
| "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| "updated_at" timestamp with time zone DEFAULT now() NOT NULL | |
| ); |
🤖 Prompt for AI Agents
In pay-as-you-go/drizzle/0000_clammy_ulik.sql around lines 40 to 47, the
created_at and updated_at columns are missing NOT NULL unlike other tables;
update their definitions to DEFAULT now() NOT NULL so both timestamps are
non-nullable and consistent with the rest of the schema, ensuring any migrations
or table-creation SQL are updated accordingly.
| import { defineConfig, globalIgnores } from "eslint/config"; | ||
| import nextVitals from "eslint-config-next/core-web-vitals"; | ||
| import nextTs from "eslint-config-next/typescript"; | ||
| import prettier from "eslint-config-prettier"; | ||
|
|
||
| const eslintConfig = defineConfig([ | ||
| ...nextVitals, | ||
| ...nextTs, | ||
| prettier, | ||
| // Override default ignores of eslint-config-next. | ||
| globalIgnores([ | ||
| // Default ignores of eslint-config-next: | ||
| ".next/**", | ||
| "out/**", | ||
| "build/**", | ||
| "next-env.d.ts", | ||
| ]), | ||
| ]); | ||
|
|
||
| export default eslintConfig; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Conflicting ESLint configurations - strict rules not being applied.
The project has both eslint.config.mjs (flat config) and .eslintrc.js (legacy config). ESLint v9+ prioritizes flat config and will ignore the .eslintrc.js file entirely. This means all the strict TypeScript rules, import ordering, and code quality checks defined in .eslintrc.js are currently not being enforced.
The flat config here only includes Next.js presets and Prettier, lacking the comprehensive TypeScript strictness from .eslintrc.js.
🔎 Resolution options
Option 1: Remove flat config and use legacy config
# Delete this file to use .eslintrc.js
rm eslint.config.mjsOption 2: Migrate all rules to flat config (recommended for future compatibility)
Convert the rules from .eslintrc.js into this flat config format. This requires significant work but aligns with ESLint's future direction.
Option 3: Use ESLINT_USE_FLAT_CONFIG=false
Set the environment variable to force legacy config, but this is a temporary workaround.
| "engines": { | ||
| "node": ">=18.18.0" | ||
| }, | ||
| "preinstall": "bunx --bun only-allow bun", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preinstall should be inside scripts object.
The preinstall key at the top level has no effect. Package lifecycle hooks must be defined inside the scripts object to execute.
🔎 Proposed fix
"engines": {
"node": ">=18.18.0"
},
- "preinstall": "bunx --bun only-allow bun",
"scripts": {
+ "preinstall": "bunx --bun only-allow bun",
"dev": "next dev",🤖 Prompt for AI Agents
In pay-as-you-go/package.json around line 10, the "preinstall" key is at the top
level and therefore not executed; move the "preinstall": "bunx --bun only-allow
bun" entry into the "scripts" object (creating "scripts" if absent) so it
becomes a proper lifecycle hook, and remove the top-level "preinstall" key after
relocating it.
| "dependencies": { | ||
| "@flowglad/nextjs": "0.15.0", | ||
| "@radix-ui/react-dropdown-menu": "^2.1.16", | ||
| "@radix-ui/react-progress": "^1.1.8", | ||
| "@radix-ui/react-scroll-area": "^1.2.10", | ||
| "@radix-ui/react-slot": "^1.2.3", | ||
| "@radix-ui/react-switch": "^1.2.6", | ||
| "@radix-ui/react-tooltip": "^1.2.8", | ||
| "@tanstack/react-query": "^5.90.5", | ||
| "better-auth": "^1.3.32", | ||
| "class-variance-authority": "^0.7.1", | ||
| "clsx": "^2.1.1", | ||
| "drizzle-orm": "^0.44.7", | ||
| "embla-carousel-autoplay": "^8.6.0", | ||
| "embla-carousel-react": "^8.6.0", | ||
| "lucide-react": "^0.548.0", | ||
| "next": "15.5.6", | ||
| "pg": "^8.16.3", | ||
| "react": "19.2.0", | ||
| "react-dom": "19.2.0", | ||
| "tailwind-merge": "^3.3.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@tailwindcss/postcss": "^4", | ||
| "@types/node": "^20", | ||
| "@types/pg": "^8.15.5", | ||
| "@types/react": "^19", | ||
| "@types/react-dom": "^19", | ||
| "@typescript-eslint/eslint-plugin": "^8.0.0", | ||
| "@typescript-eslint/parser": "^8.0.0", | ||
| "dotenv": "^17.2.3", | ||
| "drizzle-kit": "^0.31.5", | ||
| "eslint": "^9", | ||
| "eslint-config-next": "15.5.6", | ||
| "eslint-config-prettier": "^9.1.0", | ||
| "eslint-import-resolver-typescript": "^3.6.1", | ||
| "eslint-plugin-import": "^2.29.1", | ||
| "eslint-plugin-prettier": "^5.1.3", | ||
| "eslint-plugin-unused-imports": "^4.0.0", | ||
| "prettier": "^3.2.5", | ||
| "tailwindcss": "^4", | ||
| "tw-animate-css": "^1.4.0", | ||
| "typescript": "^5" | ||
| }, | ||
| "overrides": { | ||
| "esbuild": "0.18.20" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The esbuild override to 0.18.20 introduces a known vulnerability and should be removed.
drizzle-kit 0.31.5 depends on esbuild ^0.19.7, making the override to 0.18.20 a downgrade. More critically, esbuild versions before 0.25.0 contain a known vulnerability (GHSA-67mh-4wv8-2f99), and drizzle-kit 0.31.4/0.31.5 depends on @esbuild-kit/esm-loader which has a nested dependency on vulnerable esbuild versions. Rather than protecting against compatibility issues, this override exposes the project to CVEs. Remove the override and rely on drizzle-kit's version resolution, or upgrade drizzle-kit to a future version that removes the deprecated @esbuild-kit/esm-loader dependency.
🤖 Prompt for AI Agents
In pay-as-you-go/package.json lines 22-68 the "overrides" forcing esbuild to
0.18.20 introduces a known vulnerability and downgrades esbuild below what
drizzle-kit expects; remove the "overrides" entry for esbuild (or alternatively
upgrade drizzle-kit to a version that no longer depends on
@esbuild-kit/esm-loader) and then reinstall dependencies (npm/yarn install) and
run an audit/lockfile update to verify esbuild resolves to a non-vulnerable
>=0.25.0 version.
| export function useMobile(breakpoint: number = 768): boolean { | ||
| const [isMobile, setIsMobile] = useState<boolean>(false); | ||
|
|
||
| useEffect(() => { | ||
| const checkMobile = () => { | ||
| setIsMobile(window.innerWidth < breakpoint); | ||
| }; | ||
|
|
||
| // Check on mount | ||
| checkMobile(); | ||
|
|
||
| // Add event listener | ||
| window.addEventListener('resize', checkMobile); | ||
|
|
||
| // Cleanup | ||
| return () => { | ||
| window.removeEventListener('resize', checkMobile); | ||
| }; | ||
| }, [breakpoint]); | ||
|
|
||
| return isMobile; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SSR hydration mismatch risk.
The hook initializes isMobile to false and updates it in useEffect, which only runs on the client. This creates a hydration mismatch when the server renders false but the client needs true, potentially causing React warnings and layout shifts on initial load.
Additionally, the resize listener fires on every resize event without debouncing, which can impact performance during rapid window resizing.
🔎 Proposed improvements
1. Suppress hydration warning for this known client-only value:
export function useMobile(breakpoint: number = 768): boolean {
- const [isMobile, setIsMobile] = useState<boolean>(false);
+ const [isMobile, setIsMobile] = useState<boolean>(() => {
+ // Return false during SSR to match initial client render
+ if (typeof window === 'undefined') return false;
+ return window.innerWidth < breakpoint;
+ });
useEffect(() => {
+ // Suppress hydration warning - this value is inherently client-side
const checkMobile = () => {
setIsMobile(window.innerWidth < breakpoint);
};
- // Check on mount
- checkMobile();
+ // Initial check (will match useState initial value on client)
+ checkMobile();
// Add event listener
window.addEventListener('resize', checkMobile);2. Add debouncing for better performance:
+'use client';
+
+import { useEffect, useState } from 'react';
+
+// Simple debounce helper
+function debounce<T extends (...args: any[]) => any>(
+ func: T,
+ wait: number
+): (...args: Parameters<T>) => void {
+ let timeout: NodeJS.Timeout;
+ return (...args: Parameters<T>) => {
+ clearTimeout(timeout);
+ timeout = setTimeout(() => func(...args), wait);
+ };
+}
+
export function useMobile(breakpoint: number = 768): boolean {
const [isMobile, setIsMobile] = useState<boolean>(() => {
if (typeof window === 'undefined') return false;
return window.innerWidth < breakpoint;
});
useEffect(() => {
- const checkMobile = () => {
+ const checkMobile = debounce(() => {
setIsMobile(window.innerWidth < breakpoint);
- };
+ }, 150);
checkMobile();
window.addEventListener('resize', checkMobile);| export const flowglad = (customerExternalId: string) => { | ||
| return new FlowgladServer({ | ||
| customerExternalId, | ||
| getCustomerDetails: async (customerExternalId: string) => { | ||
| const session = await auth.api.getSession({ | ||
| headers: await headers(), | ||
| }); | ||
|
|
||
| if (!session?.user) { | ||
| throw new Error('User not authenticated'); | ||
| } | ||
|
|
||
| return { | ||
| email: session.user.email || '', | ||
| name: session.user.name || '', | ||
| }; | ||
| }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactor to use the customerExternalId parameter in the callback
The getCustomerDetails callback receives customerExternalId as a parameter but doesn't use it. The FlowgladServer API expects this parameter to be used to fetch customer details from your database. Replace the session-based lookup with a database query using the provided ID, following the documented integration pattern.
🤖 Prompt for AI Agents
In pay-as-you-go/src/lib/flowglad.ts around lines 5 to 23, the
getCustomerDetails callback ignores the passed customerExternalId and instead
fetches the session; change it to query your customer database using the
provided customerExternalId (e.g., call your repository/ORM function like
getCustomerByExternalId(customerExternalId)), await the result, throw a clear
error if not found, and return the customer's email and name from that DB
record; remove the session-based lookup and ensure proper error handling and
typing for the database call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
pay-as-you-go/README.md (1)
106-106: Add language specifier to fenced code block.The code fence should specify a language for proper syntax highlighting and markdown linting compliance.
🔎 Suggested fix
-``` +```text ├── src/
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
pay-as-you-go/README.md(1 hunks)
🧰 Additional context used
🪛 LanguageTool
pay-as-you-go/README.md
[grammar] ~72-~72: Ensure spelling is correct
Context: ...OPUP_PRICE_ID`** - The price id of your topup product - This can be found in the Fl...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🪛 markdownlint-cli2 (0.18.1)
pay-as-you-go/README.md
106-106: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (1)
pay-as-you-go/README.md (1)
1-135: Overall documentation quality is strong; major issues from prior review have been resolved.The pricing model description (line 4) and directory name reference (line 49) have been corrected to accurately reflect the pay-as-you-go billing example. The documentation is comprehensive, clearly structured, and provides step-by-step setup instructions. Environment variable descriptions, tech stack, and project structure are well-documented and align with the PR objectives.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
pay-as-you-go/src/lib/billing-helpers.ts (1)
27-27: Environment variable validation needed (previously flagged).The non-null assertion on
NEXT_PUBLIC_CREDIT_TOPUP_PRICE_IDwas already flagged in a previous review. If this env var is undefined, the comparison on line 32 will fail silently, causing incorrect calculations.
🧹 Nitpick comments (4)
pay-as-you-go/.env.example (1)
25-27: Polish comment capitalization and add trailing blank line.The comment for the Flowglad price ID should be capitalized to match the style of other comments in the file. Additionally, the file is missing a trailing blank line.
🔎 Proposed fix
# the price id of the Message Topup product, # can be found in the Flowglad dashboard NEXT_PUBLIC_CREDIT_TOPUP_PRICE_ID=price_your_price_id_here +Also apply:
- # the price id of the Message Topup product, + # The price ID of the Message Topup product,pay-as-you-go/src/lib/billing-helpers.ts (3)
32-32: Prefer strict equality operators.Use
!==instead of!=for type-safe comparisons. Loose equality (!=) can lead to unexpected type coercion.🔎 Proposed fix
- if (purchase.priceId != topUpPriceId || purchase.status != 'paid') + if (purchase.priceId !== topUpPriceId || purchase.status !== 'paid')
39-40: Consider logging errors before swallowing them.The empty catch block silently suppresses all errors, making debugging difficult. Consider logging the error (at minimum) before returning the fallback value.
🔎 Proposed enhancement
- } catch { + } catch (error) { + console.error('Error computing message usage total:', error); return 0; }
58-60: Optional: Remove redundant type annotations.The explicit type annotations
meter: UsageMeter,product: Product, andprice: Priceare unnecessary since TypeScript infers these types frompricingModel.usageMetersandpricingModel.products. Removing them reduces noise.🔎 Proposed simplification
const meterIdBySlug = new Map( - pricingModel.usageMeters.map((meter: UsageMeter) => [meter.slug, meter.id]) + pricingModel.usageMeters.map((meter) => [meter.slug, meter.id]) ); // Find price by meter ID const usagePrice = pricingModel.products - .flatMap((product: Product) => product.prices ?? []) + .flatMap((product) => product.prices ?? []) .find( - (price: Price) => + (price) => price.type === 'usage' && price.usageMeterId === usageMeterId );Also applies to: 66-71
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
pay-as-you-go/.env.example(1 hunks)pay-as-you-go/src/lib/billing-helpers.ts(1 hunks)
🧰 Additional context used
🪛 dotenv-linter (4.0.0)
pay-as-you-go/.env.example
[warning] 27-27: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
🔇 Additional comments (1)
pay-as-you-go/src/lib/billing-helpers.ts (1)
51-74: Well-structured usage price lookup.The function correctly maps usage meter slugs to IDs and searches for matching usage prices. The defensive null checks and optional chaining are appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (4)
pay-as-you-go/README.md (1)
103-120: Add language specifier to fenced code block.The code block starting at line 103 should include a language identifier for proper syntax highlighting and markdown linting compliance.
🔎 Proposed fix
-``` +```text ├── src/pay-as-you-go/src/components/dashboard-skeleton.tsx (1)
1-1: Remove the extra semicolon.There's a duplicate semicolon after the
'use client'directive.pay-as-you-go/src/app/home-client.tsx (2)
44-59: Potential infinite loop frombillingobject in dependency array.The
billingobject fromuseBilling()may be a new reference on each render, causing this effect to run repeatedly. Consider extracting stable primitives or using a ref to track the previous state.
229-249: MissingTooltipContentfor the purchase button tooltip.The
Tooltipwrapper around the purchase button has noTooltipContent, so hovering won't display any tooltip text. Either add content or remove the tooltip wrapper.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
pay-as-you-go/.env.example(1 hunks)pay-as-you-go/README.md(1 hunks)pay-as-you-go/src/app/home-client.tsx(1 hunks)pay-as-you-go/src/components/dashboard-skeleton.tsx(1 hunks)pay-as-you-go/src/lib/billing-helpers.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- pay-as-you-go/src/lib/billing-helpers.ts
🧰 Additional context used
🧬 Code graph analysis (1)
pay-as-you-go/src/components/dashboard-skeleton.tsx (2)
pay-as-you-go/src/components/ui/card.tsx (4)
Card(85-85)CardHeader(86-86)CardTitle(88-88)CardContent(91-91)pay-as-you-go/src/components/ui/skeleton.tsx (1)
Skeleton(13-13)
🪛 dotenv-linter (4.0.0)
pay-as-you-go/.env.example
[warning] 23-23: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
🪛 markdownlint-cli2 (0.18.1)
pay-as-you-go/README.md
103-103: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
pay-as-you-go/src/app/home-client.tsx (2)
41-56: [Duplicate] Potential infinite loop frombillingobject in dependency array.This issue was previously flagged and remains unresolved. The
billingobject fromuseBilling()may be a new reference on each render, causing this effect to run repeatedly and trigger unnecessary API calls.Based on learnings, the past review comment suggested destructuring stable primitives (
loaded,reload) from the billing object and using those in the dependency array instead.
226-246: [Duplicate] MissingTooltipContentfor the purchase button tooltip.This issue was previously flagged and remains unresolved. The
Tooltipwrapper around the purchase button (lines 226-246) has noTooltipContentchild, so hovering won't display any tooltip text.Based on learnings, the past review comment suggested either adding
TooltipContentwith appropriate text or removing the unused tooltip wrapper entirely.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
pay-as-you-go/src/app/home-client.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
pay-as-you-go/src/app/home-client.tsx (8)
pay-as-you-go/src/lib/auth-client.ts (1)
authClient(5-8)pay-as-you-go/src/components/dashboard-skeleton.tsx (1)
DashboardSkeleton(9-66)pay-as-you-go/src/components/ui/card.tsx (4)
Card(85-85)CardHeader(86-86)CardTitle(88-88)CardContent(91-91)pay-as-you-go/src/components/ui/tooltip.tsx (3)
Tooltip(61-61)TooltipTrigger(61-61)TooltipContent(61-61)pay-as-you-go/src/components/ui/button.tsx (1)
Button(61-61)pay-as-you-go/src/components/ui/scroll-area.tsx (1)
ScrollArea(58-58)pay-as-you-go/src/lib/utils.ts (1)
cn(4-6)pay-as-you-go/src/components/ui/input.tsx (1)
Input(21-21)
| const handleGenerateMessage = async () => { | ||
| if (!messageInput || messageGenerationsRemaining === 0) { | ||
| return; | ||
| } | ||
|
|
||
| setIsGenerating(true); | ||
| setGenerateError(null); | ||
|
|
||
| const msgs = messages; | ||
|
|
||
| setMessages([ | ||
| ...msgs, | ||
| { | ||
| type: 'user', | ||
| content: messageInput || '', | ||
| }, | ||
| ]); | ||
|
|
||
| setMessageInput(null); | ||
|
|
||
| try { | ||
| // Generate a unique transaction ID for idempotency | ||
| const transactionId = `message_${Date.now()}_${Math.random().toString(36).substring(7)}`; | ||
|
|
||
| const response = await fetch('/api/usage-events', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify({ | ||
| usageMeterSlug: 'message_credits', | ||
| amount: 1, | ||
| transactionId, | ||
| }), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorData = await response.json(); | ||
| throw new Error(errorData.error || 'Failed to create usage event'); | ||
| } | ||
|
|
||
| // Cycle through mock messages | ||
| const nextIndex = (currentMessageIndex + 1) % mockMessages.length; | ||
| setCurrentMessageIndex(nextIndex); | ||
| const nextMessage = mockMessages[nextIndex]; | ||
| if (nextMessage) { | ||
| // add user message again and add response to the chat history (avoids double state update problem) | ||
| setMessages([ | ||
| ...msgs, | ||
| { | ||
| type: 'user', | ||
| content: messageInput, | ||
| }, | ||
| { | ||
| type: 'assistant', | ||
| content: nextMessage, | ||
| }, | ||
| ]); | ||
| } | ||
|
|
||
| // Reload billing data to update usage balances | ||
| await billing.reload(); | ||
| } catch (error) { | ||
| setMessages(msgs); | ||
| setGenerateError( | ||
| error instanceof Error | ||
| ? error.message | ||
| : 'Failed to generate message. Please try again.' | ||
| ); | ||
| } finally { | ||
| setIsGenerating(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical bug: user message content becomes null in final state update.
On Line 106, messageInput is set to null to clear the input field. However, Line 139 still references messageInput when constructing the final chat state, resulting in the user message having null content in the displayed chat history.
🔎 Proposed fix
Save the message content in a local variable before clearing the input:
const handleGenerateMessage = async () => {
if (!messageInput || messageGenerationsRemaining === 0) {
return;
}
setIsGenerating(true);
setGenerateError(null);
const msgs = messages;
+ const userMessageContent = messageInput;
setMessages([
...msgs,
{
type: 'user',
- content: messageInput || '',
+ content: userMessageContent,
},
]);
setMessageInput(null);
try {
// Generate a unique transaction ID for idempotency
const transactionId = `message_${Date.now()}_${Math.random().toString(36).substring(7)}`;
const response = await fetch('/api/usage-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
usageMeterSlug: 'message_credits',
amount: 1,
transactionId,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to create usage event');
}
// Cycle through mock messages
const nextIndex = (currentMessageIndex + 1) % mockMessages.length;
setCurrentMessageIndex(nextIndex);
const nextMessage = mockMessages[nextIndex];
if (nextMessage) {
// add user message again and add response to the chat history (avoids double state update problem)
setMessages([
...msgs,
{
type: 'user',
- content: messageInput,
+ content: userMessageContent,
},
{
type: 'assistant',
content: nextMessage,
},
]);
}
// Reload billing data to update usage balances
await billing.reload();
} catch (error) {
setMessages(msgs);
setGenerateError(
error instanceof Error
? error.message
: 'Failed to generate message. Please try again.'
);
} finally {
setIsGenerating(false);
}
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleGenerateMessage = async () => { | |
| if (!messageInput || messageGenerationsRemaining === 0) { | |
| return; | |
| } | |
| setIsGenerating(true); | |
| setGenerateError(null); | |
| const msgs = messages; | |
| setMessages([ | |
| ...msgs, | |
| { | |
| type: 'user', | |
| content: messageInput || '', | |
| }, | |
| ]); | |
| setMessageInput(null); | |
| try { | |
| // Generate a unique transaction ID for idempotency | |
| const transactionId = `message_${Date.now()}_${Math.random().toString(36).substring(7)}`; | |
| const response = await fetch('/api/usage-events', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| usageMeterSlug: 'message_credits', | |
| amount: 1, | |
| transactionId, | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Failed to create usage event'); | |
| } | |
| // Cycle through mock messages | |
| const nextIndex = (currentMessageIndex + 1) % mockMessages.length; | |
| setCurrentMessageIndex(nextIndex); | |
| const nextMessage = mockMessages[nextIndex]; | |
| if (nextMessage) { | |
| // add user message again and add response to the chat history (avoids double state update problem) | |
| setMessages([ | |
| ...msgs, | |
| { | |
| type: 'user', | |
| content: messageInput, | |
| }, | |
| { | |
| type: 'assistant', | |
| content: nextMessage, | |
| }, | |
| ]); | |
| } | |
| // Reload billing data to update usage balances | |
| await billing.reload(); | |
| } catch (error) { | |
| setMessages(msgs); | |
| setGenerateError( | |
| error instanceof Error | |
| ? error.message | |
| : 'Failed to generate message. Please try again.' | |
| ); | |
| } finally { | |
| setIsGenerating(false); | |
| } | |
| }; | |
| const handleGenerateMessage = async () => { | |
| if (!messageInput || messageGenerationsRemaining === 0) { | |
| return; | |
| } | |
| setIsGenerating(true); | |
| setGenerateError(null); | |
| const msgs = messages; | |
| const userMessageContent = messageInput; | |
| setMessages([ | |
| ...msgs, | |
| { | |
| type: 'user', | |
| content: userMessageContent, | |
| }, | |
| ]); | |
| setMessageInput(null); | |
| try { | |
| // Generate a unique transaction ID for idempotency | |
| const transactionId = `message_${Date.now()}_${Math.random().toString(36).substring(7)}`; | |
| const response = await fetch('/api/usage-events', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| usageMeterSlug: 'message_credits', | |
| amount: 1, | |
| transactionId, | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Failed to create usage event'); | |
| } | |
| // Cycle through mock messages | |
| const nextIndex = (currentMessageIndex + 1) % mockMessages.length; | |
| setCurrentMessageIndex(nextIndex); | |
| const nextMessage = mockMessages[nextIndex]; | |
| if (nextMessage) { | |
| // add user message again and add response to the chat history (avoids double state update problem) | |
| setMessages([ | |
| ...msgs, | |
| { | |
| type: 'user', | |
| content: userMessageContent, | |
| }, | |
| { | |
| type: 'assistant', | |
| content: nextMessage, | |
| }, | |
| ]); | |
| } | |
| // Reload billing data to update usage balances | |
| await billing.reload(); | |
| } catch (error) { | |
| setMessages(msgs); | |
| setGenerateError( | |
| error instanceof Error | |
| ? error.message | |
| : 'Failed to generate message. Please try again.' | |
| ); | |
| } finally { | |
| setIsGenerating(false); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In pay-as-you-go/src/app/home-client.tsx around lines 88 to 160, the handler
clears messageInput (line ~106) before the final state update so the user
message becomes null; fix by capturing the current message text into a local
const (e.g., const userMessage = messageInput || '') before calling
setMessageInput(null), then use that local variable when building the new
messages arrays (both the interim and final setMessages calls) so the user
message content is preserved; ensure all places that reference messageInput in
this function use the saved local variable and keep error handling that restores
msgs unchanged.
agreea
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please rework some of this to get it closer in terms of patterns and robustness as we have with generation-based-subscription
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
pay-as-you-go/src/app/home-client.tsx (2)
39-55: Potential infinite loop frombillingobject in dependency array.The
billingobject fromuseBilling()may be a new reference on each render, causing this effect to run repeatedly. Consider extracting stable primitives (billing.loaded,billing.reload) into separate variables before the effect and using those in the dependency array.
226-243: MissingTooltipContentfor the purchase button tooltip.The
Tooltipwrapper around the purchase button has noTooltipContent, so hovering won't display any tooltip text. Either add content or remove the tooltip wrapper.
🧹 Nitpick comments (1)
pay-as-you-go/src/app/home-client.tsx (1)
137-144: Clarify misleading comment.The comment states "add user message again" but the code only adds the assistant message. The user message was already added on lines 101-107 and remains in the state.
🔎 Proposed fix
- // add user message again and add response to the chat history (avoids double state update problem) + // Add assistant response to the chat history setMessages((msgs) => [ ...msgs, { type: 'assistant', content: nextMessage, }, ]);
|
@agreea I cleaned up the hacky double state thing and made some of the usage balance variables similar to how they are in the generation-based example. I initially removed everything related to subscriptions (pricing page, checking if a user has the right plan, progress bar, etc.) from this since the free/default "plan" is a singular payment product in this example and not treated by Flowglad as a "subscription". I can work around this and add some of these things back but just wanted to clarify what you meant by patterns and robustness in this case since this example differs from the others in terms of the subscription/single product classification. |
This example shows a utilization of Flowglad in a pay-as-you-go scenario. The use case in this example is a chat bot that uses one credit per message generated. Users can buy 100 of these credits at a time for $100 ($1 each).
Users start on the free/default plan with the ability to buy these 100 credit topups.
Some assumptions I made:
Here's what the main chat looks like:
Summary by CodeRabbit
New Features
UI
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.