From adbfb8376727ed002a0420ce05641c3166c41dc9 Mon Sep 17 00:00:00 2001 From: Hunter Date: Sun, 8 Feb 2026 23:49:20 -0800 Subject: [PATCH 1/3] feat: wire renewal_date from cloud billing status (#8754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Wire `renewal_date` from the cloud `/billing/status` response into the workspace subscription UI so users can see when their subscription renews. ## Problem The workspace billing adapter hardcoded `renewalDate: null` because the cloud billing status endpoint didn't return a renewal date. The `SubscriptionPanelContentWorkspace` component already has UI for displaying it — it just had no data. Personal Workspace (existing `cloud-subscription-status`): Screenshot 2026-02-08 at 7 54 53 PM Current missing data from `/billing/status`: Screenshot 2026-02-08 at 7 55 38 PM ## Solution - Add `renewal_date?: string` to `BillingStatusResponse` interface - Use `status.renewal_date ?? null` instead of hardcoded `null` in `useWorkspaceBilling` ### Related - Cloud PR: Comfy-Org/cloud#2370 (adds `renewal_date` to the endpoint) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8754-feat-wire-renewal_date-from-cloud-billing-status-3026d73d365081c7ae51d79ef0633a1d) by [Unito](https://www.unito.io) --- src/composables/billing/useWorkspaceBilling.ts | 2 +- src/platform/workspace/api/workspaceApi.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/composables/billing/useWorkspaceBilling.ts b/src/composables/billing/useWorkspaceBilling.ts index def368e17..368e80277 100644 --- a/src/composables/billing/useWorkspaceBilling.ts +++ b/src/composables/billing/useWorkspaceBilling.ts @@ -47,7 +47,7 @@ export function useWorkspaceBilling(): BillingState & BillingActions { tier: status.subscription_tier ?? null, duration: status.subscription_duration ?? null, planSlug: status.plan_slug ?? null, - renewalDate: null, // Workspace billing uses cancel_at for end date + renewalDate: status.renewal_date ?? null, endDate: status.cancel_at ?? null, isCancelled: status.subscription_status === 'canceled', hasFunds: status.has_funds diff --git a/src/platform/workspace/api/workspaceApi.ts b/src/platform/workspace/api/workspaceApi.ts index 624d6d118..caf07db40 100644 --- a/src/platform/workspace/api/workspaceApi.ts +++ b/src/platform/workspace/api/workspaceApi.ts @@ -211,6 +211,7 @@ export interface BillingStatusResponse { billing_status?: BillingStatus has_funds: boolean cancel_at?: string + renewal_date?: string } export interface BillingBalanceResponse { From 815be49112ae493561ba6d3705964ef1a6fb4a0e Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 9 Feb 2026 02:01:23 -0800 Subject: [PATCH 2/3] fix: keep begin_checkout user_id reactive in subscription flows (#8726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Use reactive `userId` reads for `begin_checkout` telemetry so delayed auth state updates are reflected at event time instead of using a stale snapshot. ## Changes - **What**: switched subscription checkout telemetry paths to `storeToRefs(useFirebaseAuthStore())` and read `userId.value` when dispatching `trackBeginCheckout`. - **What**: added regression tests that mutate `userId` after setup / after checkout starts and assert telemetry uses the updated ID. ## Review Focus - Verify `PricingTable` and `performSubscriptionCheckout` still emit exactly one `begin_checkout` event per action, with `checkout_type: change` and `checkout_type: new` in their respective paths. - Verify the new tests would fail with stale store destructuring (manually validated during development). ## Screenshots (if applicable) N/A ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8726-fix-keep-begin_checkout-user_id-reactive-in-subscription-flows-3006d73d365081888c84c0335ab52e09) by [Unito](https://www.unito.io) --- .../components/PricingTable.test.ts | 40 ++++++++++++-- .../subscription/components/PricingTable.vue | 7 ++- .../utils/subscriptionCheckoutUtil.test.ts | 55 ++++++++++++++++--- .../utils/subscriptionCheckoutUtil.ts | 11 ++-- 4 files changed, 94 insertions(+), 19 deletions(-) diff --git a/src/platform/cloud/subscription/components/PricingTable.test.ts b/src/platform/cloud/subscription/components/PricingTable.test.ts index f7ac0aecd..1d24c7d13 100644 --- a/src/platform/cloud/subscription/components/PricingTable.test.ts +++ b/src/platform/cloud/subscription/components/PricingTable.test.ts @@ -1,7 +1,7 @@ import { createTestingPinia } from '@pinia/testing' import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { computed, ref } from 'vue' +import { computed, reactive, ref } from 'vue' import { createI18n } from 'vue-i18n' import PricingTable from '@/platform/cloud/subscription/components/PricingTable.vue' @@ -15,6 +15,7 @@ const mockIsYearlySubscription = ref(false) const mockAccessBillingPortal = vi.fn() const mockReportError = vi.fn() const mockTrackBeginCheckout = vi.fn() +const mockUserId = ref('user-123') const mockGetFirebaseAuthHeader = vi.fn(() => Promise.resolve({ Authorization: 'Bearer test-token' }) ) @@ -55,10 +56,11 @@ vi.mock('@/composables/useErrorHandling', () => ({ })) vi.mock('@/stores/firebaseAuthStore', () => ({ - useFirebaseAuthStore: () => ({ - getFirebaseAuthHeader: mockGetFirebaseAuthHeader, - userId: 'user-123' - }), + useFirebaseAuthStore: () => + reactive({ + getFirebaseAuthHeader: mockGetFirebaseAuthHeader, + userId: computed(() => mockUserId.value) + }), FirebaseAuthStoreError: class extends Error {} })) @@ -151,6 +153,7 @@ describe('PricingTable', () => { mockIsActiveSubscription.value = false mockSubscriptionTier.value = null mockIsYearlySubscription.value = false + mockUserId.value = 'user-123' mockTrackBeginCheckout.mockReset() vi.mocked(global.fetch).mockResolvedValue({ ok: true, @@ -201,6 +204,33 @@ describe('PricingTable', () => { expect(mockAccessBillingPortal).toHaveBeenCalledWith('pro-yearly') }) + it('should use the latest userId value when it changes after mount', async () => { + mockIsActiveSubscription.value = true + mockSubscriptionTier.value = 'STANDARD' + mockUserId.value = 'user-early' + + const wrapper = createWrapper() + await flushPromises() + + mockUserId.value = 'user-late' + + const creatorButton = wrapper + .findAll('button') + .find((btn) => btn.text().includes('Creator')) + + await creatorButton?.trigger('click') + await flushPromises() + + expect(mockTrackBeginCheckout).toHaveBeenCalledTimes(1) + expect(mockTrackBeginCheckout).toHaveBeenCalledWith({ + user_id: 'user-late', + tier: 'creator', + cycle: 'yearly', + checkout_type: 'change', + previous_tier: 'standard' + }) + }) + it('should not call accessBillingPortal when clicking current plan', async () => { mockIsActiveSubscription.value = true mockSubscriptionTier.value = 'CREATOR' diff --git a/src/platform/cloud/subscription/components/PricingTable.vue b/src/platform/cloud/subscription/components/PricingTable.vue index 0e4df0a81..3e4e8a17d 100644 --- a/src/platform/cloud/subscription/components/PricingTable.vue +++ b/src/platform/cloud/subscription/components/PricingTable.vue @@ -243,6 +243,7 @@