Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 ? formatCreditCount(0) : totalCredits.value
)
const displayPrepaid = computed(() =>
zeroState || inactivePlan ? formatCreditCount(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