mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-27 18:24:11 +00:00
Implement topupTrackerStore
(cherry picked from commit fbf2aeb54f6c82afcdb4717f49bca28d9d869ab5)
This commit is contained in:
@@ -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) =>
|
||||
|
||||
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