feat: invite member upsell for single-seat plans (#8801)

## Summary

- Show an upsell dialog when single-seat plan users try to invite
members, with a banner on the members panel directing them to upgrade.
- Misc fixes for member max seat display

## Changes

- **What**: `InviteMemberUpsellDialogContent.vue`,
`MembersPanelContent.vue`, `WorkspacePanelContent.vue`

## Screenshots

<img width="2730" height="1907" alt="image"
src="https://github.com/user-attachments/assets/e39a23be-8533-4ebb-a4ae-2797fc382bc2"
/>
<img width="2730" height="1907" alt="image"
src="https://github.com/user-attachments/assets/bec55867-1088-4d3a-b308-5d5cce64c8ae"
/>



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8801-feat-invite-member-upsell-for-single-seat-plans-3046d73d365081349b09fe1d4dc572e8)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Simula_r
2026-02-11 21:15:59 -08:00
committed by GitHub
parent 0d64d503ec
commit 85ae0a57c3
16 changed files with 411 additions and 129 deletions

View File

@@ -374,7 +374,8 @@ const {
plans: apiPlans,
currentPlanSlug,
fetchPlans,
subscription
subscription,
getMaxSeats
} = useBillingContext()
const isCancelled = computed(() => subscription.value?.isCancelled ?? false)
@@ -404,11 +405,6 @@ function getPriceFromApi(tier: PricingTierConfig): number | null {
return currentBillingCycle.value === 'yearly' ? price / 12 : price
}
function getMaxSeatsFromApi(tier: PricingTierConfig): number | null {
const plan = getApiPlanForTier(tier.key, 'monthly')
return plan ? plan.max_seats : null
}
const currentTierKey = computed<TierKey | null>(() =>
subscription.value?.tier ? TIER_TO_KEY[subscription.value.tier] : null
)
@@ -493,8 +489,7 @@ const getAnnualTotal = (tier: PricingTierConfig): number => {
return plan ? plan.price_cents / 100 : tier.pricing.yearly * 12
}
const getMaxMembers = (tier: PricingTierConfig): number =>
getMaxSeatsFromApi(tier) ?? tier.maxMembers
const getMaxMembers = (tier: PricingTierConfig): number => getMaxSeats(tier.key)
const getMonthlyCreditsPerMember = (tier: PricingTierConfig): number =>
tier.pricing.credits

View File

@@ -88,10 +88,13 @@
</div>
<div class="flex items-baseline gap-1 font-inter font-semibold">
<span class="text-2xl">${{ tierPrice }}</span>
<span class="text-base"
>{{ $t('subscription.perMonth') }} /
{{ $t('subscription.member') }}</span
>
<span class="text-base">
{{
isInPersonalWorkspace
? $t('subscription.usdPerMonth')
: $t('subscription.usdPerMonthPerMember')
}}
</span>
</div>
<div
v-if="isActiveSubscription"
@@ -176,7 +179,7 @@
<div class="flex flex-col">
<div class="flex flex-col gap-3 h-full">
<div
class="relative flex flex-col gap-6 rounded-2xl p-5 bg-modal-panel-background justify-between h-full"
class="relative flex flex-col gap-6 rounded-2xl p-5 bg-secondary-background justify-between h-full"
>
<Button
variant="muted-textonly"
@@ -359,7 +362,6 @@ import { useSubscriptionActions } from '@/platform/cloud/subscription/composable
import { useSubscriptionCredits } from '@/platform/cloud/subscription/composables/useSubscriptionCredits'
import { workspaceApi } from '@/platform/workspace/api/workspaceApi'
import { useDialogService } from '@/services/dialogService'
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
import {
DEFAULT_TIER_KEY,
TIER_TO_KEY,
@@ -388,7 +390,7 @@ const {
manageSubscription,
fetchStatus,
fetchBalance,
plans: apiPlans
getMaxSeats
} = useBillingContext()
const { showCancelSubscriptionDialog } = useDialogService()
@@ -511,23 +513,6 @@ const tierPrice = computed(() =>
const memberCount = computed(() => members.value.length)
const nextMonthInvoice = computed(() => memberCount.value * tierPrice.value)
function getApiPlanForTier(tierKey: TierKey, duration: 'monthly' | 'yearly') {
const apiDuration = duration === 'yearly' ? 'ANNUAL' : 'MONTHLY'
const apiTier = tierKey.toUpperCase()
return apiPlans.value.find(
(p) => p.tier === apiTier && p.duration === apiDuration
)
}
function getMaxSeatsFromApi(tierKey: TierKey): number | null {
const plan = getApiPlanForTier(tierKey, 'monthly')
return plan ? plan.max_seats : null
}
function getMaxMembers(tierKey: TierKey): number {
return getMaxSeatsFromApi(tierKey) ?? getTierFeatures(tierKey).maxMembers
}
const refillsDate = computed(() => {
if (!subscription.value?.renewalDate) return ''
const date = new Date(subscription.value.renewalDate)
@@ -571,13 +556,18 @@ interface Benefit {
const tierBenefits = computed((): Benefit[] => {
const key = tierKey.value
const benefits: Benefit[] = [
{
const benefits: Benefit[] = []
if (!isInPersonalWorkspace.value) {
benefits.push({
key: 'members',
type: 'icon',
label: t('subscription.membersLabel', { count: getMaxMembers(key) }),
label: t('subscription.membersLabel', { count: getMaxSeats(key) }),
icon: 'pi pi-user'
},
})
}
benefits.push(
{
key: 'maxDuration',
type: 'metric',
@@ -594,7 +584,7 @@ const tierBenefits = computed((): Benefit[] => {
type: 'feature',
label: t('subscription.addCreditsLabel')
}
]
)
if (getTierFeatures(key).customLoRAs) {
benefits.push({

View File

@@ -11,6 +11,13 @@ export const TIER_TO_KEY: Record<SubscriptionTier, TierKey> = {
FOUNDERS_EDITION: 'founder'
}
export const KEY_TO_TIER: Record<TierKey, SubscriptionTier> = {
standard: 'STANDARD',
creator: 'CREATOR',
pro: 'PRO',
founder: 'FOUNDERS_EDITION'
}
export interface TierPricing {
monthly: number
yearly: number