94b8c357b4
- add workspace-scoped billing storage, usage tracking, and add-on catalog - enforce plan entitlements for search and deep research routes - expand pricing and account UI around billing state, usage, and upgrades
620 lines
19 KiB
TypeScript
620 lines
19 KiB
TypeScript
export type PlanTier = 'starter' | 'growth' | 'pro' | 'enterprise';
|
|
|
|
export type BillingInterval = 'monthly' | 'annual' | 'custom';
|
|
|
|
export type PlanListingCategory = 'pricing_page_primary' | 'pricing_page_hidden' | 'internal_only';
|
|
|
|
export type FeatureReadiness = 'launch_ready' | 'marketed_not_enforced' | 'future';
|
|
|
|
export type ActivePlanCode =
|
|
| 'starter_monthly'
|
|
| 'starter_annual'
|
|
| 'growth_monthly'
|
|
| 'growth_annual'
|
|
| 'pro_monthly'
|
|
| 'pro_annual'
|
|
| 'enterprise_custom';
|
|
|
|
export type ReservedPlanCode = 'founder_lifetime' | 'founder_pro_lifetime';
|
|
|
|
export type PlanCode = ActivePlanCode | ReservedPlanCode;
|
|
|
|
export type AddonCode = 'export_pack_10k' | 'export_pack_50k' | 'enrichment_pack_1k' | 'ai_assistant_monthly' | 'white_label_monthly';
|
|
|
|
export type ProcessingTier = 'standard' | 'priority' | 'dedicated';
|
|
|
|
export interface PlanLimits {
|
|
researchRunsPerMonth: number | null;
|
|
exportsPerMonth: number | null;
|
|
// These are commercial packaging allowances for now. Hard enforcement comes later
|
|
// after workspace-scoped ownership and collaboration rules are ready.
|
|
usersIncluded: number | null;
|
|
workspacesIncluded: number | null;
|
|
enrichmentCreditsIncluded: number | null;
|
|
pooledUsage: boolean;
|
|
}
|
|
|
|
export interface PlanFeatures {
|
|
csvExport: boolean;
|
|
mapSearch: boolean;
|
|
radiusSearch: boolean;
|
|
basicFilters: boolean;
|
|
advancedFilters: boolean;
|
|
savedSearches: boolean;
|
|
territoryMapping: boolean;
|
|
deduplication: boolean;
|
|
exportHistory: boolean;
|
|
taggingNotes: boolean;
|
|
sharedLists: boolean;
|
|
scheduledResearch: boolean;
|
|
bulkExports: boolean;
|
|
crmIntegrations: boolean;
|
|
apiAccess: boolean;
|
|
webhooks: boolean;
|
|
collaboration: boolean;
|
|
enrichments: boolean;
|
|
prioritySupport: boolean;
|
|
sso: boolean;
|
|
sla: boolean;
|
|
whiteLabel: boolean;
|
|
}
|
|
|
|
export interface PlanDefinition {
|
|
code: ActivePlanCode;
|
|
tier: PlanTier;
|
|
planFamily: PlanTier;
|
|
name: string;
|
|
billingInterval: BillingInterval;
|
|
listingCategory: PlanListingCategory;
|
|
isSelfServe: boolean;
|
|
contactSalesRequired: boolean;
|
|
priceCents: number | null;
|
|
currencyCode: 'USD';
|
|
annualDiscountPercent: number | null;
|
|
limits: PlanLimits;
|
|
features: PlanFeatures;
|
|
featureReadiness: Partial<Record<keyof PlanFeatures, FeatureReadiness>>;
|
|
processingTier: ProcessingTier;
|
|
eligibleAddonCodes: AddonCode[];
|
|
}
|
|
|
|
export interface PlanDisplayMeta {
|
|
audience: string;
|
|
summary: string;
|
|
ctaLabel: string;
|
|
ctaMode: 'sign_in' | 'sign_up';
|
|
badgeLabel?: string;
|
|
qualitativeBullets: string[];
|
|
}
|
|
|
|
function createFeatureFlags(overrides: Partial<PlanFeatures>): PlanFeatures {
|
|
return {
|
|
csvExport: false,
|
|
mapSearch: false,
|
|
radiusSearch: false,
|
|
basicFilters: false,
|
|
advancedFilters: false,
|
|
savedSearches: false,
|
|
territoryMapping: false,
|
|
deduplication: false,
|
|
exportHistory: false,
|
|
taggingNotes: false,
|
|
sharedLists: false,
|
|
scheduledResearch: false,
|
|
bulkExports: false,
|
|
crmIntegrations: false,
|
|
apiAccess: false,
|
|
webhooks: false,
|
|
collaboration: false,
|
|
enrichments: false,
|
|
prioritySupport: false,
|
|
sso: false,
|
|
sla: false,
|
|
whiteLabel: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createPlan(plan: PlanDefinition): PlanDefinition {
|
|
return plan;
|
|
}
|
|
|
|
const activePlanCatalog = [
|
|
createPlan({
|
|
code: 'starter_monthly',
|
|
tier: 'starter',
|
|
planFamily: 'starter',
|
|
name: 'Starter',
|
|
billingInterval: 'monthly',
|
|
listingCategory: 'pricing_page_primary',
|
|
isSelfServe: true,
|
|
contactSalesRequired: false,
|
|
priceCents: 3900,
|
|
currencyCode: 'USD',
|
|
annualDiscountPercent: null,
|
|
limits: {
|
|
researchRunsPerMonth: 25,
|
|
exportsPerMonth: 2500,
|
|
usersIncluded: 1,
|
|
workspacesIncluded: 1,
|
|
enrichmentCreditsIncluded: 0,
|
|
pooledUsage: false,
|
|
},
|
|
features: createFeatureFlags({
|
|
csvExport: true,
|
|
mapSearch: true,
|
|
radiusSearch: true,
|
|
basicFilters: true,
|
|
}),
|
|
featureReadiness: {},
|
|
processingTier: 'standard',
|
|
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k'],
|
|
}),
|
|
createPlan({
|
|
code: 'starter_annual',
|
|
tier: 'starter',
|
|
planFamily: 'starter',
|
|
name: 'Starter',
|
|
billingInterval: 'annual',
|
|
listingCategory: 'pricing_page_hidden',
|
|
isSelfServe: true,
|
|
contactSalesRequired: false,
|
|
priceCents: 37440,
|
|
currencyCode: 'USD',
|
|
annualDiscountPercent: 20,
|
|
limits: {
|
|
researchRunsPerMonth: 25,
|
|
exportsPerMonth: 2500,
|
|
usersIncluded: 1,
|
|
workspacesIncluded: 1,
|
|
enrichmentCreditsIncluded: 0,
|
|
pooledUsage: false,
|
|
},
|
|
features: createFeatureFlags({
|
|
csvExport: true,
|
|
mapSearch: true,
|
|
radiusSearch: true,
|
|
basicFilters: true,
|
|
}),
|
|
featureReadiness: {},
|
|
processingTier: 'standard',
|
|
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k'],
|
|
}),
|
|
createPlan({
|
|
code: 'growth_monthly',
|
|
tier: 'growth',
|
|
planFamily: 'growth',
|
|
name: 'Growth',
|
|
billingInterval: 'monthly',
|
|
listingCategory: 'pricing_page_primary',
|
|
isSelfServe: true,
|
|
contactSalesRequired: false,
|
|
priceCents: 9900,
|
|
currencyCode: 'USD',
|
|
annualDiscountPercent: null,
|
|
limits: {
|
|
researchRunsPerMonth: 150,
|
|
exportsPerMonth: 15000,
|
|
usersIncluded: 3,
|
|
workspacesIncluded: 5,
|
|
enrichmentCreditsIncluded: 0,
|
|
pooledUsage: false,
|
|
},
|
|
features: createFeatureFlags({
|
|
csvExport: true,
|
|
mapSearch: true,
|
|
radiusSearch: true,
|
|
basicFilters: true,
|
|
advancedFilters: true,
|
|
savedSearches: true,
|
|
territoryMapping: true,
|
|
deduplication: true,
|
|
exportHistory: true,
|
|
taggingNotes: true,
|
|
prioritySupport: true,
|
|
}),
|
|
featureReadiness: {
|
|
savedSearches: 'marketed_not_enforced',
|
|
territoryMapping: 'launch_ready',
|
|
deduplication: 'marketed_not_enforced',
|
|
exportHistory: 'marketed_not_enforced',
|
|
taggingNotes: 'marketed_not_enforced',
|
|
},
|
|
processingTier: 'priority',
|
|
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k'],
|
|
}),
|
|
createPlan({
|
|
code: 'growth_annual',
|
|
tier: 'growth',
|
|
planFamily: 'growth',
|
|
name: 'Growth',
|
|
billingInterval: 'annual',
|
|
listingCategory: 'pricing_page_hidden',
|
|
isSelfServe: true,
|
|
contactSalesRequired: false,
|
|
priceCents: 95040,
|
|
currencyCode: 'USD',
|
|
annualDiscountPercent: 20,
|
|
limits: {
|
|
researchRunsPerMonth: 150,
|
|
exportsPerMonth: 15000,
|
|
usersIncluded: 3,
|
|
workspacesIncluded: 5,
|
|
enrichmentCreditsIncluded: 0,
|
|
pooledUsage: false,
|
|
},
|
|
features: createFeatureFlags({
|
|
csvExport: true,
|
|
mapSearch: true,
|
|
radiusSearch: true,
|
|
basicFilters: true,
|
|
advancedFilters: true,
|
|
savedSearches: true,
|
|
territoryMapping: true,
|
|
deduplication: true,
|
|
exportHistory: true,
|
|
taggingNotes: true,
|
|
prioritySupport: true,
|
|
}),
|
|
featureReadiness: {
|
|
savedSearches: 'marketed_not_enforced',
|
|
territoryMapping: 'launch_ready',
|
|
deduplication: 'marketed_not_enforced',
|
|
exportHistory: 'marketed_not_enforced',
|
|
taggingNotes: 'marketed_not_enforced',
|
|
},
|
|
processingTier: 'priority',
|
|
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k'],
|
|
}),
|
|
createPlan({
|
|
code: 'pro_monthly',
|
|
tier: 'pro',
|
|
planFamily: 'pro',
|
|
name: 'Pro',
|
|
billingInterval: 'monthly',
|
|
listingCategory: 'pricing_page_primary',
|
|
isSelfServe: true,
|
|
contactSalesRequired: false,
|
|
priceCents: 24900,
|
|
currencyCode: 'USD',
|
|
annualDiscountPercent: null,
|
|
limits: {
|
|
researchRunsPerMonth: 500,
|
|
exportsPerMonth: 75000,
|
|
usersIncluded: 15,
|
|
workspacesIncluded: null,
|
|
enrichmentCreditsIncluded: 0,
|
|
pooledUsage: false,
|
|
},
|
|
features: createFeatureFlags({
|
|
csvExport: true,
|
|
mapSearch: true,
|
|
radiusSearch: true,
|
|
basicFilters: true,
|
|
advancedFilters: true,
|
|
savedSearches: true,
|
|
territoryMapping: true,
|
|
deduplication: true,
|
|
exportHistory: true,
|
|
taggingNotes: true,
|
|
sharedLists: true,
|
|
scheduledResearch: true,
|
|
bulkExports: true,
|
|
crmIntegrations: true,
|
|
apiAccess: true,
|
|
webhooks: true,
|
|
collaboration: true,
|
|
enrichments: true,
|
|
prioritySupport: true,
|
|
}),
|
|
featureReadiness: {
|
|
savedSearches: 'marketed_not_enforced',
|
|
deduplication: 'marketed_not_enforced',
|
|
exportHistory: 'marketed_not_enforced',
|
|
taggingNotes: 'marketed_not_enforced',
|
|
sharedLists: 'marketed_not_enforced',
|
|
scheduledResearch: 'marketed_not_enforced',
|
|
bulkExports: 'marketed_not_enforced',
|
|
crmIntegrations: 'marketed_not_enforced',
|
|
apiAccess: 'marketed_not_enforced',
|
|
webhooks: 'marketed_not_enforced',
|
|
collaboration: 'marketed_not_enforced',
|
|
enrichments: 'marketed_not_enforced',
|
|
},
|
|
processingTier: 'priority',
|
|
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k', 'ai_assistant_monthly', 'white_label_monthly'],
|
|
}),
|
|
createPlan({
|
|
code: 'pro_annual',
|
|
tier: 'pro',
|
|
planFamily: 'pro',
|
|
name: 'Pro',
|
|
billingInterval: 'annual',
|
|
listingCategory: 'pricing_page_hidden',
|
|
isSelfServe: true,
|
|
contactSalesRequired: false,
|
|
priceCents: 239040,
|
|
currencyCode: 'USD',
|
|
annualDiscountPercent: 20,
|
|
limits: {
|
|
researchRunsPerMonth: 500,
|
|
exportsPerMonth: 75000,
|
|
usersIncluded: 15,
|
|
workspacesIncluded: null,
|
|
enrichmentCreditsIncluded: 0,
|
|
pooledUsage: false,
|
|
},
|
|
features: createFeatureFlags({
|
|
csvExport: true,
|
|
mapSearch: true,
|
|
radiusSearch: true,
|
|
basicFilters: true,
|
|
advancedFilters: true,
|
|
savedSearches: true,
|
|
territoryMapping: true,
|
|
deduplication: true,
|
|
exportHistory: true,
|
|
taggingNotes: true,
|
|
sharedLists: true,
|
|
scheduledResearch: true,
|
|
bulkExports: true,
|
|
crmIntegrations: true,
|
|
apiAccess: true,
|
|
webhooks: true,
|
|
collaboration: true,
|
|
enrichments: true,
|
|
prioritySupport: true,
|
|
}),
|
|
featureReadiness: {
|
|
savedSearches: 'marketed_not_enforced',
|
|
deduplication: 'marketed_not_enforced',
|
|
exportHistory: 'marketed_not_enforced',
|
|
taggingNotes: 'marketed_not_enforced',
|
|
sharedLists: 'marketed_not_enforced',
|
|
scheduledResearch: 'marketed_not_enforced',
|
|
bulkExports: 'marketed_not_enforced',
|
|
crmIntegrations: 'marketed_not_enforced',
|
|
apiAccess: 'marketed_not_enforced',
|
|
webhooks: 'marketed_not_enforced',
|
|
collaboration: 'marketed_not_enforced',
|
|
enrichments: 'marketed_not_enforced',
|
|
},
|
|
processingTier: 'priority',
|
|
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k', 'ai_assistant_monthly', 'white_label_monthly'],
|
|
}),
|
|
createPlan({
|
|
code: 'enterprise_custom',
|
|
tier: 'enterprise',
|
|
planFamily: 'enterprise',
|
|
name: 'Enterprise',
|
|
billingInterval: 'custom',
|
|
listingCategory: 'pricing_page_primary',
|
|
isSelfServe: false,
|
|
contactSalesRequired: true,
|
|
priceCents: null,
|
|
currencyCode: 'USD',
|
|
annualDiscountPercent: null,
|
|
limits: {
|
|
researchRunsPerMonth: null,
|
|
exportsPerMonth: null,
|
|
usersIncluded: null,
|
|
workspacesIncluded: null,
|
|
enrichmentCreditsIncluded: null,
|
|
pooledUsage: true,
|
|
},
|
|
features: createFeatureFlags({
|
|
csvExport: true,
|
|
mapSearch: true,
|
|
radiusSearch: true,
|
|
basicFilters: true,
|
|
advancedFilters: true,
|
|
savedSearches: true,
|
|
territoryMapping: true,
|
|
deduplication: true,
|
|
exportHistory: true,
|
|
taggingNotes: true,
|
|
sharedLists: true,
|
|
scheduledResearch: true,
|
|
bulkExports: true,
|
|
crmIntegrations: true,
|
|
apiAccess: true,
|
|
webhooks: true,
|
|
collaboration: true,
|
|
enrichments: true,
|
|
prioritySupport: true,
|
|
sso: true,
|
|
sla: true,
|
|
whiteLabel: true,
|
|
}),
|
|
featureReadiness: {
|
|
sharedLists: 'marketed_not_enforced',
|
|
scheduledResearch: 'marketed_not_enforced',
|
|
bulkExports: 'marketed_not_enforced',
|
|
crmIntegrations: 'marketed_not_enforced',
|
|
apiAccess: 'marketed_not_enforced',
|
|
webhooks: 'marketed_not_enforced',
|
|
collaboration: 'marketed_not_enforced',
|
|
enrichments: 'marketed_not_enforced',
|
|
sso: 'future',
|
|
sla: 'future',
|
|
whiteLabel: 'future',
|
|
},
|
|
processingTier: 'dedicated',
|
|
eligibleAddonCodes: ['export_pack_10k', 'export_pack_50k', 'enrichment_pack_1k', 'ai_assistant_monthly', 'white_label_monthly'],
|
|
}),
|
|
] as const satisfies readonly PlanDefinition[];
|
|
|
|
const activePlanCatalogByCode = new Map(activePlanCatalog.map((plan) => [plan.code, plan]));
|
|
|
|
const planDisplayMetaByCode: Record<ActivePlanCode, PlanDisplayMeta> = {
|
|
starter_monthly: {
|
|
audience: 'For freelancers and solo operators',
|
|
summary: 'A focused entry point for recurring local market research.',
|
|
ctaLabel: 'Choose Starter',
|
|
ctaMode: 'sign_up',
|
|
qualitativeBullets: ['CSV export, map search, radius search, and basic filters'],
|
|
},
|
|
starter_annual: {
|
|
audience: 'For freelancers and solo operators',
|
|
summary: 'A focused entry point for recurring local market research.',
|
|
ctaLabel: 'Choose Starter',
|
|
ctaMode: 'sign_up',
|
|
badgeLabel: 'Save 20%',
|
|
qualitativeBullets: ['CSV export, map search, radius search, and basic filters'],
|
|
},
|
|
growth_monthly: {
|
|
audience: 'For agencies and outbound teams',
|
|
summary: 'The best-value plan for repeatable territory workflows.',
|
|
ctaLabel: 'Choose Growth',
|
|
ctaMode: 'sign_up',
|
|
badgeLabel: 'Best Value',
|
|
qualitativeBullets: ['Saved searches, territory mapping, advanced filtering, deduplication, export history, and priority support'],
|
|
},
|
|
growth_annual: {
|
|
audience: 'For agencies and outbound teams',
|
|
summary: 'The best-value plan for repeatable territory workflows.',
|
|
ctaLabel: 'Choose Growth',
|
|
ctaMode: 'sign_up',
|
|
badgeLabel: 'Save 20%',
|
|
qualitativeBullets: ['Saved searches, territory mapping, advanced filtering, deduplication, export history, and priority support'],
|
|
},
|
|
pro_monthly: {
|
|
audience: 'For power users and scaling teams',
|
|
summary: 'Operational infrastructure for teams that need more scale and automation.',
|
|
ctaLabel: 'Choose Pro',
|
|
ctaMode: 'sign_up',
|
|
qualitativeBullets: ['Scheduled research, bulk exports, CRM integrations, API access, enrichment access, and collaboration features'],
|
|
},
|
|
pro_annual: {
|
|
audience: 'For power users and scaling teams',
|
|
summary: 'Operational infrastructure for teams that need more scale and automation.',
|
|
ctaLabel: 'Choose Pro',
|
|
ctaMode: 'sign_up',
|
|
badgeLabel: 'Save 20%',
|
|
qualitativeBullets: ['Scheduled research, bulk exports, CRM integrations, API access, enrichment access, and collaboration features'],
|
|
},
|
|
enterprise_custom: {
|
|
audience: 'For multi-location and enterprise rollouts',
|
|
summary: 'Custom market intelligence infrastructure for security, governance, and scale.',
|
|
ctaLabel: 'Talk to sales',
|
|
ctaMode: 'sign_in',
|
|
qualitativeBullets: ['SSO, SLA, onboarding, and account management', 'White-labeling, custom enrichments, and custom integrations', 'Dedicated infrastructure and advanced API scaling'],
|
|
},
|
|
};
|
|
|
|
export const ACTIVE_PLAN_CATALOG = [...activePlanCatalog];
|
|
|
|
export function getActivePlanCatalog() {
|
|
return ACTIVE_PLAN_CATALOG;
|
|
}
|
|
|
|
export function getPlanByCode(code: PlanCode) {
|
|
return activePlanCatalogByCode.get(code as ActivePlanCode) ?? null;
|
|
}
|
|
|
|
export function getSelfServePlans() {
|
|
return ACTIVE_PLAN_CATALOG.filter((plan) => plan.isSelfServe);
|
|
}
|
|
|
|
export function getAnnualPlans() {
|
|
return ACTIVE_PLAN_CATALOG.filter((plan) => plan.billingInterval === 'annual');
|
|
}
|
|
|
|
export function getPublicPricingPlans() {
|
|
return ACTIVE_PLAN_CATALOG.filter((plan) => plan.listingCategory === 'pricing_page_primary');
|
|
}
|
|
|
|
export function getPublicPricingPlansForInterval(billingInterval: BillingInterval) {
|
|
return ACTIVE_PLAN_CATALOG.filter((plan) => {
|
|
if (plan.code === 'enterprise_custom') {
|
|
return true;
|
|
}
|
|
|
|
return plan.listingCategory === 'pricing_page_hidden'
|
|
? plan.billingInterval === billingInterval
|
|
: plan.listingCategory === 'pricing_page_primary' && plan.billingInterval === billingInterval;
|
|
});
|
|
}
|
|
|
|
export function getPlanVariant(tier: PlanTier, billingInterval: BillingInterval) {
|
|
return ACTIVE_PLAN_CATALOG.find((plan) => plan.planFamily === tier && plan.billingInterval === billingInterval) ?? null;
|
|
}
|
|
|
|
export function getSiblingPlans(code: ActivePlanCode) {
|
|
const selectedPlan = getPlanByCode(code);
|
|
|
|
if (!selectedPlan) {
|
|
return [];
|
|
}
|
|
|
|
return ACTIVE_PLAN_CATALOG.filter((plan) => plan.planFamily === selectedPlan.planFamily);
|
|
}
|
|
|
|
export function isAnnualPlan(code: ActivePlanCode) {
|
|
return getPlanByCode(code)?.billingInterval === 'annual';
|
|
}
|
|
|
|
export function getPlanDisplayMeta(code: ActivePlanCode) {
|
|
return planDisplayMetaByCode[code];
|
|
}
|
|
|
|
export function getPlanCardBullets(code: ActivePlanCode) {
|
|
const plan = getPlanByCode(code);
|
|
const display = getPlanDisplayMeta(code);
|
|
|
|
if (!plan) {
|
|
return display.qualitativeBullets;
|
|
}
|
|
|
|
const quantitativeBullets = [
|
|
formatResearchRunsBullet(plan.limits.researchRunsPerMonth),
|
|
formatExportsBullet(plan.limits.exportsPerMonth),
|
|
formatSeatAllowanceBullet(plan.limits.usersIncluded, plan.limits.workspacesIncluded, plan.limits.pooledUsage),
|
|
].filter((bullet): bullet is string => bullet !== null);
|
|
|
|
return [...quantitativeBullets, ...display.qualitativeBullets];
|
|
}
|
|
|
|
function formatResearchRunsBullet(researchRunsPerMonth: number | null) {
|
|
if (researchRunsPerMonth === null) {
|
|
return 'Pooled or custom research capacity';
|
|
}
|
|
|
|
return `${formatCount(researchRunsPerMonth)} research runs / month`;
|
|
}
|
|
|
|
function formatExportsBullet(exportsPerMonth: number | null) {
|
|
if (exportsPerMonth === null) {
|
|
return null;
|
|
}
|
|
|
|
return `${formatCount(exportsPerMonth)} exports / month`;
|
|
}
|
|
|
|
function formatSeatAllowanceBullet(usersIncluded: number | null, workspacesIncluded: number | null, pooledUsage: boolean) {
|
|
if (pooledUsage) {
|
|
return 'Pooled or custom team usage';
|
|
}
|
|
|
|
if (usersIncluded === null && workspacesIncluded === null) {
|
|
return null;
|
|
}
|
|
|
|
if (usersIncluded !== null && workspacesIncluded === null) {
|
|
return usersIncluded === 1 ? '1 user and unlimited workspaces' : `${formatCount(usersIncluded)} users and unlimited workspaces`;
|
|
}
|
|
|
|
if (usersIncluded === null && workspacesIncluded !== null) {
|
|
return workspacesIncluded === 1 ? '1 workspace' : `${formatCount(workspacesIncluded)} workspaces`;
|
|
}
|
|
|
|
const userLabel = usersIncluded === 1 ? '1 user' : `${formatCount(usersIncluded as number)} users`;
|
|
const workspaceLabel = workspacesIncluded === 1 ? '1 workspace' : `${formatCount(workspacesIncluded as number)} workspaces`;
|
|
|
|
return `${userLabel} and ${workspaceLabel}`;
|
|
}
|
|
|
|
function formatCount(value: number) {
|
|
return new Intl.NumberFormat('en-US').format(value);
|
|
}
|