mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Compare commits
4 Commits
backport/c
...
backport/6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d9a566a13 | ||
|
|
cdc0bb591a | ||
|
|
d6ef19eedb | ||
|
|
9a3e387d36 |
@@ -101,12 +101,14 @@ import {
|
||||
EventType,
|
||||
useCustomerEventsService
|
||||
} from '@/services/customerEventsService'
|
||||
import { useTopupTrackerStore } from '@/stores/topupTrackerStore'
|
||||
|
||||
const events = ref<AuditLog[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const customerEventService = useCustomerEventsService()
|
||||
const topupTracker = useTopupTrackerStore()
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
@@ -159,6 +161,8 @@ const loadEvents = async () => {
|
||||
if (response.totalPages) {
|
||||
pagination.value.totalPages = response.totalPages
|
||||
}
|
||||
|
||||
void topupTracker.reconcileWithEvents(response.events)
|
||||
} else {
|
||||
error.value = customerEventService.error.value || 'Failed to load events'
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useTopupTrackerStore } from '@/stores/topupTrackerStore'
|
||||
import { usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
@@ -98,7 +99,7 @@ export const useFirebaseAuthActions = () => {
|
||||
)
|
||||
}
|
||||
|
||||
// Go to Stripe checkout page
|
||||
useTopupTrackerStore().startTopup(amount)
|
||||
window.open(response.checkout_url, '_blank')
|
||||
}, reportError)
|
||||
|
||||
@@ -115,7 +116,9 @@ export const useFirebaseAuthActions = () => {
|
||||
}, reportError)
|
||||
|
||||
const fetchBalance = wrapWithErrorHandlingAsync(async () => {
|
||||
return await authStore.fetchBalance()
|
||||
const result = await authStore.fetchBalance()
|
||||
void useTopupTrackerStore().reconcileByFetchingEvents()
|
||||
return result
|
||||
}, reportError)
|
||||
|
||||
const signInWithGoogle = (errorHandler = reportError) =>
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { OverridedMixpanel } from 'mixpanel-browser'
|
||||
|
||||
import type {
|
||||
AuthMetadata,
|
||||
CreditTopupMetadata,
|
||||
ExecutionContext,
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
@@ -282,6 +283,27 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
this.trackEvent(eventName)
|
||||
}
|
||||
|
||||
trackAddApiCreditButtonClicked(): void {
|
||||
this.trackEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED)
|
||||
}
|
||||
|
||||
trackMonthlySubscriptionSucceeded(): void {
|
||||
this.trackEvent(TelemetryEvents.MONTHLY_SUBSCRIPTION_SUCCEEDED)
|
||||
}
|
||||
|
||||
trackApiCreditTopupButtonPurchaseClicked(amount: number): void {
|
||||
const metadata: CreditTopupMetadata = {
|
||||
credit_amount: amount
|
||||
}
|
||||
this.trackEvent(
|
||||
TelemetryEvents.API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED,
|
||||
metadata
|
||||
)
|
||||
}
|
||||
|
||||
trackApiCreditTopupSucceeded(): void {
|
||||
this.trackEvent(TelemetryEvents.API_CREDIT_TOPUP_SUCCEEDED)
|
||||
}
|
||||
trackRunButton(options?: { subscribe_to_run?: boolean }): void {
|
||||
if (this.isOnboardingMode) {
|
||||
// During onboarding, track basic run button click without workflow context
|
||||
|
||||
@@ -89,6 +89,13 @@ export interface TemplateMetadata {
|
||||
template_license?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit topup metadata
|
||||
*/
|
||||
export interface CreditTopupMetadata {
|
||||
credit_amount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow import metadata
|
||||
*/
|
||||
@@ -169,6 +176,10 @@ export interface TelemetryProvider {
|
||||
|
||||
// Subscription flow events
|
||||
trackSubscription(event: 'modal_opened' | 'subscribe_clicked'): void
|
||||
trackMonthlySubscriptionSucceeded(): void
|
||||
trackAddApiCreditButtonClicked(): void
|
||||
trackApiCreditTopupButtonPurchaseClicked(amount: number): void
|
||||
trackApiCreditTopupSucceeded(): void
|
||||
trackRunButton(options?: { subscribe_to_run?: boolean }): void
|
||||
|
||||
// Survey flow events
|
||||
@@ -221,6 +232,11 @@ 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',
|
||||
ADD_API_CREDIT_BUTTON_CLICKED: 'app:add_api_credit_button_clicked',
|
||||
API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED:
|
||||
'app:api_credit_topup_button_purchase_clicked',
|
||||
API_CREDIT_TOPUP_SUCCEEDED: 'app:api_credit_topup_succeeded',
|
||||
|
||||
// Onboarding Survey
|
||||
USER_SURVEY_OPENED: 'app:user_survey_opened',
|
||||
@@ -267,6 +283,7 @@ export type TelemetryEventProperties =
|
||||
| RunButtonProperties
|
||||
| ExecutionErrorMetadata
|
||||
| ExecutionSuccessMetadata
|
||||
| CreditTopupMetadata
|
||||
| WorkflowImportMetadata
|
||||
| TemplateLibraryMetadata
|
||||
| TemplateLibraryClosedMetadata
|
||||
|
||||
@@ -179,7 +179,7 @@ export const useCustomerEventsService = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
return executeRequest<CustomerEventsResponse>(
|
||||
const result = await executeRequest<CustomerEventsResponse>(
|
||||
() =>
|
||||
customerApiClient.get('/customers/events', {
|
||||
params: { page, limit },
|
||||
@@ -187,6 +187,8 @@ export const useCustomerEventsService = () => {
|
||||
}),
|
||||
{ errorContext, routeSpecificErrors }
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
155
src/stores/topupTrackerStore.ts
Normal file
155
src/stores/topupTrackerStore.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { AuditLog } from '@/services/customerEventsService'
|
||||
import {
|
||||
EventType,
|
||||
useCustomerEventsService
|
||||
} from '@/services/customerEventsService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
type PendingTopupRecord = {
|
||||
startedAtIso: string
|
||||
amountUsd?: number
|
||||
expectedCents?: number
|
||||
}
|
||||
|
||||
const storageKeyForUser = (userId: string) => `topupTracker:pending:${userId}`
|
||||
|
||||
export const useTopupTrackerStore = defineStore('topupTracker', () => {
|
||||
const telemetry = useTelemetry()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const pendingTopup = ref<PendingTopupRecord | null>(null)
|
||||
const storageListenerInitialized = ref(false)
|
||||
|
||||
const loadFromStorage = () => {
|
||||
const userId = authStore.userId
|
||||
if (!userId) return
|
||||
try {
|
||||
const rawValue = localStorage.getItem(storageKeyForUser(userId))
|
||||
if (!rawValue) return
|
||||
const parsedValue = JSON.parse(rawValue) as PendingTopupRecord
|
||||
pendingTopup.value = parsedValue
|
||||
} catch {
|
||||
pendingTopup.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const persistToStorage = () => {
|
||||
const userId = authStore.userId
|
||||
if (!userId) return
|
||||
if (pendingTopup.value) {
|
||||
localStorage.setItem(
|
||||
storageKeyForUser(userId),
|
||||
JSON.stringify(pendingTopup.value)
|
||||
)
|
||||
} else {
|
||||
localStorage.removeItem(storageKeyForUser(userId))
|
||||
}
|
||||
}
|
||||
|
||||
const initializeStorageSynchronization = () => {
|
||||
if (storageListenerInitialized.value) return
|
||||
storageListenerInitialized.value = true
|
||||
loadFromStorage()
|
||||
window.addEventListener('storage', (e: StorageEvent) => {
|
||||
const userId = authStore.userId
|
||||
if (!userId) return
|
||||
if (e.key === storageKeyForUser(userId)) {
|
||||
loadFromStorage()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => authStore.userId,
|
||||
(newUserId, oldUserId) => {
|
||||
if (newUserId && newUserId !== oldUserId) {
|
||||
loadFromStorage()
|
||||
return
|
||||
}
|
||||
if (!newUserId && oldUserId) {
|
||||
pendingTopup.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const startTopup = (amountUsd: number) => {
|
||||
const userId = authStore.userId
|
||||
if (!userId) return
|
||||
const expectedCents = Math.round(amountUsd * 100)
|
||||
pendingTopup.value = {
|
||||
startedAtIso: new Date().toISOString(),
|
||||
amountUsd,
|
||||
expectedCents
|
||||
}
|
||||
persistToStorage()
|
||||
}
|
||||
|
||||
const clearTopup = () => {
|
||||
pendingTopup.value = null
|
||||
persistToStorage()
|
||||
}
|
||||
|
||||
const reconcileWithEvents = async (
|
||||
events: AuditLog[] | undefined | null
|
||||
): Promise<boolean> => {
|
||||
if (!events || events.length === 0) return false
|
||||
if (!pendingTopup.value) return false
|
||||
|
||||
const startedAt = new Date(pendingTopup.value.startedAtIso)
|
||||
if (Number.isNaN(+startedAt)) {
|
||||
clearTopup()
|
||||
return false
|
||||
}
|
||||
|
||||
const withinWindow = (createdAt: string) => {
|
||||
const created = new Date(createdAt)
|
||||
if (Number.isNaN(+created)) return false
|
||||
const maxAgeMs = 1000 * 60 * 60 * 24
|
||||
return (
|
||||
created >= startedAt &&
|
||||
created.getTime() - startedAt.getTime() <= maxAgeMs
|
||||
)
|
||||
}
|
||||
|
||||
let matched = events.filter((e) => {
|
||||
if (e.event_type !== EventType.CREDIT_ADDED) return false
|
||||
if (!e.createdAt || !withinWindow(e.createdAt)) return false
|
||||
return true
|
||||
})
|
||||
|
||||
if (pendingTopup.value.expectedCents != null) {
|
||||
matched = matched.filter((e) =>
|
||||
typeof e.params?.amount === 'number'
|
||||
? e.params.amount === pendingTopup.value?.expectedCents
|
||||
: true
|
||||
)
|
||||
}
|
||||
|
||||
if (matched.length === 0) return false
|
||||
|
||||
telemetry?.trackApiCreditTopupSucceeded()
|
||||
await authStore.fetchBalance().catch(() => {})
|
||||
clearTopup()
|
||||
return true
|
||||
}
|
||||
|
||||
const reconcileByFetchingEvents = async (): Promise<boolean> => {
|
||||
const service = useCustomerEventsService()
|
||||
const response = await service.getMyEvents({ page: 1, limit: 10 })
|
||||
if (!response) return false
|
||||
return await reconcileWithEvents(response.events)
|
||||
}
|
||||
|
||||
initializeStorageSynchronization()
|
||||
|
||||
return {
|
||||
pendingTopup,
|
||||
startTopup,
|
||||
clearTopup,
|
||||
reconcileWithEvents,
|
||||
reconcileByFetchingEvents
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user