Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client';
import { usePlans, useSubscription, useStatements } from '@clerk/nextjs/experimental';

export default function Home() {
const { data: plans, count: planCount } = usePlans();
const { data: subscription } = useSubscription();
const { data: statements, count: statementCount } = useStatements();
Comment on lines +4 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add explicit return type and handle loading/error states.

The component definition has several concerns:

  1. Missing explicit return type: Per coding guidelines, functions should have explicit return types.
  2. No loading or error state handling: The experimental hooks likely provide isLoading and error properties that should be handled to improve UX.

Apply this diff to add return type and basic state handling:

-export default function Home() {
+export default function Home(): JSX.Element {
-  const { data: plans, count: planCount } = usePlans();
-  const { data: subscription } = useSubscription();
-  const { data: statements, count: statementCount } = useStatements();
+  const { data: plans, count: planCount, isLoading: plansLoading, error: plansError } = usePlans();
+  const { data: subscription, isLoading: subscriptionLoading, error: subscriptionError } = useSubscription();
+  const { data: statements, count: statementCount, isLoading: statementsLoading, error: statementsError } = useStatements();
+
+  if (plansLoading || subscriptionLoading || statementsLoading) {
+    return <main><p>Loading billing information...</p></main>;
+  }
+
+  if (plansError || subscriptionError || statementsError) {
+    return <main><p>Error loading billing information. Please try again.</p></main>;
+  }
+
   return (

Note: Verify that the experimental hooks actually expose isLoading and error properties before applying this change.


🏁 Script executed:

#!/bin/bash
# Description: Check the experimental hooks API to confirm they expose loading and error states

# Search for the hook type definitions or implementations
rg -nP --type=ts -A 10 'export (function|const) (usePlans|useSubscription|useStatements)'

Length of output: 9370


🏁 Script executed:

#!/bin/bash
# Search for createBillingPaginatedHook definition and return type

rg -nP --type=ts -B 5 -A 15 'function createBillingPaginatedHook|type createBillingPaginatedHook' 

Length of output: 2054


🏁 Script executed:

#!/bin/bash
# Search for SubscriptionResult type definition

rg -nP --type=ts 'type SubscriptionResult|interface SubscriptionResult' -A 10

Length of output: 870


🏁 Script executed:

#!/bin/bash
# Check what properties are actually destructured from these hooks in the codebase

rg -nP --type=tsx 'usePlans\(\)|useSubscription\(\)|useStatements\(\)' -A 5 -B 2 | head -100

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for PaginatedResources type definition

rg -nP --type=ts 'type PaginatedResources|interface PaginatedResources' -A 15

Length of output: 2331


🏁 Script executed:

#!/bin/bash
# Get the actual file under review

cat integration/templates/next-app-router/src/app/billing/hooks/page.tsx

Length of output: 1027


🏁 Script executed:

#!/bin/bash
# Check how these hooks are actually used throughout the codebase with correct file type

rg -nP --type=tsx,ts 'const.*=\s*(usePlans|useSubscription|useStatements)\(' -A 3 | head -150

Length of output: 30


Add explicit return type and handle loading/error states from hooks.

The component is missing two improvements:

  1. Missing explicit return type: Per coding guidelines, all functions should have explicit return types. Add JSX.Element.
  2. No loading/error state handling: All three hooks expose isLoading and error properties that should be handled for better UX. The component currently renders potentially undefined data during loading and ignores errors.

The hooks expose these properties via their return types:

  • usePlans() and useStatements() return PaginatedResources<T> with error, isLoading, isFetching
  • useSubscription() returns SubscriptionResult with error, isLoading, isFetching
🤖 Prompt for AI Agents
In integration/templates/next-app-router/src/app/billing/hooks/page.tsx around
lines 4 to 7, the Home component lacks an explicit return type and does not
handle loading/error states from usePlans, useSubscription, and useStatements;
update the function signature to return JSX.Element and add simple guards that
check each hook's isLoading and error values (e.g., show a loading indicator
while any isLoading is true, and render an error message if any error exists),
and ensure the main render uses only defined plans, subscription, and statements
(fallback to empty arrays or null checks) so undefined data is not rendered
during fetches.

return (
<main>
{plans?.map(plan => (
<div key={plan.id}>
<h2>Plan: {plan.name}</h2>
<p>{plan.description}</p>
</div>
))}

{planCount > 0 ? <p>Plans found</p> : <p>No plans found</p>}

{statements?.map(statement => (
<div key={statement.id}>
<p>Statement total: {statement.totals.grandTotal.amountFormatted}</p>
</div>
))}

{statementCount > 0 ? <p>Statements found</p> : <p>No statements found</p>}

{subscription ? (
<div>
<h2>Subscribed to {subscription.subscriptionItems[0].plan.name}</h2>
</div>
) : (
<p>No subscription found</p>
)}
Comment on lines +27 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unsafe array access will cause runtime error.

Line 29 directly accesses subscription.subscriptionItems[0] without verifying:

  1. subscriptionItems exists
  2. subscriptionItems is an array
  3. The array has at least one element

This will throw a runtime error if subscriptionItems is undefined, null, or an empty array.

Apply this diff to add proper bounds checking:

       {subscription ? (
         <div>
-          <h2>Subscribed to {subscription.subscriptionItems[0].plan.name}</h2>
+          <h2>Subscribed to {subscription.subscriptionItems?.[0]?.plan?.name || 'Unknown Plan'}</h2>
         </div>
       ) : (
📝 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.

Suggested change
{subscription ? (
<div>
<h2>Subscribed to {subscription.subscriptionItems[0].plan.name}</h2>
</div>
) : (
<p>No subscription found</p>
)}
{subscription ? (
<div>
<h2>Subscribed to {subscription.subscriptionItems?.[0]?.plan?.name || 'Unknown Plan'}</h2>
</div>
) : (
<p>No subscription found</p>
)}
🤖 Prompt for AI Agents
In integration/templates/next-app-router/src/app/billing/hooks/page.tsx around
lines 27 to 33, the code directly accesses
subscription.subscriptionItems[0].plan.name which can throw if subscriptionItems
is missing or empty; update the conditional/render to first verify
subscription.subscriptionItems is an array with length > 0 (or use optional
chaining and Array.isArray) before accessing [0], and render a safe fallback
(e.g., "No subscription items" or the generic "No subscription found") when the
check fails so no runtime error occurs.

</main>
);
}
86 changes: 86 additions & 0 deletions integration/tests/billing-hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { expect, test } from '@playwright/test';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('billing hooks @billing', ({ app }) => {
test.describe.configure({ mode: 'parallel' });
test.skip(!app.name.includes('next'), 'Skipping: Only runs on next');

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test.describe('when signed out', () => {
test('renders billing hooks with plans, but no statements and no subscription', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.page.goToRelative('/billing/hooks');

await u.po.page.waitForClerkJsLoaded();

await expect(u.po.page.getByText('Plans found')).toBeVisible();
await expect(u.po.page.getByRole('heading', { name: 'Plan: Pro' })).toBeVisible();
await expect(u.po.page.getByText('No statements found')).toBeVisible();
await expect(u.po.page.getByText('No subscription found')).toBeVisible();
});
});

test.describe('when signed in', () => {
test.describe.configure({ mode: 'serial' });
test('subscribes to a plan', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.page.goToRelative('/pricing-table?newSubscriptionRedirectUrl=/pricing-table');
await u.po.pricingTable.waitForMounted();
await u.po.pricingTable.startCheckout({ planSlug: 'plus' });
await u.po.checkout.waitForMounted();
await u.po.checkout.fillTestCard();
await u.po.checkout.clickPayOrSubscribe();
await expect(u.po.checkout.root.getByText('Payment was successful!')).toBeVisible();
await u.po.checkout.confirmAndContinue();
});

test('renders billing hooks with plans, statements and subscription', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.page.goToRelative('/billing/hooks');

await u.po.page.waitForClerkJsLoaded();

await expect(u.po.page.getByText('Plans found')).toBeVisible();
await expect(u.po.page.getByRole('heading', { name: 'Plan: Pro' })).toBeVisible();

await expect(u.po.page.getByText('Statements found')).toBeVisible();
await expect(u.po.page.getByText('Statement total: 99.96')).toBeVisible();

await expect(u.po.page.getByRole('heading', { name: 'Subscribed to Plus' })).toBeVisible();

await u.page.evaluate(async () => {
await window.Clerk.signOut({
redirectUrl: '/billing/hooks',
});
});

await expect(u.po.page.getByText('Plans found')).toBeVisible();
await expect(u.po.page.getByRole('heading', { name: 'Plan: Pro' })).toBeVisible();
await expect(u.po.page.getByText('No statements found')).toBeVisible();
await expect(u.po.page.getByText('No subscription found')).toBeVisible();

await expect(u.po.page.getByRole('heading', { name: 'Subscribed to Plus' })).toBeHidden();
await expect(u.po.page.getByText('Statements found', { exact: true })).toBeHidden();
await expect(u.po.page.getByText('Statement total: 99.96', { exact: true })).toBeHidden();
});
Comment on lines +54 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add wait after sign-out to prevent race condition.

After calling window.Clerk.signOut() on line 70-74, the test immediately makes assertions without waiting for:

  1. The redirect to /billing/hooks to complete
  2. The page to re-render with signed-out state
  3. The billing hooks to refetch data

This creates a race condition where assertions may run before sign-out completes.

Apply this diff to add an explicit wait:

       await u.page.evaluate(async () => {
         await window.Clerk.signOut({
           redirectUrl: '/billing/hooks',
         });
       });
+
+      // Wait for sign-out to complete and page to reload
+      await u.po.page.waitForURL('**/billing/hooks');
+      await u.po.page.waitForClerkJsLoaded();
+      // Wait for signed-out state to render
+      await expect(u.po.page.getByText('No subscription found')).toBeVisible({ timeout: 5000 });

       await expect(u.po.page.getByText('Plans found')).toBeVisible();

});
});