mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
Backport of #9142 to `core/1.39` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9160-backport-core-1-39-fix-use-getAuthHeader-for-API-key-auth-in-subscription-billing-3116d73d365081979e17cc176087b7fb) by [Unito](https://www.unito.io) Co-authored-by: Christian Byrne <cbyrne@comfy.org>
302 lines
8.6 KiB
TypeScript
302 lines
8.6 KiB
TypeScript
import { computed, ref, watch } from 'vue'
|
|
import { createSharedComposable } from '@vueuse/core'
|
|
|
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
|
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
|
import { getComfyApiBaseUrl, getComfyPlatformBaseUrl } from '@/config/comfyApi'
|
|
import { t } from '@/i18n'
|
|
import { isCloud } from '@/platform/distribution/types'
|
|
import { useTelemetry } from '@/platform/telemetry'
|
|
import type { CheckoutAttributionMetadata } from '@/platform/telemetry/types'
|
|
import {
|
|
FirebaseAuthStoreError,
|
|
useFirebaseAuthStore
|
|
} from '@/stores/firebaseAuthStore'
|
|
import { useDialogService } from '@/services/dialogService'
|
|
import { TIER_TO_KEY } from '@/platform/cloud/subscription/constants/tierPricing'
|
|
import type { operations } from '@/types/comfyRegistryTypes'
|
|
import { useSubscriptionCancellationWatcher } from './useSubscriptionCancellationWatcher'
|
|
|
|
type CloudSubscriptionCheckoutResponse = NonNullable<
|
|
operations['createCloudSubscriptionCheckout']['responses']['201']['content']['application/json']
|
|
>
|
|
|
|
export type CloudSubscriptionStatusResponse = NonNullable<
|
|
operations['GetCloudSubscriptionStatus']['responses']['200']['content']['application/json']
|
|
>
|
|
|
|
function useSubscriptionInternal() {
|
|
const subscriptionStatus = ref<CloudSubscriptionStatusResponse | null>(null)
|
|
const telemetry = useTelemetry()
|
|
const isInitialized = ref(false)
|
|
|
|
const isSubscribedOrIsNotCloud = computed(() => {
|
|
if (!isCloud || !window.__CONFIG__?.subscription_required) return true
|
|
|
|
return subscriptionStatus.value?.is_active ?? false
|
|
})
|
|
const { reportError, accessBillingPortal } = useFirebaseAuthActions()
|
|
const { showSubscriptionRequiredDialog } = useDialogService()
|
|
|
|
const firebaseAuthStore = useFirebaseAuthStore()
|
|
const { getAuthHeader } = firebaseAuthStore
|
|
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
|
|
|
const { isLoggedIn } = useCurrentUser()
|
|
|
|
const isCancelled = computed(() => {
|
|
return !!subscriptionStatus.value?.end_date
|
|
})
|
|
|
|
const formattedRenewalDate = computed(() => {
|
|
if (!subscriptionStatus.value?.renewal_date) return ''
|
|
|
|
const renewalDate = new Date(subscriptionStatus.value.renewal_date)
|
|
|
|
return renewalDate.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
})
|
|
})
|
|
|
|
const formattedEndDate = computed(() => {
|
|
if (!subscriptionStatus.value?.end_date) return ''
|
|
|
|
const endDate = new Date(subscriptionStatus.value.end_date)
|
|
|
|
return endDate.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
})
|
|
})
|
|
|
|
const subscriptionTier = computed(
|
|
() => subscriptionStatus.value?.subscription_tier ?? null
|
|
)
|
|
|
|
const subscriptionDuration = computed(
|
|
() => subscriptionStatus.value?.subscription_duration ?? null
|
|
)
|
|
|
|
const isYearlySubscription = computed(
|
|
() => subscriptionDuration.value === 'ANNUAL'
|
|
)
|
|
|
|
const subscriptionTierName = computed(() => {
|
|
const tier = subscriptionTier.value
|
|
if (!tier) return ''
|
|
const key = TIER_TO_KEY[tier] ?? 'standard'
|
|
const baseName = t(`subscription.tiers.${key}.name`)
|
|
return isYearlySubscription.value
|
|
? t('subscription.tierNameYearly', { name: baseName })
|
|
: baseName
|
|
})
|
|
|
|
function buildApiUrl(path: string): string {
|
|
return `${getComfyApiBaseUrl()}${path}`
|
|
}
|
|
|
|
const getCheckoutAttributionForCloud =
|
|
async (): Promise<CheckoutAttributionMetadata> => {
|
|
if (__DISTRIBUTION__ !== 'cloud') {
|
|
return {}
|
|
}
|
|
|
|
const { getCheckoutAttribution } =
|
|
await import('@/platform/telemetry/utils/checkoutAttribution')
|
|
|
|
return getCheckoutAttribution()
|
|
}
|
|
|
|
const fetchStatus = wrapWithErrorHandlingAsync(
|
|
fetchSubscriptionStatus,
|
|
reportError
|
|
)
|
|
|
|
const subscribe = wrapWithErrorHandlingAsync(async () => {
|
|
const response = await initiateSubscriptionCheckout()
|
|
|
|
if (!response.checkout_url) {
|
|
throw new Error(
|
|
t('toastMessages.failedToInitiateSubscription', {
|
|
error: 'No checkout URL returned'
|
|
})
|
|
)
|
|
}
|
|
|
|
window.open(response.checkout_url, '_blank')
|
|
}, reportError)
|
|
|
|
const showSubscriptionDialog = () => {
|
|
if (isCloud) {
|
|
useTelemetry()?.trackSubscription('modal_opened')
|
|
}
|
|
|
|
void showSubscriptionRequiredDialog()
|
|
}
|
|
|
|
/**
|
|
* Whether cloud subscription mode is enabled (cloud distribution with subscription_required config).
|
|
* Use to determine which UI to show (SubscriptionPanel vs LegacyCreditsPanel).
|
|
*/
|
|
const isSubscriptionEnabled = (): boolean =>
|
|
Boolean(isCloud && window.__CONFIG__?.subscription_required)
|
|
|
|
const { startCancellationWatcher, stopCancellationWatcher } =
|
|
useSubscriptionCancellationWatcher({
|
|
fetchStatus,
|
|
isActiveSubscription: isSubscribedOrIsNotCloud,
|
|
subscriptionStatus,
|
|
telemetry,
|
|
shouldWatchCancellation: isSubscriptionEnabled
|
|
})
|
|
|
|
const manageSubscription = async () => {
|
|
await accessBillingPortal()
|
|
startCancellationWatcher()
|
|
}
|
|
|
|
const requireActiveSubscription = async (): Promise<void> => {
|
|
await fetchSubscriptionStatus()
|
|
|
|
if (!isSubscribedOrIsNotCloud.value) {
|
|
showSubscriptionDialog()
|
|
}
|
|
}
|
|
|
|
const handleViewUsageHistory = () => {
|
|
window.open(`${getComfyPlatformBaseUrl()}/profile/usage`, '_blank')
|
|
}
|
|
|
|
const handleLearnMore = () => {
|
|
window.open('https://docs.comfy.org', '_blank')
|
|
}
|
|
|
|
const handleInvoiceHistory = async () => {
|
|
await accessBillingPortal()
|
|
}
|
|
|
|
/**
|
|
* Fetch the current cloud subscription status for the authenticated user
|
|
* @returns Subscription status or null if no subscription exists
|
|
*/
|
|
async function fetchSubscriptionStatus(): Promise<CloudSubscriptionStatusResponse | null> {
|
|
const authHeader = await getAuthHeader()
|
|
if (!authHeader) {
|
|
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
|
}
|
|
|
|
const response = await fetch(
|
|
buildApiUrl('/customers/cloud-subscription-status'),
|
|
{
|
|
headers: {
|
|
...authHeader,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}
|
|
)
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json()
|
|
throw new FirebaseAuthStoreError(
|
|
t('toastMessages.failedToFetchSubscription', {
|
|
error: errorData.message
|
|
})
|
|
)
|
|
}
|
|
|
|
const statusData = await response.json()
|
|
subscriptionStatus.value = statusData
|
|
|
|
return statusData
|
|
}
|
|
|
|
watch(
|
|
() => isLoggedIn.value,
|
|
async (loggedIn) => {
|
|
if (loggedIn && isCloud) {
|
|
try {
|
|
await fetchSubscriptionStatus()
|
|
} catch (error) {
|
|
// Network errors are expected during navigation/component unmount
|
|
// and when offline - log for debugging but don't surface to user
|
|
console.error('Failed to fetch subscription status:', error)
|
|
} finally {
|
|
isInitialized.value = true
|
|
}
|
|
} else {
|
|
subscriptionStatus.value = null
|
|
stopCancellationWatcher()
|
|
isInitialized.value = true
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
const initiateSubscriptionCheckout =
|
|
async (): Promise<CloudSubscriptionCheckoutResponse> => {
|
|
const authHeader = await getAuthHeader()
|
|
if (!authHeader) {
|
|
throw new FirebaseAuthStoreError(
|
|
t('toastMessages.userNotAuthenticated')
|
|
)
|
|
}
|
|
const checkoutAttribution = await getCheckoutAttributionForCloud()
|
|
|
|
const response = await fetch(
|
|
buildApiUrl('/customers/cloud-subscription-checkout'),
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
...authHeader,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(checkoutAttribution)
|
|
}
|
|
)
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json()
|
|
throw new FirebaseAuthStoreError(
|
|
t('toastMessages.failedToInitiateSubscription', {
|
|
error: errorData.message
|
|
})
|
|
)
|
|
}
|
|
|
|
return response.json()
|
|
}
|
|
|
|
return {
|
|
// State
|
|
isActiveSubscription: isSubscribedOrIsNotCloud,
|
|
isInitialized,
|
|
isCancelled,
|
|
formattedRenewalDate,
|
|
formattedEndDate,
|
|
subscriptionTier,
|
|
subscriptionDuration,
|
|
isYearlySubscription,
|
|
subscriptionTierName,
|
|
subscriptionStatus,
|
|
|
|
// Utilities
|
|
isSubscriptionEnabled,
|
|
|
|
// Actions
|
|
subscribe,
|
|
fetchStatus,
|
|
showSubscriptionDialog,
|
|
manageSubscription,
|
|
requireActiveSubscription,
|
|
handleViewUsageHistory,
|
|
handleLearnMore,
|
|
handleInvoiceHistory
|
|
}
|
|
}
|
|
|
|
export const useSubscription = createSharedComposable(useSubscriptionInternal)
|