Skip to content

Commit 239cd04

Browse files
authored
[backport cloud/1.48] fix(workspace): show subscribe prompt for ended subscriptions (#14510)
## Summary Backport of #14504 to `cloud/1.48`. - Resolves the release-branch cherry-pick conflicts while preserving the existing cancellation behavior. - Treats `subscription_status: ended` as terminal even when paid-plan metadata is stale. - Includes component and cloud E2E regression coverage adapted to this release branch. ## Validation - 31 component tests passed - App and browser typechecks - Targeted formatting and lint checks - `knip` push hook
1 parent beb5367 commit 239cd04

5 files changed

Lines changed: 121 additions & 11 deletions

File tree

browser_tests/fixtures/data/cloudWorkspace.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import type { BillingStatusResponse as IngestBillingStatusResponse } from '@comfyorg/ingest-types'
2+
13
import type {
2-
BillingStatusResponse,
34
Member,
45
Plan,
56
WorkspaceWithRole
@@ -70,16 +71,29 @@ export const DEFAULT_TEAM_MEMBERS: Member[] = [
7071

7172
const TEAM_PLAN_SLUG = 'team-pro-monthly'
7273

73-
export const TEAM_BILLING_STATUS: BillingStatusResponse = {
74+
export const TEAM_BILLING_STATUS = {
7475
is_active: true,
7576
subscription_status: 'active',
7677
subscription_tier: 'PRO',
7778
subscription_duration: 'MONTHLY',
7879
plan_slug: TEAM_PLAN_SLUG,
7980
billing_status: 'paid',
8081
has_funds: true,
81-
renewal_date: '2099-02-20T00:00:00Z'
82-
}
82+
renewal_date: '2099-02-20T00:00:00Z',
83+
team_credit_stop: null
84+
} satisfies IngestBillingStatusResponse
85+
86+
export const ENDED_STANDARD_BILLING_STATUS = {
87+
billing_rail: 'stripe',
88+
billing_status: 'inactive',
89+
has_funds: true,
90+
is_active: false,
91+
plan_slug: 'standard-monthly',
92+
subscription_duration: 'MONTHLY',
93+
subscription_status: 'ended',
94+
subscription_tier: 'STANDARD',
95+
team_credit_stop: null
96+
} satisfies IngestBillingStatusResponse & { billing_rail: 'stripe' }
8397

8498
export const TEAM_PRO_PLAN: Plan = {
8599
slug: TEAM_PLAN_SLUG,

browser_tests/fixtures/helpers/CloudWorkspaceMockHelper.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { Page, Route } from '@playwright/test'
2+
import type { BillingStatusResponse } from '@comfyorg/ingest-types'
23

34
import type {
45
Member,
6+
Plan,
57
WorkspaceWithRole
68
} from '@/platform/workspace/api/workspaceApi'
79

@@ -45,9 +47,10 @@ export class CloudWorkspaceMockHelper {
4547

4648
async setup(
4749
members: Member[] = DEFAULT_TEAM_MEMBERS,
48-
activeWorkspace: WorkspaceWithRole = TEAM_WORKSPACE
50+
activeWorkspace: WorkspaceWithRole = TEAM_WORKSPACE,
51+
billingStatus: BillingStatusResponse = TEAM_BILLING_STATUS
4952
): Promise<MemberMockState> {
50-
const state = await this.mockBoot(members, activeWorkspace)
53+
const state = await this.mockBoot(members, activeWorkspace, billingStatus)
5154
await new CloudAuthHelper(this.page).mockAuth()
5255
await this.page.addInitScript((workspaceId) => {
5356
localStorage.setItem('Comfy.userId', 'test-user-e2e')
@@ -58,7 +61,8 @@ export class CloudWorkspaceMockHelper {
5861

5962
private async mockBoot(
6063
members: Member[],
61-
activeWorkspace: WorkspaceWithRole
64+
activeWorkspace: WorkspaceWithRole,
65+
billingStatus: BillingStatusResponse
6266
): Promise<MemberMockState> {
6367
const state: MemberMockState = {
6468
members: members.map((m) => ({ ...m })),
@@ -132,7 +136,7 @@ export class CloudWorkspaceMockHelper {
132136
)
133137

134138
await page.route('**/api/billing/status', (r) =>
135-
r.fulfill(jsonRoute(TEAM_BILLING_STATUS))
139+
r.fulfill(jsonRoute(billingStatus))
136140
)
137141
await page.route('**/api/billing/balance', (r) =>
138142
r.fulfill(
@@ -145,11 +149,24 @@ export class CloudWorkspaceMockHelper {
145149
})
146150
)
147151
)
152+
const currentPlan: Plan =
153+
billingStatus.plan_slug && billingStatus.plan_slug !== TEAM_PRO_PLAN.slug
154+
? {
155+
...TEAM_PRO_PLAN,
156+
slug: billingStatus.plan_slug,
157+
tier:
158+
billingStatus.subscription_tier === 'TEAM'
159+
? TEAM_PRO_PLAN.tier
160+
: (billingStatus.subscription_tier ?? TEAM_PRO_PLAN.tier),
161+
duration:
162+
billingStatus.subscription_duration ?? TEAM_PRO_PLAN.duration
163+
}
164+
: TEAM_PRO_PLAN
148165
await page.route('**/api/billing/plans', (r) =>
149166
r.fulfill(
150167
jsonRoute({
151-
current_plan_slug: TEAM_PRO_PLAN.slug,
152-
plans: [TEAM_PRO_PLAN]
168+
current_plan_slug: currentPlan.slug,
169+
plans: [currentPlan]
153170
})
154171
)
155172
)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { expect } from '@playwright/test'
2+
3+
import {
4+
cloudAppFixture as test,
5+
waitForCloudApp
6+
} from '@e2e/fixtures/cloudAppFixture'
7+
import {
8+
DEFAULT_TEAM_MEMBERS,
9+
ENDED_STANDARD_BILLING_STATUS,
10+
TEAM_WORKSPACE
11+
} from '@e2e/fixtures/data/cloudWorkspace'
12+
import { CloudWorkspaceMockHelper } from '@e2e/fixtures/helpers/CloudWorkspaceMockHelper'
13+
import { TestIds } from '@e2e/fixtures/selectors'
14+
15+
test.describe('Ended workspace subscription', { tag: '@cloud' }, () => {
16+
test.describe.configure({ timeout: 60_000 })
17+
18+
test.beforeEach(async ({ page }) => {
19+
await new CloudWorkspaceMockHelper(page).setup(
20+
DEFAULT_TEAM_MEMBERS,
21+
TEAM_WORKSPACE,
22+
ENDED_STANDARD_BILLING_STATUS
23+
)
24+
await page.goto(process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188')
25+
await waitForCloudApp(page)
26+
27+
await page
28+
.getByRole('button', { name: /^Settings/ })
29+
.first()
30+
.click()
31+
const dialog = page.getByTestId(TestIds.dialogs.settings)
32+
await expect(dialog).toBeVisible()
33+
await dialog
34+
.locator('nav')
35+
.getByRole('button', { name: 'Workspace', exact: true })
36+
.click()
37+
})
38+
39+
test('shows subscribe prompt instead of stale paid plan metadata', async ({
40+
page
41+
}) => {
42+
const content = page.getByTestId(TestIds.dialogs.settings).getByRole('main')
43+
44+
await expect(
45+
content.getByRole('heading', {
46+
name: 'This workspace is not on a subscription'
47+
})
48+
).toBeVisible()
49+
await expect(
50+
content.getByRole('button', { name: 'Subscribe Now' })
51+
).toBeVisible()
52+
await expect(
53+
content.getByRole('heading', { name: 'Standard' })
54+
).toHaveCount(0)
55+
})
56+
})

src/platform/workspace/components/SubscriptionPanelContentWorkspace.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import enMessages from '@/locales/en/main.json'
1010
import * as tierPricing from '@/platform/cloud/subscription/constants/tierPricing'
1111
import type {
1212
BillingStatus,
13+
BillingSubscriptionStatus,
1314
CurrentTeamCreditStop,
1415
TeamCreditStops
1516
} from '@/platform/workspace/api/workspaceApi'
@@ -54,7 +55,7 @@ const teamCreditStops: TeamCreditStops = {
5455
]
5556
}
5657

57-
const mockSubscriptionStatus = ref<'active' | 'canceled'>('active')
58+
const mockSubscriptionStatus = ref<BillingSubscriptionStatus>('active')
5859
const mockBillingStatus = ref<BillingStatus>('paid')
5960
const mockSubscriptionDuration = ref<'MONTHLY' | 'ANNUAL'>('MONTHLY')
6061
const mockHasSubscription = ref(true)
@@ -134,6 +135,7 @@ vi.mock('@/composables/billing/useBillingContext', () => ({
134135
isActiveSubscription: computed(() => mockIsActiveSubscription.value),
135136
isFreeTier: computed(() => false),
136137
billingStatus: mockBillingStatus,
138+
subscriptionStatus: mockSubscriptionStatus,
137139
isTeamPlan: mockIsTeamPlan,
138140
subscription: mockSubscription,
139141
teamCreditStops: mockTeamCreditStops,
@@ -472,6 +474,25 @@ describe('SubscriptionPanelContentWorkspace', () => {
472474
).not.toBeInTheDocument()
473475
})
474476

477+
it('shows subscribe prompt for an ended Standard plan in a Team workspace', () => {
478+
mockSubscriptionStatus.value = 'ended'
479+
mockSubscriptionTier.value = 'STANDARD'
480+
mockPlanSlug.value = 'standard-monthly'
481+
renderComponent()
482+
483+
expect(
484+
screen.getByRole('heading', {
485+
name: 'This workspace is not on a subscription'
486+
})
487+
).toBeInTheDocument()
488+
expect(
489+
screen.getByRole('button', { name: 'Subscribe Now' })
490+
).toBeInTheDocument()
491+
expect(
492+
screen.queryByRole('heading', { name: 'Standard' })
493+
).not.toBeInTheDocument()
494+
})
495+
475496
it.for(['paid', 'payment_failed', 'paused'] as BillingStatus[])(
476497
'keeps a %s personal plan visible until it is terminal',
477498
(billingStatus) => {

src/platform/workspace/components/SubscriptionPanelContentWorkspace.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ const {
377377
isTeamPlan,
378378
subscription,
379379
billingStatus,
380+
subscriptionStatus,
380381
isLoading,
381382
error,
382383
showSubscriptionDialog,
@@ -401,6 +402,7 @@ const isTerminalPersonalSubscription = computed(
401402
// stays active until its end date, so it keeps the subscribed treatment.
402403
const showSubscribePrompt = computed(() => {
403404
if (!permissions.value.canManageSubscription) return false
405+
if (subscriptionStatus.value === 'ended') return true
404406
if (isTerminalPersonalSubscription.value) return true
405407
if (isSubscriptionCancelled.value) return false
406408
if (

0 commit comments

Comments
 (0)