Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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({ 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
67 changes: 60 additions & 7 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-credit'
)
"
/>
<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 } = defineProps<{
/** Forces the zero-credit display (e.g. unsubscribed / member view). */
zeroState?: boolean
inactivePlan?: boolean
}>()

const { locale, t } = useI18n()
Expand Down Expand Up @@ -292,8 +335,12 @@ const creditPoolTotalCompact = computed(() => {
return total === null ? '—' : compactNumber.value.format(total)
})

const displayTotal = computed(() => (zeroState ? '0' : totalCredits.value))
const displayPrepaid = computed(() => (zeroState ? '0' : prepaidCredits.value))
const displayTotal = computed(() =>
zeroState || inactivePlan ? '0' : totalCredits.value
)
const displayPrepaid = computed(() =>
zeroState || inactivePlan ? '0' : prepaidCredits.value
)
Comment thread
dante01yoon marked this conversation as resolved.
const usedBarWidth = computed(
() => `${(usage.value.usedFraction * 100).toFixed(2)}%`
)
Expand All @@ -304,15 +351,21 @@ const monthlyUsageLabel = computed(() =>
})
)

const showBreakdown = computed(() => isActiveSubscription.value && !zeroState)
const showBreakdown = computed(
() => isActiveSubscription.value && !zeroState && !inactivePlan
)
const showBar = computed(
() =>
showBreakdown.value &&
creditPoolTotalCredits.value !== null &&
creditPoolTotalCredits.value > 0
)
const showActionButton = computed(
() => isActiveSubscription.value && !zeroState && permissions.value.canTopUp
() =>
isActiveSubscription.value &&
!zeroState &&
!inactivePlan &&
permissions.value.canTopUp
)

const isMonthlyDepleted = computed(
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
Loading
Loading