feat: differentiate personal/team pricing table with two-stage team workspace flow (#9901)

## Summary

Differentiates the subscription pricing dialog between personal and team
workspaces with distinct visual treatments and a two-stage team
workspace upgrade flow.

### Changes

- **Personal pricing dialog**: Shows "P" avatar badge, "Plans for
Personal Workspace" header, and "Solo use only – Need team workspace?"
banner on each tier card
- **Team pricing dialog**: Shows workspace avatar, "Plans for Team
Workspace" header (emerald), green "Invite up to X members" badge, and
emerald border on Creator card
- **Two-stage upgrade flow**: "Need team workspace?" → closes pricing →
opens CreateWorkspaceDialog → sessionStorage flag → page reload →
WorkspaceAuthGate auto-opens team pricing dialog
- **Spacing**: Reduced vertical gaps/padding/font sizes so the table
fits without scrolling

### Key decisions

- sessionStorage key `comfy:resume-team-pricing` bridges the page reload
during workspace creation
- `onChooseTeam` prop is conditionally passed only to the personal
variant
- `resumePendingPricingFlow()` is called from WorkspaceAuthGate after
workspace initialization

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9901-feat-differentiate-personal-team-pricing-table-with-two-stage-team-workspace-flow-3226d73d365081e7af60dcca86e83673)
by [Unito](https://www.unito.io)
This commit is contained in:
Hunter
2026-03-23 09:17:19 -07:00
committed by GitHub
parent bd322314bc
commit cd45efa983
32 changed files with 1426 additions and 154 deletions

View File

@@ -1,9 +1,5 @@
<template>
<div class="flex flex-col gap-8">
<h2 class="m-0 text-center text-xl text-muted-foreground lg:text-2xl">
{{ t('subscription.chooseBestPlanWorkspace') }}
</h2>
<div class="flex flex-col gap-6">
<div class="flex justify-center">
<SelectButton
v-model="currentBillingCycle"
@@ -42,18 +38,18 @@
</template>
</SelectButton>
</div>
<div class="flex flex-col items-stretch gap-6 xl:flex-row">
<div class="flex flex-col items-stretch gap-4 xl:flex-row">
<div
v-for="tier in tiers"
:key="tier.id"
:class="
cn(
'flex flex-1 flex-col rounded-2xl border border-border-default bg-base-background shadow-[0_0_12px_rgba(0,0,0,0.1)]',
tier.isPopular ? 'border-muted-foreground' : ''
tier.isPopular ? 'border-emerald-500' : ''
)
"
>
<div class="flex flex-col gap-8 p-8 pb-0">
<div class="flex flex-col gap-4 p-6 pb-0">
<div class="flex flex-row items-center justify-between gap-2">
<span
class="font-inter text-base/normal font-bold text-base-foreground"
@@ -71,7 +67,7 @@
<div class="flex flex-col gap-2">
<div class="flex flex-row items-baseline gap-2">
<span
class="font-inter text-[32px] leading-normal font-semibold text-base-foreground"
class="font-inter text-[28px] leading-normal font-semibold text-base-foreground"
>
<span
v-show="currentBillingCycle === 'yearly'"
@@ -99,7 +95,35 @@
</div>
</div>
<div class="flex flex-1 flex-col gap-4 pb-0">
<div
:class="
cn(
'flex h-10 items-center justify-between rounded-lg px-3',
maxMembersByTier[tier.key] > 1 ? 'bg-emerald-500/20' : ''
)
"
>
<template v-if="maxMembersByTier[tier.key] > 1">
<div class="flex items-center gap-2">
<i
class="pi pi-users text-xs text-emerald-400"
aria-hidden="true"
/>
<span class="text-sm text-emerald-400">
{{ t('subscription.inviteUpTo') }}
</span>
</div>
<span class="text-sm font-bold text-base-foreground">
{{
t('subscription.memberCount', {
count: maxMembersByTier[tier.key]
})
}}
</span>
</template>
</div>
<div class="flex flex-1 flex-col gap-3 pb-0">
<div class="flex flex-row items-center justify-between">
<span class="text-foreground text-sm font-normal">
{{ t('subscription.monthlyCreditsPerMemberLabel') }}
@@ -121,7 +145,7 @@
<span
class="font-inter text-sm/normal font-bold text-base-foreground"
>
{{ getMaxMembers(tier) }}
{{ maxMembersByTier[tier.key] }}
</span>
</div>
@@ -188,7 +212,7 @@
</div>
</div>
</div>
<div class="flex flex-col p-8">
<div class="flex flex-col p-6">
<Button
:variant="getButtonSeverity(tier)"
:disabled="isButtonDisabled(tier)"
@@ -198,7 +222,7 @@
'h-10 w-full',
getButtonTextClass(tier),
tier.key === 'creator'
? 'border-transparent bg-base-foreground hover:bg-inverted-background-hover'
? 'border-transparent bg-success-background hover:bg-success-background/80'
: 'border-transparent bg-secondary-background hover:bg-secondary-background-hover focus:bg-secondary-background-selected'
)
"
@@ -482,7 +506,13 @@ const getAnnualTotal = (tier: PricingTierConfig): number => {
return plan ? plan.price_cents / 100 : tier.pricing.yearly * 12
}
const getMaxMembers = (tier: PricingTierConfig): number => getMaxSeats(tier.key)
const maxMembersByTier = computed(
() =>
Object.fromEntries(tiers.map((t) => [t.key, getMaxSeats(t.key)])) as Record<
CheckoutTierKey,
number
>
)
const getMonthlyCreditsPerMember = (tier: PricingTierConfig): number =>
tier.pricing.credits