mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-12 10:17:08 +00:00
## Summary Answers "why did this user want to pay?" by capturing the triggering product moment at every paywall/upsell entry point and carrying it through checkout and success telemetry. ## Changes - **What**: - Widen `SubscriptionDialogReason` from 4 coarse values to 13 grounded intent sources (`subscribe_to_run`, `upgrade_to_add_credits`, `invite_member_upsell`, `settings_billing_panel`, etc.) - Fire `app:subscription_required_modal_opened` from `useSubscriptionDialog` (the choke point all dialog variants pass through) — the workspace/unified path previously emitted nothing; remove the now-duplicate emitters in `useSubscription` and `usePricingTableUrlLoader` - Add `payment_intent_source` to `BeginCheckoutMetadata`/`SubscriptionSuccessMetadata`, threaded via the existing `reason` prop: dialog → `PricingTable` → `performSubscriptionCheckout` → pending-attempt record, so legacy `app:monthly_subscription_succeeded` carries intent alongside `checkout_attempt_id` - Fire `begin_checkout` on the workspace checkout path (`useSubscriptionCheckout`, personal + team confirm) and the team deep-link util — both previously emitted nothing; `tier` widened to `TierKey | 'team'` - Implement `trackBeginCheckout` in `PostHogTelemetryProvider` (was GTM/host-only, so `begin_checkout` never reached PostHog) - Thread `showSubscriptionDialog(options)` through the billing-context adapters and pass a reason at ~14 call sites; add `source` to `app:add_api_credit_button_clicked` ## Review Focus - `modal_opened` now fires once per dialog actually shown, so a free-tier user clicking Upgrade emits two events (free-tier dialog, then pricing table) where the legacy path emitted one - Intent is threaded explicitly via props/params rather than shared state; `useSubscriptionCheckout` gained an optional second parameter
64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { createI18n } from 'vue-i18n'
|
|
|
|
import { render, screen } from '@testing-library/vue'
|
|
|
|
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
|
|
|
import type { PaymentIntentSource } from '@/platform/telemetry/types'
|
|
|
|
import FreeTierDialogContent from './FreeTierDialogContent.vue'
|
|
|
|
const mockRenewalDate = vi.hoisted(() => ({ value: null as string | null }))
|
|
|
|
vi.mock('@/composables/billing/useBillingContext', () => ({
|
|
useBillingContext: vi.fn(() => ({
|
|
renewalDate: mockRenewalDate
|
|
}))
|
|
}))
|
|
|
|
function renderComponent(props?: { reason?: PaymentIntentSource }) {
|
|
const i18n = createI18n({
|
|
legacy: false,
|
|
locale: 'en',
|
|
messages: { en: enMessages }
|
|
})
|
|
|
|
return render(FreeTierDialogContent, {
|
|
props,
|
|
global: {
|
|
plugins: [i18n]
|
|
}
|
|
})
|
|
}
|
|
|
|
describe('FreeTierDialogContent', () => {
|
|
it('renders the next refresh line formatted from the facade renewalDate', () => {
|
|
mockRenewalDate.value = '2026-07-15T10:00:00Z'
|
|
renderComponent()
|
|
expect(
|
|
screen.getByText('Your credits refresh on Jul 15, 2026.')
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('hides the next refresh line when renewalDate is null', () => {
|
|
mockRenewalDate.value = null
|
|
renderComponent()
|
|
expect(screen.queryByText(/credits refresh on/)).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('keeps the generic copy for intent reasons outside the credits variants', () => {
|
|
mockRenewalDate.value = '2026-07-15T10:00:00Z'
|
|
renderComponent({ reason: 'subscribe_to_run' })
|
|
expect(
|
|
screen.getByText('Your credits refresh on Jul 15, 2026.')
|
|
).toBeInTheDocument()
|
|
})
|
|
|
|
it('swaps to the out-of-credits copy without the refresh line', () => {
|
|
mockRenewalDate.value = '2026-07-15T10:00:00Z'
|
|
renderComponent({ reason: 'out_of_credits' })
|
|
expect(screen.queryByText(/credits refresh on/)).not.toBeInTheDocument()
|
|
})
|
|
})
|