-
Notifications
You must be signed in to change notification settings - Fork 672
feat(billing): render the Enterprise tier and hide its self-serve pricing surfaces #15403
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?
Changes from 1 commit
7209fe4
5ee9b28
ca29a67
fe0a575
1d9543b
b849cd0
af2f2a8
2b8fe82
844e4c6
5e9d79b
d296689
d222055
bb0bc30
8743dc5
8bf9f76
b38acfa
32c7f10
2e89c37
9fd27ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,10 @@ | ||
| import { useRoute, useRouter } from 'vue-router' | ||
|
|
||
| import { useBillingContext } from '@/composables/billing/useBillingContext' | ||
| import { | ||
| isEnterprisePlanSlug, | ||
| isEnterpriseTier | ||
| } from '@/platform/cloud/subscription/constants/tierPricing' | ||
| import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog' | ||
| import { | ||
| clearPreservedQuery, | ||
|
|
@@ -102,7 +106,7 @@ | |
| const route = useRoute() | ||
| const router = useRouter() | ||
| const subscriptionDialog = useSubscriptionDialog() | ||
| const { teamCreditStops, fetchPlans } = useBillingContext() | ||
| const { subscription, teamCreditStops, fetchPlans } = useBillingContext() | ||
| const { permissions } = useWorkspaceUI() | ||
|
|
||
| /** Reads `?pricing=`, strips it, and opens the table when the gate allows. */ | ||
|
|
@@ -139,6 +143,14 @@ | |
| if (typeof param !== 'string' || !param) return | ||
|
|
||
| if (!permissions.value.canManageSubscription) return | ||
| // Enterprise is sales-managed: the pricing table never opens for it, even | ||
| // from a deep link. The param was already stripped above. | ||
| if ( | ||
| isEnterpriseTier(subscription.value?.tier) || | ||
|
Check failure on line 149 in src/platform/cloud/subscription/composables/usePricingTableUrlLoader.ts
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High — The Enterprise gate reads |
||
| isEnterprisePlanSlug(subscription.value?.planSlug) | ||
| ) { | ||
| return | ||
| } | ||
|
|
||
| const teamCheckoutRequest = getTeamCheckoutRequest( | ||
| param, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,7 +58,21 @@ export const DEFAULT_TIER_KEY: TierKey = 'standard' | |
|
|
||
| // TEAM is workspace-level, so it maps to no key in this personal plan catalog. | ||
| export function toTierKey(tier: IngestSubscriptionTier): TierKey | null { | ||
| return tier === 'TEAM' ? null : TIER_TO_KEY[tier] | ||
| return tier === 'TEAM' ? null : (TIER_TO_KEY[tier] ?? null) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low — |
||
| } | ||
|
|
||
| // ENTERPRISE is a sales-managed workspace tier: absent from the personal | ||
| // catalog and (until the ingest enum ships it) from the generated types, so it | ||
| // is matched as a runtime string. It never self-serves plan changes. | ||
| const ENTERPRISE_TIER = 'ENTERPRISE' | ||
| const ENTERPRISE_PLAN_SLUG_PREFIX = 'enterprise' | ||
|
|
||
| export function isEnterpriseTier(tier: string | null | undefined): boolean { | ||
| return tier?.toUpperCase() === ENTERPRISE_TIER | ||
| } | ||
|
|
||
| export function isEnterprisePlanSlug(slug: string | null | undefined): boolean { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low — |
||
| return slug?.toLowerCase().startsWith(ENTERPRISE_PLAN_SLUG_PREFIX) === true | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This predicate encodes server-owned business policy in the frontend. |
||
| } | ||
|
|
||
| // Includes the workspace-level TEAM, which toTierKey maps to null: a catalog | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -229,6 +229,10 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser' | |
|
|
||
| import { useExternalLink } from '@/composables/useExternalLink' | ||
| import { useBillingContext } from '@/composables/billing/useBillingContext' | ||
| import { | ||
| isEnterprisePlanSlug, | ||
| isEnterpriseTier | ||
| } from '@/platform/cloud/subscription/constants/tierPricing' | ||
| import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue' | ||
| import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog' | ||
| import { isCloud } from '@/platform/distribution/types' | ||
|
|
@@ -303,8 +307,13 @@ const displayedCredits = computed(() => { | |
| }) | ||
| }) | ||
|
|
||
| const isEnterprisePlan = computed( | ||
| () => | ||
| isEnterpriseTier(subscription.value?.tier) || | ||
| isEnterprisePlanSlug(subscription.value?.planSlug) | ||
| ) | ||
| const showPlansAndPricing = computed( | ||
| () => permissions.value.canManageSubscription | ||
| () => permissions.value.canManageSubscription && !isEnterprisePlan.value | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High — Hiding |
||
| ) | ||
| const hasDelinquentSubscription = computed( | ||
| () => | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -225,7 +225,10 @@ | |
| severity="warn" | ||
| /> | ||
| </div> | ||
| <div class="flex items-baseline gap-1 font-inter"> | ||
| <div | ||
| v-if="!isEnterprisePlan" | ||
| class="flex items-baseline gap-1 font-inter" | ||
| > | ||
| <span class="text-2xl font-semibold">{{ displayPrice }}</span> | ||
| <span class="text-base">{{ priceUnitLabel }}</span> | ||
| </div> | ||
|
|
@@ -276,7 +279,8 @@ | |
| v-else-if=" | ||
| !isSubscriptionCancelled && | ||
| canAccessSubscriptionFeatures && | ||
| permissions.canManageSubscription | ||
| permissions.canManageSubscription && | ||
| !isEnterprisePlan | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium — Only the "Change plan" branch is gated on |
||
| " | ||
| size="lg" | ||
| variant="secondary" | ||
|
|
@@ -417,6 +421,10 @@ import { useBillingContext } from '@/composables/billing/useBillingContext' | |
| import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog' | ||
| import { useFreeTierQuota } from '@/platform/cloud/subscription/composables/useFreeTierQuota' | ||
| import type { TierBenefit } from '@/platform/cloud/subscription/utils/tierBenefits' | ||
| import { | ||
| isEnterprisePlanSlug, | ||
| isEnterpriseTier | ||
| } from '@/platform/cloud/subscription/constants/tierPricing' | ||
| import { getCommonTierBenefits } from '@/platform/cloud/subscription/utils/tierBenefits' | ||
| import { isCloud } from '@/platform/distribution/types' | ||
| import { useResubscribe } from '@/platform/workspace/composables/useResubscribe' | ||
|
|
@@ -566,6 +574,12 @@ const scheduledPlanName = computed(() => { | |
| (plan) => plan.slug === subscription.value?.scheduledPlanSlug | ||
| ) | ||
| if (!scheduledPlan) return '' | ||
| if ( | ||
| isEnterprisePlanSlug(scheduledPlan.slug) || | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium — Enterprise classification happens only after the scheduled plan is found in the self-service |
||
| isEnterpriseTier(scheduledPlan.tier) | ||
| ) { | ||
| return t('subscription.tiers.enterprise.name') | ||
| } | ||
| if (scheduledPlan.slug.startsWith('team')) { | ||
| return t('subscription.teamPlanName') | ||
| } | ||
|
|
@@ -627,10 +641,19 @@ const subscriptionTierName = computed(() => { | |
| : baseName | ||
| }) | ||
|
|
||
| const planDisplayName = computed(() => | ||
| isTeamPlan.value ? t('subscription.teamPlanName') : subscriptionTierName.value | ||
| const isEnterprisePlan = computed( | ||
| () => | ||
| isEnterpriseTier(subscription.value?.tier) || | ||
| isEnterprisePlanSlug(subscription.value?.planSlug) | ||
| ) | ||
|
|
||
| const planDisplayName = computed(() => { | ||
| if (isEnterprisePlan.value) return t('subscription.tiers.enterprise.name') | ||
| return isTeamPlan.value | ||
| ? t('subscription.teamPlanName') | ||
| : subscriptionTierName.value | ||
| }) | ||
|
|
||
| const tierKey = computed(() => | ||
| resolveSubscriptionTierKey(subscription.value?.tier) | ||
| ) | ||
|
|
@@ -643,6 +666,7 @@ const TEAM_PERK_KEYS = [ | |
| ] as const | ||
|
|
||
| const tierBenefits = computed((): TierBenefit[] => { | ||
| if (isEnterprisePlan.value) return [] | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| if (isTeamActive.value || showInactiveTeamSubscription.value) { | ||
| return TEAM_PERK_KEYS.map((key) => ({ | ||
| key, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -257,7 +257,7 @@ defineEmits<{ | |
| back: [] | ||
| }>() | ||
|
|
||
| const { t, n } = useI18n() | ||
| const { t, te, n } = useI18n() | ||
| const { subscription } = useBillingContext() | ||
|
|
||
| function openVerification() { | ||
|
|
@@ -266,7 +266,10 @@ function openVerification() { | |
| } | ||
|
|
||
| function formatTierName(tier: string): string { | ||
| return t(`subscription.tiers.${tier.toLowerCase()}.name`) | ||
| const nameKey = `subscription.tiers.${tier.toLowerCase()}.name` | ||
| if (te(nameKey)) return t(nameKey) | ||
| const lower = tier.toLowerCase() | ||
| return lower.charAt(0).toUpperCase() + lower.slice(1) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium — |
||
| } | ||
|
|
||
| function isTeamTier(tier: string): boolean { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,8 @@ const tierKeyMap: Record<string, string> = { | |
| CREATOR: 'creator', | ||
| PRO: 'pro', | ||
| FOUNDER: 'founder', | ||
| FOUNDERS_EDITION: 'founder' | ||
| FOUNDERS_EDITION: 'founder', | ||
| ENTERPRISE: 'enterprise' | ||
|
coderabbitai[bot] marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium — The new |
||
| } | ||
|
|
||
| interface WorkspaceSubscriptionInfo { | ||
|
|
||
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.
🟠 High — This is the only Enterprise check in the PR that tests
tieralone; every other site (CurrentUserPopoverWorkspace,SubscriptionPanelContentWorkspace,usePricingTableUrlLoader) usesisEnterpriseTier(tier) || isEnterprisePlanSlug(planSlug). The PR's own test fixture models Enterprise as{ slug: 'enterprise_monthly', tier: 'PRO' }, and for that shape the guard misses,tierKeyfalls back throughtoTierKey(tier) ?? DEFAULT_TIER_KEY, and the tile renders a fabricated monthly allowance plus a usage bar computed against it. Raised by 5 of 8 reviewers (gpt-5.6-sol-max adversarial, gpt-5.6-sol-max edge-case, claude-opus-5-thinking-max adversarial, claude-opus-5-thinking-max edge-case, gemini-3.1-pro edge-case, kimi-k3-max edge-case).