mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-07 14:09:59 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user