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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/cute-ideas-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

Add support for parsing seat-based billing fields from FAPI.
1 change: 1 addition & 0 deletions packages/clerk-js/sandbox/scenarios/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { UserButtonSignedIn } from './user-button-signed-in';
export { CheckoutAccountCredit } from './checkout-account-credit';
export { PricingTableSBB } from './pricing-table-sbb';
371 changes: 371 additions & 0 deletions packages/clerk-js/sandbox/scenarios/pricing-table-sbb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,371 @@
import {
clerkHandlers,
http,
HttpResponse,
EnvironmentService,
SessionService,
setClerkState,
type MockScenario,
UserService,
} from '@clerk/msw';
import type { BillingPlanJSON } from '@clerk/shared/types';

export function PricingTableSBB(): MockScenario {
const user = UserService.create();
const session = SessionService.create(user);
const money = (amount: number) => ({
amount,
amount_formatted: (amount / 100).toFixed(2),
currency: 'USD',
currency_symbol: '$',
});
const mockFeatures = [
{
object: 'feature' as const,
id: 'feature_custom_domains',
name: 'Custom domains',
description: 'Connect and manage branded domains.',
slug: 'custom-domains',
avatar_url: null,
},
{
object: 'feature' as const,
id: 'feature_saml_sso',
name: 'SAML SSO',
description: 'Single sign-on with enterprise identity providers.',
slug: 'saml-sso',
avatar_url: null,
},
{
object: 'feature' as const,
id: 'feature_audit_logs',
name: 'Audit logs',
description: 'Track account activity and security events.',
slug: 'audit-logs',
avatar_url: null,
},
{
object: 'feature' as const,
id: 'feature_priority_support',
name: 'Priority support',
description: 'Faster response times from the support team.',
slug: 'priority-support',
avatar_url: null,
},
{
object: 'feature' as const,
id: 'feature_rate_limit_boost',
name: 'Rate limit boost',
description: 'Higher API request thresholds for production traffic.',
slug: 'rate-limit-boost',
avatar_url: null,
},
];

setClerkState({
environment: EnvironmentService.MULTI_SESSION,
session,
user,
});

const subscriptionHandler = http.get('https://*.clerk.accounts.dev/v1/me/billing/subscription', () => {
return HttpResponse.json({
response: {
data: {},
},
});
});
Comment on lines +65 to +77
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

This signed-in sandbox scenario won't actually show the pricing table.

The scenario authenticates a user and then answers billing/subscription with an empty payload. In this PR's own PricingTable visibility tests, signed-in users without a subscription render no plans, so pricing-table-sbb is likely to come up blank instead of exercising the new seat-based states. Either return a subscription shape that keeps plans visible or make this scenario unauthenticated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/clerk-js/sandbox/scenarios/pricing-table-sbb.ts` around lines 65 -
77, The scenario authenticates via
setClerkState(EnvironmentService.MULTI_SESSION, session, user) but the mocked
subscriptionHandler
(http.get('https://*.clerk.accounts.dev/v1/me/billing/subscription')) returns an
empty data object so the PricingTable renders no plans; fix by returning a
realistic subscription shape in subscriptionHandler.response.response.data that
matches the pricing-table visibility tests (include active plan/tiers and seat
information) so plans remain visible, or alternatively remove the
setClerkState/login to make this scenario unauthenticated and keep the empty
subscription response—update the mock in subscriptionHandler or the
authentication setup accordingly.


const paymentMethodsHandler = http.get('https://*.clerk.accounts.dev/v1/me/billing/payment_methods', () => {
return HttpResponse.json({
response: {
data: {},
},
});
});

const plansHandler = http.get('https://*.clerk.accounts.dev/v1/billing/plans', () => {
return HttpResponse.json({
data: [
{
object: 'commerce_plan',
id: 'plan_a_sbb',
name: 'Plan A',
fee: money(12989),
annual_fee: null,
annual_monthly_fee: null,
description: null,
is_default: false,
is_recurring: true,
has_base_fee: true,
for_payer_type: 'org',
publicly_visible: true,
slug: 'plan-a-sbb',
avatar_url: null,
features: mockFeatures,
free_trial_enabled: false,
free_trial_days: null,
unit_prices: [
{
name: 'seat',
block_size: 1,
tiers: [
{
id: 'tier_plan_a_seats_1',
object: 'commerce_unit_price',
starts_at_block: 1,
ends_after_block: 5,
fee_per_block: money(0),
},
],
},
],
},
{
object: 'commerce_plan',
id: 'plan_b_sbb',
name: 'Plan B',
fee: money(12989),
annual_fee: null,
annual_monthly_fee: null,
description: null,
is_default: false,
is_recurring: true,
has_base_fee: true,
for_payer_type: 'org',
publicly_visible: true,
slug: 'plan-b-sbb',
avatar_url: null,
features: mockFeatures,
free_trial_enabled: false,
free_trial_days: null,
unit_prices: [
{
name: 'seat',
block_size: 1,
tiers: [
{
id: 'tier_plan_b_seats_1',
object: 'commerce_unit_price',
starts_at_block: 1,
ends_after_block: null,
fee_per_block: money(1200),
},
],
},
],
},
{
object: 'commerce_plan',
id: 'plan_c_sbb',
name: 'Plan C',
fee: money(0),
annual_fee: null,
annual_monthly_fee: null,
description: null,
is_default: false,
is_recurring: true,
has_base_fee: false,
for_payer_type: 'org',
publicly_visible: true,
slug: 'plan-c-sbb',
avatar_url: null,
features: mockFeatures,
free_trial_enabled: false,
free_trial_days: null,
unit_prices: [
{
name: 'seat',
block_size: 1,
tiers: [
{
id: 'tier_plan_c_seats_1',
object: 'commerce_unit_price',
starts_at_block: 1,
ends_after_block: null,
fee_per_block: money(1200),
},
],
},
],
},
{
object: 'commerce_plan',
id: 'plan_d_sbb',
name: 'Plan D',
fee: money(12989),
annual_fee: null,
annual_monthly_fee: null,
description: null,
is_default: false,
is_recurring: true,
has_base_fee: true,
for_payer_type: 'org',
publicly_visible: true,
slug: 'plan-d-sbb',
avatar_url: null,
features: mockFeatures,
free_trial_enabled: false,
free_trial_days: null,
unit_prices: [
{
name: 'seat',
block_size: 1,
tiers: [
{
id: 'tier_plan_d_seats_1',
object: 'commerce_unit_price',
starts_at_block: 1,
ends_after_block: 5,
fee_per_block: money(0),
},
{
id: 'tier_plan_d_seats_2',
object: 'commerce_unit_price',
starts_at_block: 6,
ends_after_block: null,
fee_per_block: money(1200),
},
],
},
],
},
{
object: 'commerce_plan',
id: 'plan_e_sbb',
name: 'Plan E',
fee: money(12989),
annual_fee: null,
annual_monthly_fee: null,
description: null,
is_default: false,
is_recurring: true,
has_base_fee: true,
for_payer_type: 'org',
publicly_visible: true,
slug: 'plan-e-sbb',
avatar_url: null,
features: mockFeatures,
free_trial_enabled: false,
free_trial_days: null,
},
{
object: 'commerce_plan',
id: 'plan_f_sbb',
name: 'Plan F',
fee: money(0),
annual_fee: null,
annual_monthly_fee: null,
description: null,
is_default: true,
is_recurring: true,
has_base_fee: false,
for_payer_type: 'org',
publicly_visible: true,
slug: 'plan-f-sbb',
avatar_url: null,
features: mockFeatures,
free_trial_enabled: false,
free_trial_days: null,
unit_prices: [
{
name: 'seat',
block_size: 1,
tiers: [
{
id: 'tier_plan_f_seats_1',
object: 'commerce_unit_price',
starts_at_block: 1,
ends_after_block: 5,
fee_per_block: money(0),
},
{
id: 'tier_plan_f_seats_2',
object: 'commerce_unit_price',
starts_at_block: 6,
ends_after_block: null,
fee_per_block: money(1200),
},
],
},
],
},
{
object: 'commerce_plan',
id: 'plan_g_sbb',
name: 'Plan G',
fee: money(0),
annual_fee: null,
annual_monthly_fee: null,
description: null,
is_default: false,
is_recurring: true,
has_base_fee: false,
for_payer_type: 'org',
publicly_visible: true,
slug: 'plan-g-sbb',
avatar_url: null,
features: mockFeatures,
free_trial_enabled: false,
free_trial_days: null,
unit_prices: [
{
name: 'seat',
block_size: 1,
tiers: [
{
id: 'tier_plan_g_seats_1',
object: 'commerce_unit_price',
starts_at_block: 1,
ends_after_block: null,
fee_per_block: money(0),
},
],
},
],
},
{
object: 'commerce_plan',
id: 'plan_h_sbb',
name: 'Plan H',
fee: money(12989),
annual_fee: money(10000),
annual_monthly_fee: money(833),
description: null,
is_default: false,
is_recurring: true,
has_base_fee: true,
for_payer_type: 'org',
publicly_visible: true,
slug: 'plan-h-sbb',
avatar_url: null,
features: mockFeatures,
free_trial_enabled: false,
free_trial_days: null,
unit_prices: [
{
name: 'seat',
block_size: 1,
tiers: [
{
id: 'tier_plan_h_seats_1',
object: 'commerce_unit_price',
starts_at_block: 1,
ends_after_block: null,
fee_per_block: money(0),
},
],
},
],
},
] as BillingPlanJSON[],
});
});

return {
description: 'PricingTable with seat-based billing plans',
handlers: [plansHandler, subscriptionHandler, paymentMethodsHandler, ...clerkHandlers],
initialState: { session, user },
name: 'pricing-table-sbb',
};
}
Loading
Loading