Compare commits

...

2 Commits

Author SHA1 Message Date
GitHub Action
06a4e63804 [automated] Apply ESLint and Oxfmt fixes 2026-02-28 04:29:14 +00:00
Hunter Senft-Grupp
d73363fc29 feat: comprehensive subscription funnel telemetry
- Add modal_name property to subscription modal events (free_tier_upsell vs pricing_table)
- Track subscribe_clicked when user proceeds from FreeTierDialog to PricingTable
- Fire second modal_opened impression when PricingTable opens in two-step flow
- Add current_tier to run_button_clicked and add_api_credit_button_clicked events
- Add previous_tier to workspace begin_checkout event
- Add entry_point tracking to settings upgrade and popover entry points
- Wire up workspace topup success tracking in billingOperationStore
- Add subscription_succeeded tracking to legacy dialog polling
- Fix TopbarSubscribeButton to fire subscribe_clicked instead of run_button_clicked
- Remove redundant modal_opened from useSubscription (now centralized in useSubscriptionDialog)
2026-02-27 23:26:45 -05:00
23 changed files with 274 additions and 72 deletions

View File

@@ -1,19 +1,16 @@
<template>
<component
:is="currentButton"
:key="isActiveSubscription ? 'queue' : 'subscribe'"
/>
<div class="relative">
<ComfyQueueButton />
<SubscribeToRunButton
v-if="!isActiveSubscription"
class="absolute inset-0 z-10"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import ComfyQueueButton from '@/components/actionbar/ComfyRunButton/ComfyQueueButton.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
const { isActiveSubscription } = useBillingContext()
const currentButton = computed(() =>
isActiveSubscription.value ? ComfyQueueButton : SubscribeToRunButton
)
</script>

View File

@@ -137,7 +137,7 @@ const authStore = useFirebaseAuthStore()
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const telemetry = useTelemetry()
const { isActiveSubscription } = useBillingContext()
const { isActiveSubscription, subscription } = useBillingContext()
const loading = computed(() => authStore.loading)
const balanceLoading = computed(() => authStore.isFetchingBalance)
@@ -160,7 +160,9 @@ watch(
const handlePurchaseCreditsClick = () => {
// Track purchase credits entry from Settings > Credits panel
useTelemetry()?.trackAddApiCreditButtonClicked()
useTelemetry()?.trackAddApiCreditButtonClicked({
current_tier: subscription.value?.tier?.toLowerCase()
})
dialogService.showTopUpCreditsDialog()
}

View File

@@ -18,6 +18,7 @@
>
<WorkflowTabs />
<TopbarBadges />
<TopbarSubscribeButton />
</div>
</div>
</template>
@@ -140,6 +141,7 @@ import NodePropertiesPanel from '@/components/rightSidePanel/RightSidePanel.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
import TopbarSubscribeButton from '@/components/topbar/TopbarSubscribeButton.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'

View File

@@ -27,6 +27,20 @@
>
{{ subscriptionTierName }}
</span>
<Button
v-if="isFreeTier"
variant="primary"
size="sm"
class="mt-2 whitespace-nowrap"
:style="{
background: 'var(--color-subscription-button-gradient)',
color: 'var(--color-white)',
borderColor: 'transparent'
}"
@click="handleSubscribeForMore"
>
{{ $t('subscription.subscribeForMore') }}
</Button>
</div>
<!-- Credits Section -->
@@ -170,6 +184,7 @@ const settingsDialog = useSettingsDialog()
const dialogService = useDialogService()
const {
isActiveSubscription,
isFreeTier,
subscriptionTierName,
subscriptionTier,
fetchStatus
@@ -208,7 +223,9 @@ const handleOpenUserSettings = () => {
}
const handleOpenPlansAndPricing = () => {
subscriptionDialog.showPricingTable()
subscriptionDialog.showPricingTable({
entry_point: 'popover_plans_and_pricing'
})
emit('close')
}
@@ -224,7 +241,9 @@ const handleOpenPlanAndCreditsSettings = () => {
const handleTopUp = () => {
// Track purchase credits entry from avatar popover
useTelemetry()?.trackAddApiCreditButtonClicked()
useTelemetry()?.trackAddApiCreditButtonClicked({
current_tier: subscriptionTier.value?.toLowerCase()
})
dialogService.showTopUpCreditsDialog()
emit('close')
}
@@ -242,6 +261,11 @@ const handleLogout = async () => {
emit('close')
}
const handleSubscribeForMore = () => {
subscriptionDialog.show({ entry_point: 'popover_upgrade' })
emit('close')
}
const handleSubscribed = async () => {
await fetchStatus()
}

View File

@@ -0,0 +1,35 @@
<template>
<Button
v-if="isFreeTier"
class="mr-2 shrink-0 whitespace-nowrap"
variant="primary"
size="sm"
:style="{
background: 'var(--color-subscription-button-gradient)',
color: 'var(--color-white)',
borderColor: 'transparent'
}"
data-testid="topbar-subscribe-button"
@click="handleClick"
>
{{ $t('subscription.subscribeForMore') }}
</Button>
</template>
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
const { isFreeTier, showSubscriptionDialog } = useBillingContext()
function handleClick() {
if (isCloud) {
useTelemetry()?.trackSubscription('subscribe_clicked', {
current_tier: 'free'
})
}
showSubscriptionDialog()
}
</script>

View File

@@ -71,7 +71,8 @@ import { useDialogStore } from '@/stores/dialogStore'
const moveSelectedNodesVersionAdded = '1.22.2'
export function useCoreCommands(): ComfyCommand[] {
const { isActiveSubscription, showSubscriptionDialog } = useBillingContext()
const { isActiveSubscription, showSubscriptionDialog, subscription } =
useBillingContext()
const workflowService = useWorkflowService()
const workflowStore = useWorkflowStore()
const settingsDialog = useSettingsDialog()
@@ -495,7 +496,10 @@ export function useCoreCommands(): ComfyCommand[] {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton(metadata)
useTelemetry()?.trackRunButton({
...metadata,
current_tier: subscription.value?.tier?.toLowerCase()
})
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return
@@ -518,7 +522,10 @@ export function useCoreCommands(): ComfyCommand[] {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton(metadata)
useTelemetry()?.trackRunButton({
...metadata,
current_tier: subscription.value?.tier?.toLowerCase()
})
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return
@@ -540,7 +547,10 @@ export function useCoreCommands(): ComfyCommand[] {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton(metadata)
useTelemetry()?.trackRunButton({
...metadata,
current_tier: subscription.value?.tier?.toLowerCase()
})
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return

View File

@@ -2274,6 +2274,7 @@
},
"subscribeToRun": "Subscribe",
"subscribeToRunFull": "Subscribe to Run",
"subscribeForMore": "Upgrade",
"subscribeNow": "Subscribe Now",
"subscribeToComfyCloud": "Subscribe to Comfy Cloud",
"workspaceNotSubscribed": "This workspace is not on a subscription",

View File

@@ -40,11 +40,14 @@ const buttonLabel = computed(() =>
: t('subscription.subscribeToRun')
)
const { showSubscriptionDialog } = useBillingContext()
const { showSubscriptionDialog, subscription } = useBillingContext()
const handleSubscribeToRun = () => {
if (isCloud) {
useTelemetry()?.trackRunButton({ subscribe_to_run: true })
useTelemetry()?.trackRunButton({
subscribe_to_run: true,
current_tier: subscription.value?.tier?.toLowerCase()
})
}
showSubscriptionDialog()

View File

@@ -48,7 +48,7 @@
v-if="isActiveSubscription"
variant="primary"
class="rounded-lg px-4 py-2 text-sm font-normal text-text-primary"
@click="showSubscriptionDialog"
@click="handleUpgradePlan"
>
{{ $t('subscription.upgradePlan') }}
</Button>
@@ -234,7 +234,11 @@ const {
isYearlySubscription
} = useSubscription()
const { show: showSubscriptionDialog } = useSubscriptionDialog()
const { showPricingTable } = useSubscriptionDialog()
function handleUpgradePlan() {
showPricingTable({ entry_point: 'settings_upgrade_plan' })
}
const tierKey = computed(() => {
const tier = subscriptionTier.value

View File

@@ -153,7 +153,7 @@ const emit = defineEmits<{
close: [subscribed: boolean]
}>()
const { fetchStatus, isActiveSubscription } = useBillingContext()
const { fetchStatus, isActiveSubscription, subscription } = useBillingContext()
const isSubscriptionEnabled = (): boolean =>
Boolean(isCloud && window.__CONFIG__?.subscription_required)
@@ -236,6 +236,10 @@ watch(
() => isActiveSubscription.value,
(isActive) => {
if (isActive && showCustomPricingTable.value) {
telemetry?.trackSubscriptionSucceeded({
tier: subscription.value?.tier?.toLowerCase(),
duration: subscription.value?.duration?.toLowerCase()
})
emit('close', true)
}
}

View File

@@ -136,13 +136,7 @@ function useSubscriptionInternal() {
const showSubscriptionDialog = (options?: {
reason?: SubscriptionDialogReason
}) => {
if (isCloud) {
useTelemetry()?.trackSubscription('modal_opened', {
current_tier: subscriptionTier.value?.toLowerCase(),
reason: options?.reason
})
}
// modal_opened tracking is handled by useSubscriptionDialog.show()/showPricingTable()
void showSubscriptionRequiredDialog(options)
}

View File

@@ -15,7 +15,7 @@ export function useSubscriptionActions() {
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const telemetry = useTelemetry()
const { fetchStatus } = useBillingContext()
const { fetchStatus, subscription } = useBillingContext()
const isLoadingSupport = ref(false)
@@ -24,6 +24,11 @@ export function useSubscriptionActions() {
})
const handleAddApiCredits = () => {
if (isCloud) {
telemetry?.trackAddApiCreditButtonClicked({
current_tier: subscription.value?.tier?.toLowerCase()
})
}
void dialogService.showTopUpCreditsDialog()
}

View File

@@ -2,6 +2,8 @@ import { defineAsyncComponent } from 'vue'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
@@ -13,19 +15,38 @@ export type SubscriptionDialogReason =
| 'out_of_credits'
| 'top_up_blocked'
interface SubscriptionDialogOptions {
reason?: SubscriptionDialogReason
entry_point?: string
}
export const useSubscriptionDialog = () => {
const { flags } = useFeatureFlags()
const dialogService = useDialogService()
const dialogStore = useDialogStore()
const workspaceStore = useTeamWorkspaceStore()
const { isFreeTier } = useSubscription()
const { isFreeTier, subscriptionTier } = useSubscription()
function hide() {
dialogStore.closeDialog({ key: DIALOG_KEY })
dialogStore.closeDialog({ key: FREE_TIER_DIALOG_KEY })
}
function showPricingTable(options?: { reason?: SubscriptionDialogReason }) {
function trackModalOpened(
modalName: 'free_tier_upsell' | 'pricing_table',
options?: SubscriptionDialogOptions
) {
if (isCloud) {
useTelemetry()?.trackSubscription('modal_opened', {
current_tier: subscriptionTier.value?.toLowerCase(),
reason: options?.reason,
entry_point: options?.entry_point,
modal_name: modalName
})
}
}
function openPricingDialog(options?: SubscriptionDialogOptions) {
const useWorkspaceVariant =
flags.teamWorkspacesEnabled && !workspaceStore.isInPersonalWorkspace
@@ -61,8 +82,17 @@ export const useSubscriptionDialog = () => {
})
}
function show(options?: { reason?: SubscriptionDialogReason }) {
if (isFreeTier.value && workspaceStore.isInPersonalWorkspace) {
function showPricingTable(options?: SubscriptionDialogOptions) {
trackModalOpened('pricing_table', options)
openPricingDialog(options)
}
function show(options?: SubscriptionDialogOptions) {
const isPersonalContext =
workspaceStore.isInPersonalWorkspace || !flags.teamWorkspacesEnabled
if (isFreeTier.value && isPersonalContext) {
trackModalOpened('free_tier_upsell', options)
const component = defineAsyncComponent(
() =>
import('@/platform/cloud/subscription/components/FreeTierDialogContent.vue')
@@ -75,6 +105,13 @@ export const useSubscriptionDialog = () => {
reason: options?.reason,
onClose: hide,
onUpgrade: () => {
if (isCloud) {
useTelemetry()?.trackSubscription('subscribe_clicked', {
current_tier: subscriptionTier.value?.toLowerCase(),
reason: options?.reason,
entry_point: options?.entry_point
})
}
hide()
showPricingTable(options)
}

View File

@@ -16,6 +16,7 @@ import type {
PageVisibilityMetadata,
SettingChangedMetadata,
SubscriptionMetadata,
SubscriptionSucceededMetadata,
SurveyResponses,
TabCountMetadata,
TelemetryDispatcher,
@@ -77,16 +78,18 @@ export class TelemetryRegistry implements TelemetryDispatcher {
this.dispatch((provider) => provider.trackBeginCheckout?.(metadata))
}
trackMonthlySubscriptionSucceeded(): void {
this.dispatch((provider) => provider.trackMonthlySubscriptionSucceeded?.())
trackSubscriptionSucceeded(metadata?: SubscriptionSucceededMetadata): void {
this.dispatch((provider) => provider.trackSubscriptionSucceeded?.(metadata))
}
trackMonthlySubscriptionCancelled(): void {
this.dispatch((provider) => provider.trackMonthlySubscriptionCancelled?.())
}
trackAddApiCreditButtonClicked(): void {
this.dispatch((provider) => provider.trackAddApiCreditButtonClicked?.())
trackAddApiCreditButtonClicked(metadata?: { current_tier?: string }): void {
this.dispatch((provider) =>
provider.trackAddApiCreditButtonClicked?.(metadata)
)
}
trackApiCreditTopupButtonPurchaseClicked(amount: number): void {
@@ -102,6 +105,7 @@ export class TelemetryRegistry implements TelemetryDispatcher {
trackRunButton(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
current_tier?: string
}): void {
this.dispatch((provider) => provider.trackRunButton?.(options))
}

View File

@@ -36,6 +36,7 @@ import type {
RunButtonProperties,
SettingChangedMetadata,
SubscriptionMetadata,
SubscriptionSucceededMetadata,
SurveyResponses,
TabCountMetadata,
TelemetryEventName,
@@ -235,12 +236,12 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
this.trackEvent(eventName, metadata)
}
trackAddApiCreditButtonClicked(): void {
this.trackEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED)
trackAddApiCreditButtonClicked(metadata?: { current_tier?: string }): void {
this.trackEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED, metadata)
}
trackMonthlySubscriptionSucceeded(): void {
this.trackEvent(TelemetryEvents.MONTHLY_SUBSCRIPTION_SUCCEEDED)
trackSubscriptionSucceeded(metadata?: SubscriptionSucceededMetadata): void {
this.trackEvent(TelemetryEvents.SUBSCRIPTION_SUCCEEDED, metadata)
}
/**
@@ -281,6 +282,7 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
trackRunButton(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
current_tier?: string
}): void {
const executionContext = this.getExecutionContext()
@@ -295,7 +297,8 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
api_node_names: executionContext.api_node_names,
has_toolkit_nodes: executionContext.has_toolkit_nodes,
toolkit_node_names: executionContext.toolkit_node_names,
trigger_source: options?.trigger_source
trigger_source: options?.trigger_source,
current_tier: options?.current_tier
}
this.lastTriggerSource = options?.trigger_source

View File

@@ -63,6 +63,7 @@ export interface RunButtonProperties {
has_toolkit_nodes: boolean
toolkit_node_names: string[]
trigger_source?: ExecutionTriggerSource
current_tier?: string
}
/**
@@ -305,6 +306,13 @@ export interface CheckoutAttributionMetadata {
export interface SubscriptionMetadata {
current_tier?: string
reason?: SubscriptionDialogReason
entry_point?: string
modal_name?: 'free_tier_upsell' | 'pricing_table'
}
export interface SubscriptionSucceededMetadata {
tier?: string
duration?: string
}
export interface BeginCheckoutMetadata
@@ -332,14 +340,15 @@ export interface TelemetryProvider {
metadata?: SubscriptionMetadata
): void
trackBeginCheckout?(metadata: BeginCheckoutMetadata): void
trackMonthlySubscriptionSucceeded?(): void
trackSubscriptionSucceeded?(metadata?: SubscriptionSucceededMetadata): void
trackMonthlySubscriptionCancelled?(): void
trackAddApiCreditButtonClicked?(): void
trackAddApiCreditButtonClicked?(metadata?: { current_tier?: string }): void
trackApiCreditTopupButtonPurchaseClicked?(amount: number): void
trackApiCreditTopupSucceeded?(): void
trackRunButton?(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
current_tier?: string
}): void
// Credit top-up tracking (composition with internal utilities)
@@ -423,7 +432,7 @@ export const TelemetryEvents = {
RUN_BUTTON_CLICKED: 'app:run_button_click',
SUBSCRIPTION_REQUIRED_MODAL_OPENED: 'app:subscription_required_modal_opened',
SUBSCRIBE_NOW_BUTTON_CLICKED: 'app:subscribe_now_button_clicked',
MONTHLY_SUBSCRIPTION_SUCCEEDED: 'app:monthly_subscription_succeeded',
SUBSCRIPTION_SUCCEEDED: 'app:subscription_succeeded',
MONTHLY_SUBSCRIPTION_CANCELLED: 'app:monthly_subscription_cancelled',
ADD_API_CREDIT_BUTTON_CLICKED: 'app:add_api_credit_button_clicked',
API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED:
@@ -522,3 +531,4 @@ export type TelemetryEventProperties =
| WorkflowCreatedMetadata
| EnterLinearMetadata
| SubscriptionMetadata
| SubscriptionSucceededMetadata

View File

@@ -296,7 +296,9 @@ const handleOpenWorkspaceSettings = () => {
}
const handleOpenPlansAndPricing = () => {
subscriptionDialog.showPricingTable()
subscriptionDialog.showPricingTable({
entry_point: 'popover_plans_and_pricing'
})
emit('close')
}
@@ -312,7 +314,9 @@ const handleOpenPlanAndCreditsSettings = () => {
const handleTopUp = () => {
// Track purchase credits entry from avatar popover
useTelemetry()?.trackAddApiCreditButtonClicked()
useTelemetry()?.trackAddApiCreditButtonClicked({
current_tier: subscription.value?.tier?.toLowerCase()
})
dialogService.showTopUpCreditsDialog()
emit('close')
}

View File

@@ -462,7 +462,12 @@ function handleSubscribeWorkspace() {
}
function handleUpgrade() {
isFreeTierPlan.value ? showPricingTable() : showSubscriptionDialog()
const options = { entry_point: 'settings_upgrade_plan' }
if (isFreeTierPlan.value) {
showPricingTable(options)
} else {
showSubscriptionDialog()
}
}
const subscriptionTier = computed(() => subscription.value?.tier ?? null)
const isYearlySubscription = computed(

View File

@@ -78,9 +78,13 @@ import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { TIER_TO_KEY } from '@/platform/cloud/subscription/constants/tierPricing'
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
import type { BillingCycle } from '@/platform/cloud/subscription/utils/subscriptionTierRank'
import { useTelemetry } from '@/platform/telemetry'
import { getCheckoutAttribution } from '@/platform/telemetry/utils/checkoutAttribution'
import type { PreviewSubscribeResponse } from '@/platform/workspace/api/workspaceApi'
import { workspaceApi } from '@/platform/workspace/api/workspaceApi'
import { useBillingOperationStore } from '@/platform/workspace/stores/billingOperationStore'
@@ -104,9 +108,17 @@ const emit = defineEmits<{
const { t } = useI18n()
const toast = useToast()
const { subscribe, previewSubscribe, plans, fetchStatus, fetchBalance } =
useBillingContext()
const {
subscribe,
previewSubscribe,
plans,
fetchStatus,
fetchBalance,
subscription
} = useBillingContext()
const telemetry = useTelemetry()
const { resolvedUserInfo } = useCurrentUser()
const billingOperationStore = useBillingOperationStore()
const isPolling = computed(() => billingOperationStore.hasPendingOperations)
@@ -184,6 +196,22 @@ async function handleSubscribeClick(payload: {
}
}
async function trackCheckout(checkoutType: 'new' | 'change') {
if (!resolvedUserInfo.value?.id || !selectedTierKey.value) return
const attribution = await getCheckoutAttribution()
telemetry?.trackBeginCheckout({
user_id: resolvedUserInfo.value.id,
tier: selectedTierKey.value,
cycle: selectedBillingCycle.value,
checkout_type: checkoutType,
...(subscription.value?.tier
? { previous_tier: TIER_TO_KEY[subscription.value.tier] }
: {}),
...attribution
})
}
function handleBackToPricing() {
checkoutStep.value = 'pricing'
previewData.value = null
@@ -194,6 +222,8 @@ async function handleAddCreditCard() {
isSubscribing.value = true
try {
await trackCheckout('new')
const planSlug = getApiPlanSlug(
selectedTierKey.value,
selectedBillingCycle.value
@@ -208,6 +238,10 @@ async function handleAddCreditCard() {
if (!response) return
if (response.status === 'subscribed') {
telemetry?.trackSubscriptionSucceeded({
tier: selectedTierKey.value ?? undefined,
duration: selectedBillingCycle.value === 'yearly' ? 'annual' : 'monthly'
})
toast.add({
severity: 'success',
summary: t('subscription.required.pollingSuccess'),
@@ -249,6 +283,8 @@ async function handleConfirmTransition() {
isSubscribing.value = true
try {
await trackCheckout('change')
const planSlug = getApiPlanSlug(
selectedTierKey.value,
selectedBillingCycle.value
@@ -263,6 +299,10 @@ async function handleConfirmTransition() {
if (!response) return
if (response.status === 'subscribed') {
telemetry?.trackSubscriptionSucceeded({
tier: selectedTierKey.value ?? undefined,
duration: selectedBillingCycle.value === 'yearly' ? 'annual' : 'monthly'
})
toast.add({
severity: 'success',
summary: t('subscription.required.pollingSuccess'),

View File

@@ -259,6 +259,7 @@ async function handleBuy() {
const response = await workspaceApi.createTopup(amountCents)
if (response.status === 'completed') {
telemetry?.trackApiCreditTopupSucceeded()
toast.add({
severity: 'success',
summary: t('credits.topUp.purchaseSuccess'),

View File

@@ -4,6 +4,7 @@ import { computed, ref } from 'vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { t } from '@/i18n'
import { useTelemetry } from '@/platform/telemetry'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { workspaceApi } from '@/platform/workspace/api/workspaceApi'
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
@@ -139,6 +140,15 @@ export const useBillingOperationStore = defineStore('billingOperation', () => {
billingContext.fetchBalance()
])
if (operation.type === 'subscription') {
useTelemetry()?.trackSubscriptionSucceeded({
tier: billingContext.subscription.value?.tier?.toLowerCase(),
duration: billingContext.subscription.value?.duration?.toLowerCase()
})
} else if (operation.type === 'topup') {
useTelemetry()?.trackApiCreditTopupSucceeded()
}
// Close any open billing dialogs and show settings
const dialogStore = useDialogStore()
dialogStore.closeDialog({ key: 'subscription-required' })

View File

@@ -185,26 +185,31 @@ defineExpose({ runButtonClick })
root-class="text-base-foreground grid-cols-[auto_96px]"
class="*:[.min-w-0]:w-24"
/>
<SubscribeToRunButton v-if="!isActiveSubscription" class="w-full mt-4" />
<div v-else class="flex mt-4 gap-2">
<Button
variant="primary"
class="grow-1"
size="lg"
@click="runButtonClick"
>
<i class="icon-[lucide--play]" />
{{ t('menu.run') }}
</Button>
<Button
v-if="!executionStore.isIdle"
variant="destructive"
size="lg"
class="w-10 p-2"
@click="commandStore.execute('Comfy.Interrupt')"
>
<i class="icon-[lucide--x]" />
</Button>
<div class="relative mt-4">
<div class="flex gap-2">
<Button
variant="primary"
class="grow-1"
size="lg"
@click="runButtonClick"
>
<i class="icon-[lucide--play]" />
{{ t('menu.run') }}
</Button>
<Button
v-if="!executionStore.isIdle"
variant="destructive"
size="lg"
class="w-10 p-2"
@click="commandStore.execute('Comfy.Interrupt')"
>
<i class="icon-[lucide--x]" />
</Button>
</div>
<SubscribeToRunButton
v-if="!isActiveSubscription"
class="absolute inset-0 w-full z-10"
/>
</div>
</section>
<section

View File

@@ -11,6 +11,7 @@ import AppModeToolbar from '@/components/appMode/AppModeToolbar.vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
import TopbarSubscribeButton from '@/components/topbar/TopbarSubscribeButton.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -82,6 +83,7 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
<div class="flex h-full items-center">
<WorkflowTabs />
<TopbarBadges />
<TopbarSubscribeButton />
</div>
</div>
<div