Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
25 changes: 25 additions & 0 deletions browser_tests/tests/dialogs/creditsTile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ const endedPersonalBillingStatus: BillingStatusResponse = {
has_funds: true
}

const paymentFailedBillingStatus: BillingStatusResponse = {
...mockBillingStatus,
is_active: false,
billing_status: 'payment_failed'
}

async function mockCloudBoot(
page: Page,
billingControlEnabled = true,
Expand Down Expand Up @@ -276,6 +282,25 @@ test.describe('Credits tile (Plan & Credits)', { tag: '@cloud' }, () => {
.toBe('https://billing.example/portal')
})

test('keeps billing management available when payment fails', async ({
page
}) => {
test.setTimeout(60_000)

await mockCloudBoot(page, true, paymentFailedBillingStatus)

const content = await openPlanAndCredits(page)
await expect(
content.getByRole('button', { name: 'Billing & invoices' })
).toBeVisible()
await expect(
content.getByRole('button', { name: 'Change plan' })
).toHaveCount(0)

await content.getByRole('button', { name: 'More Options' }).click()
await expect(page.getByText('Cancel plan', { exact: true })).toBeVisible()
})

test('keeps the legacy Workspace UX when billing controls are disabled', async ({
page
}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import CurrentUserPopoverWorkspace from './CurrentUserPopoverWorkspace.vue'

const state = vi.hoisted(() => ({
billingStatus: 'paid',
canAccessSubscriptionFeatures: true,
isFreeTier: false,
isCancelled: false,
Expand Down Expand Up @@ -53,6 +54,7 @@

vi.mock('@/composables/billing/useBillingContext', () => ({
useBillingContext: () => ({
billingStatus: computed(() => state.billingStatus),
canAccessSubscriptionFeatures: computed(
() => state.canAccessSubscriptionFeatures
),
Expand Down Expand Up @@ -157,6 +159,7 @@

describe('CurrentUserPopoverWorkspace', () => {
beforeEach(() => {
state.billingStatus = 'paid'
state.canAccessSubscriptionFeatures = true
state.isFreeTier = false
state.isCancelled = false
Expand Down Expand Up @@ -249,6 +252,22 @@
).not.toBeInTheDocument()
})

it.each(['payment_failed', 'paused'])(

Check failure on line 255 in src/platform/workspace/components/CurrentUserPopoverWorkspace.test.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

vitest(consistent-each-for)

src/platform/workspace/components/CurrentUserPopoverWorkspace.test.ts:255:6: `it` can not be used with `.each` to create parameterized test.
'keeps Manage plan available instead of Subscribe when billing is %s',
(billingStatus) => {
state.billingStatus = billingStatus
state.canAccessSubscriptionFeatures = false
state.canManageSubscription = true

renderComponent('team')

expect(screen.getByTestId('manage-plan-menu-item')).toBeInTheDocument()
expect(
screen.queryByRole('button', { name: 'Subscribe' })
).not.toBeInTheDocument()
}
)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
it.for([
{
name: 'allows a lifecycle manager to resubscribe a cancelled plan',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
const settingsDialog = useSettingsDialog()
const dialogService = useDialogService()
const {
billingStatus,
canAccessSubscriptionFeatures,
isFreeTier,
subscription,
Expand Down Expand Up @@ -307,12 +308,16 @@ const showPlansAndPricing = computed(
const showManagePlan = computed(
() =>
permissions.value.canManageSubscription &&
canAccessSubscriptionFeatures.value
(canAccessSubscriptionFeatures.value ||
billingStatus.value === 'payment_failed' ||
billingStatus.value === 'paused')
)
const showSubscribeAction = computed(
() =>
(isCancelled.value && permissions.value.canManageSubscriptionLifecycle) ||
(!canAccessSubscriptionFeatures.value &&
billingStatus.value !== 'payment_failed' &&
billingStatus.value !== 'paused' &&
Comment thread
dante01yoon marked this conversation as resolved.
Outdated
permissions.value.canManageSubscription)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ describe('SubscriptionPanelContentWorkspace', () => {
})

it.for(['paid', 'payment_failed', 'paused'] as BillingStatus[])(
'keeps a %s personal plan visible until it is terminal',
'keeps billing access for a non-terminal %s personal plan',
(billingStatus) => {
mockIsInPersonalWorkspace.value = true
mockIsActiveSubscription.value = false
Expand All @@ -598,6 +598,12 @@ describe('SubscriptionPanelContentWorkspace', () => {
expect(
screen.queryByRole('button', { name: 'Subscribe' })
).not.toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Billing & invoices' })
).toBeInTheDocument()
expect(
screen.queryByRole('button', { name: 'Change plan' })
).not.toBeInTheDocument()
}
)

Expand Down Expand Up @@ -866,7 +872,7 @@ describe('SubscriptionPanelContentWorkspace', () => {
{ state: 'never-subscribed', hasSubscription: false, tier: 'PRO' },
{ state: 'Free', hasSubscription: true, tier: 'FREE' }
] as const)(
'hides legacy billing access from $state personal workspaces',
'keeps billing access for $state personal workspaces',
({ hasSubscription, tier }) => {
mockBillingType.value = 'legacy'
mockBillingStatus.value = 'inactive'
Expand All @@ -881,8 +887,8 @@ describe('SubscriptionPanelContentWorkspace', () => {

expect(screen.getByRole('heading', { name: 'Free' })).toBeInTheDocument()
expect(
screen.queryByRole('button', { name: 'Billing & invoices' })
).not.toBeInTheDocument()
screen.getByRole('button', { name: 'Billing & invoices' })
).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Subscribe' })
).toBeInTheDocument()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,7 @@
</div>
<div class="flex flex-wrap gap-2 md:ml-auto">
<Button
v-if="
isCloud &&
permissions.canManageSubscription &&
(billingType === 'workspace' ||
(isSubscriptionEnded &&
!isFreeTierPlan &&
subscription !== null))
"
v-if="isCloud && permissions.canManageSubscription"
size="lg"
variant="secondary"
class="rounded-lg bg-interface-menu-component-surface-selected px-4 text-sm font-normal text-text-primary"
Expand Down Expand Up @@ -242,7 +235,10 @@
</div>

<div
v-if="canAccessSubscriptionFeatures"
v-if="
canAccessSubscriptionFeatures ||
(isCloud && permissions.canManageSubscription)
"
class="flex flex-wrap gap-2 md:ml-auto"
>
<Button
Expand Down Expand Up @@ -279,6 +275,7 @@
<Button
v-else-if="
!isSubscriptionCancelled &&
canAccessSubscriptionFeatures &&
permissions.canManageSubscription
"
size="lg"
Expand Down Expand Up @@ -452,7 +449,6 @@ function openSubscriptionVerification() {
}

const {
type: billingType,
canAccessSubscriptionFeatures,
isFreeTier: isFreeTierPlan,
isTeamPlan,
Expand Down
18 changes: 18 additions & 0 deletions src/platform/workspace/composables/useWorkspaceMenuItems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { useWorkspaceMenuItems } from './useWorkspaceMenuItems'

const state = vi.hoisted(() => ({
billingStatus: 'paid',
canLeaveWorkspace: false,
canManageSubscription: false,
canManageSubscriptionLifecycle: false,
Expand All @@ -27,6 +28,7 @@

vi.mock('@/composables/billing/useBillingContext', () => ({
useBillingContext: () => ({
billingStatus: computed(() => state.billingStatus),
isFreeTier: computed(() => state.isFreeTier),
subscription: computed(() => ({ endDate: '2026-08-01T00:00:00Z' }))
})
Expand Down Expand Up @@ -66,6 +68,7 @@

describe('useWorkspaceMenuItems', () => {
beforeEach(() => {
state.billingStatus = 'paid'
state.canLeaveWorkspace = false
state.canManageSubscription = false
state.canManageSubscriptionLifecycle = false
Expand Down Expand Up @@ -113,6 +116,21 @@
)
})

it.each(['payment_failed', 'paused'])(

Check failure on line 119 in src/platform/workspace/composables/useWorkspaceMenuItems.test.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

vitest(consistent-each-for)

src/platform/workspace/composables/useWorkspaceMenuItems.test.ts:119:6: `it` can not be used with `.each` to create parameterized test.
'allows cancellation while a %s plan needs payment recovery',
(billingStatus) => {
state.billingStatus = billingStatus
state.canManageSubscriptionLifecycle = true
state.isActiveSubscription = false

const { menuItems } = useWorkspaceMenuItems()

expect(menuItems.value.map((item) => item.label)).toContain(
'subscription.cancelPlan'
)
}
)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
it('rechecks eligibility before opening the cancellation dialog', () => {
state.canManageSubscriptionLifecycle = true
const { menuItems } = useWorkspaceMenuItems()
Expand Down
6 changes: 4 additions & 2 deletions src/platform/workspace/composables/useWorkspaceMenuItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useDialogService } from '@/services/dialogService'
*/
export function useWorkspaceMenuItems() {
const { t } = useI18n()
const { isFreeTier, subscription } = useBillingContext()
const { billingStatus, isFreeTier, subscription } = useBillingContext()
const {
permissions,
uiConfig,
Expand Down Expand Up @@ -65,7 +65,9 @@ export function useWorkspaceMenuItems() {
const canCancelPlan = computed(
() =>
permissions.value.canManageSubscriptionLifecycle &&
isActiveSubscription.value &&
(isActiveSubscription.value ||
billingStatus.value === 'payment_failed' ||
billingStatus.value === 'paused') &&
!isSubscriptionCancelled.value &&
!isFreeTier.value
)
Expand Down
Loading