Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2640,6 +2640,7 @@
"endsOnDate": "Ends on {date}",
"changesToPlanOnDate": "Changes to {plan} on {date}",
"manageSubscription": "Manage subscription",
"billingAndInvoices": "Billing & invoices",
"manageBilling": "Manage billing",
"changePlan": "Change plan",
"cancelPlan": "Cancel plan",
Expand Down Expand Up @@ -2724,6 +2725,10 @@
"planLoadErrorRetry": "Try again",
"teamPlanName": "Team",
"teamPlanIncludes": "Your plan includes everything in {plan}, plus:",
"inactiveTeamTitle": "Inactive team subscription",
"inactiveTeamDescription": "Reactivate your team plan to add more members and run workflows",
"inactiveTeamPlanIncludes": "An active plan features everything in {plan}, plus:",
"reactivateToUseCredits": "Reactivate your plan to use these credits",
"teamPerks": {
"inviteMembers": "Invite members",
"concurrentRuns": "Members can run workflows concurrently",
Expand Down
13 changes: 13 additions & 0 deletions src/platform/cloud/subscription/components/CreditsTile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ const i18n = createI18n({
additionalCredits: 'Additional credits',
additionalCreditsInUse: 'In use',
usedAfterMonthly: 'Used after monthly runs out',
reactivateToUseCredits: 'Reactivate your plan to use these credits',
monthlyCreditsUsedUpTitle:
'Monthly credits are used up. Refills {date}',
monthlyCreditsUsedUpTitleNoDate: 'Monthly credits are used up',
Expand Down Expand Up @@ -283,6 +284,18 @@ describe('CreditsTile', () => {
expect(screen.queryByText('Add credits')).toBeNull()
})

it('shows disabled credit details for an inactive plan', () => {
activeProSubscription()
const { container } = renderTile({ zeroState: true, inactivePlan: true })

expect(container.textContent).toContain('0remaining')
expect(container.textContent).toContain('Additional credits')
expect(container.textContent).toContain(
'Reactivate your plan to use these credits'
)
expect(screen.queryByText('Add credits')).toBeNull()
})

it('shows only the balance with no breakdown when there is no active subscription', () => {
state.isActiveSubscription = false
state.balance = { amountMicros: 500 }
Expand Down
49 changes: 46 additions & 3 deletions src/platform/cloud/subscription/components/CreditsTile.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<template>
<div
class="@container relative flex flex-col gap-6 rounded-2xl border border-interface-stroke bg-modal-panel-background px-6 py-5"
:class="
cn(
'@container relative flex flex-col gap-6 rounded-2xl border border-interface-stroke bg-modal-panel-background px-6 py-5',
inactivePlan && 'text-muted'
)
"
>
<Button
variant="muted-textonly"
Expand All @@ -19,7 +24,14 @@
</div>
<Skeleton v-if="isLoadingBalance" width="8rem" height="2rem" />
<div v-else class="flex items-baseline gap-2">
<i class="icon-[lucide--component] size-4 self-center text-credit" />
<i
:class="
cn(
'icon-[lucide--component] size-4 self-center',
inactivePlan ? 'text-muted' : 'text-credit'
Comment thread
dante01yoon marked this conversation as resolved.
Outdated
)
"
/>
<span class="text-2xl leading-none font-bold">{{ displayTotal }}</span>
<span class="text-sm text-muted @max-[300px]:hidden">{{
$t('subscription.remaining')
Expand Down Expand Up @@ -144,6 +156,36 @@
</div>
</template>

<template v-else-if="inactivePlan">
<div class="h-px w-full bg-interface-stroke" />
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between gap-2 text-sm">
<span class="flex items-center gap-1">
{{ $t('subscription.additionalCredits') }}
<Button
v-tooltip="{
value: $t('subscription.additionalCreditsTooltip'),
showDelay: 300
}"
variant="muted-textonly"
size="icon-sm"
:aria-label="$t('subscription.additionalCreditsInfo')"
class="text-muted"
>
<i class="icon-[lucide--info] size-4" />
</Button>
</span>
<span class="flex items-center gap-1 font-bold">
<i class="icon-[lucide--component] size-4" />
{{ displayPrepaid }}
</span>
</div>
<span class="text-sm">
{{ $t('subscription.reactivateToUseCredits') }}
</span>
</div>
</template>

<div v-if="showActionButton" class="flex flex-col gap-3">
<Button
v-if="isFreeTier"
Expand Down Expand Up @@ -197,9 +239,10 @@ import { consumePendingTopup } from '@/platform/telemetry/topupTracker'
import { useWorkspaceUI } from '@/platform/workspace/composables/useWorkspaceUI'
import { useDialogService } from '@/services/dialogService'

const { zeroState = false } = defineProps<{
const { zeroState = false, inactivePlan = false } = defineProps<{
Comment thread
dante01yoon marked this conversation as resolved.
Outdated
/** Forces the zero-credit display (e.g. unsubscribed / member view). */
zeroState?: boolean
inactivePlan?: boolean
}>()

const { locale, t } = useI18n()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,9 @@ const i18n = createI18n({
})

const CreditsTileStub = {
props: ['zeroState'],
props: ['zeroState', 'inactivePlan'],
template:
'<div data-testid="credits-tile" :data-zero-state="String(zeroState)" />'
'<div data-testid="credits-tile" :data-zero-state="String(zeroState)" :data-inactive-plan="String(inactivePlan)" />'
}

const ButtonStub = {
Expand Down Expand Up @@ -538,6 +538,7 @@ describe('SubscriptionPanelContentWorkspace', () => {
mockSubscriptionStatus.value = 'ended'
mockSubscriptionTier.value = 'STANDARD'
mockPlanSlug.value = 'standard-monthly'
mockHasTeamPlan.value = false
renderComponent()

expect(
Expand Down Expand Up @@ -600,6 +601,7 @@ describe('SubscriptionPanelContentWorkspace', () => {

await user.click(screen.getByRole('button', { name: 'Reactivate plan' }))
expect(mockResubscribe).toHaveBeenCalledOnce()
expect(mockShowSubscriptionDialog).not.toHaveBeenCalled()
})

it('shows ended copy for an inactive ended subscription without a date', () => {
Expand All @@ -622,20 +624,57 @@ describe('SubscriptionPanelContentWorkspace', () => {
).toBeInTheDocument()
})

it('shows ended copy and subscribe CTA after a canceled Team plan becomes inactive', () => {
it('renders an ended Team plan for its owner and routes reactivation to checkout', async () => {
mockSubscriptionStatus.value = 'canceled'
mockIsActiveSubscription.value = false
mockIsWorkspaceSubscribed.value = false
const user = userEvent.setup()
renderComponent()

expect(screen.getByText('Your subscription has ended')).toBeInTheDocument()
expect(
screen.getByText('Your subscription is no longer active.')
screen.getByRole('heading', { name: 'Inactive team subscription' })
).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Subscribe Now' })
screen.getByText(
'Reactivate your team plan to add more members and run workflows'
)
).toBeInTheDocument()
expect(screen.queryByText(/^Ends on/i)).not.toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Billing & invoices' })
).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Reactivate plan' })
).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'More Options' })
).toBeInTheDocument()
expect(screen.getByTestId('credits-tile')).toHaveAttribute(
'data-zero-state',
'true'
)
expect(screen.getByTestId('credits-tile')).toHaveAttribute(
'data-inactive-plan',
'true'
)
expect(document.body.textContent).toContain(
'An active plan features everything in Pro, plus:'
)
expect(screen.getByText('Invite members')).toBeInTheDocument()
expect(
screen.getByText('Members can run workflows concurrently')
).toBeInTheDocument()
expect(
screen.getByText('Shared credit pool for all members')
).toBeInTheDocument()
expect(screen.getByText('Role-based permissions')).toBeInTheDocument()

await user.click(screen.getByRole('button', { name: 'Reactivate plan' }))

expect(mockShowSubscriptionDialog).toHaveBeenCalledWith({
reason: 'settings_billing_panel'
})
expect(mockResubscribe).not.toHaveBeenCalled()
})

it('does not show stale renewal copy for an explicitly ended active state', () => {
Expand Down Expand Up @@ -692,6 +731,9 @@ describe('SubscriptionPanelContentWorkspace', () => {
expect(
screen.getByRole('button', { name: 'Subscribe Now' })
).toBeInTheDocument()
expect(
screen.queryByRole('button', { name: 'Billing & invoices' })
).not.toBeInTheDocument()
expect(screen.getByTestId('credits-tile')).toHaveAttribute(
'data-zero-state',
'true'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,73 @@
<!-- OWNER Unsubscribed TEAM workspace -->
<template v-if="showTeamSubscribePrompt">
<div class="flex flex-col gap-2">
<h3 class="m-0 text-sm font-bold text-text-primary">
{{ $t('subscription.workspaceNotSubscribed') }}
<h3
:class="
cn(
'm-0 font-bold text-text-primary',
showInactiveTeamSubscription ? 'text-base' : 'text-sm'
)
"
>
{{
$t(
showInactiveTeamSubscription
? 'subscription.inactiveTeamTitle'
: 'subscription.workspaceNotSubscribed'
)
}}
</h3>
<div class="text-sm text-text-secondary">
{{ $t('subscription.subscriptionRequiredMessage') }}
{{
$t(
showInactiveTeamSubscription
? 'subscription.inactiveTeamDescription'
: 'subscription.subscriptionRequiredMessage'
)
}}
</div>
</div>
<Button
variant="primary"
size="lg"
class="ml-auto rounded-lg px-4 py-2 text-sm font-normal"
@click="handleSubscribeWorkspace"
>
{{ $t('subscription.subscribeNow') }}
</Button>
<div class="flex flex-wrap gap-2 md:ml-auto">
<Button
v-if="showInactiveTeamSubscription"
size="lg"
variant="secondary"
class="rounded-lg bg-interface-menu-component-surface-selected px-4 text-sm font-normal text-text-primary"
@click="manageSubscription"
>
{{ $t('subscription.billingAndInvoices') }}
</Button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<Button
variant="primary"
size="lg"
class="rounded-lg px-4 py-2 text-sm font-normal"
@click="handleSubscribeWorkspace"
>
{{
$t(
showInactiveTeamSubscription
? 'subscription.reactivatePlan'
: 'subscription.subscribeNow'
)
}}
</Button>
<DropdownMenu
v-if="showInactiveTeamSubscription && menuEntries.length > 0"
:entries="menuEntries"
>
<template #button>
<Button
v-tooltip="{ value: $t('g.moreOptions'), showDelay: 300 }"
variant="secondary"
size="icon-lg"
class="rounded-lg bg-interface-menu-component-surface-selected text-text-primary"
:aria-label="$t('g.moreOptions')"
>
<i class="pi pi-ellipsis-h" />
</Button>
</template>
</DropdownMenu>
</div>
</template>

<!-- MEMBER View - read-only, workspace not subscribed -->
Expand Down Expand Up @@ -235,16 +287,27 @@

<div class="flex flex-col gap-6 pt-6 lg:flex-row lg:items-stretch">
<div class="w-full lg:max-w-md">
<CreditsTile :zero-state="showZeroState" />
<CreditsTile
:zero-state="showZeroState"
:inactive-plan="showInactiveTeamSubscription"
/>
</div>

<div
v-if="isActiveSubscription || isPersonalFree"
v-if="
isActiveSubscription ||
isPersonalFree ||
showInactiveTeamSubscription
"
class="flex flex-col gap-2"
>
<i18n-t
v-if="isTeamActive"
keypath="subscription.teamPlanIncludes"
v-if="isTeamActive || showInactiveTeamSubscription"
:keypath="
showInactiveTeamSubscription
? 'subscription.inactiveTeamPlanIncludes'
: 'subscription.teamPlanIncludes'
"
tag="div"
class="text-sm text-muted"
>
Expand All @@ -269,7 +332,14 @@
>
<i
v-if="benefit.type === 'feature'"
class="pi pi-check text-xs text-text-primary"
:class="
cn(
'pi pi-check text-xs',
showInactiveTeamSubscription
? 'text-muted'
: 'text-text-primary'
)
"
/>
<span
v-else-if="benefit.type === 'metric' && benefit.value"
Expand Down Expand Up @@ -307,6 +377,7 @@
</template>

<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
Expand Down Expand Up @@ -406,6 +477,13 @@ const showTeamSubscribePrompt = computed(
() => showSubscribePrompt.value && !isInPersonalWorkspace.value
)

const showInactiveTeamSubscription = computed(
() =>
showTeamSubscribePrompt.value &&
isSubscriptionEnded.value &&
isTeamPlan.value
)

const isPersonalFree = computed(
() =>
isInPersonalWorkspace.value &&
Expand Down Expand Up @@ -542,7 +620,7 @@ const TEAM_PERK_KEYS = [
] as const

const tierBenefits = computed((): TierBenefit[] => {
if (isTeamActive.value) {
if (isTeamActive.value || showInactiveTeamSubscription.value) {
return TEAM_PERK_KEYS.map((key) => ({
key,
type: 'feature',
Expand Down
Loading