mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-04 12:40:00 +00:00
[backport rh-test] subscription panel (#6140)
## Summary Backport of #6064 (subscription page) to the `rh-test` branch. This PR manually cherry-picks commit7e1e8e3b65to the rh-test branch and resolves merge conflicts that prevented automatic backporting. ## Conflicts Resolved ### 1. `src/components/actionbar/ComfyActionbar.vue` - **Conflict**: HEAD (rh-test) used `<ComfyQueueButton />` while the subscription PR introduced `<ComfyRunButton />` - **Resolution**: Updated to use `<ComfyRunButton />` to include the subscription functionality wrapper while maintaining the existing rh-test template structure ### 2. `src/composables/auth/useFirebaseAuthActions.ts` - **Conflict**: Simple ordering difference in the return statement - **Resolution**: Used the subscription PR's ordering: `deleteAccount, accessError, reportError` ## Testing The cherry-pick completed successfully and passed all pre-commit hooks: - ✅ ESLint - ✅ Prettier formatting - ⚠️ Note: 2 unused i18n keys detected (informational only, same as original PR) ## Related - Original PR: #6064 - Cherry-picked commit:7e1e8e3b65┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6140-backport-subscription-page-to-rh-test-2916d73d365081f38f00df422004f61a) by [Unito](https://www.unito.io) Co-authored-by: Terry Jia <terryjia88@gmail.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<Button
|
||||
:label="label || $t('subscription.required.subscribe')"
|
||||
:size="size"
|
||||
:class="buttonClass"
|
||||
:loading="isLoading"
|
||||
:disabled="isPolling"
|
||||
severity="primary"
|
||||
@click="handleSubscribe"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label?: string
|
||||
size?: 'small' | 'large'
|
||||
buttonClass?: string
|
||||
}>(),
|
||||
{
|
||||
size: 'large',
|
||||
buttonClass: 'w-full font-bold'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
subscribed: []
|
||||
}>()
|
||||
|
||||
const { subscribe, isActiveSubscription, fetchStatus } = useSubscription()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const isPolling = ref(false)
|
||||
let pollInterval: number | null = null
|
||||
|
||||
const POLL_INTERVAL_MS = 3000 // Poll every 3 seconds
|
||||
const MAX_POLL_DURATION_MS = 5 * 60 * 1000 // Stop polling after 5 minutes
|
||||
|
||||
const startPollingSubscriptionStatus = () => {
|
||||
isPolling.value = true
|
||||
isLoading.value = true
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
if (Date.now() - startTime > MAX_POLL_DURATION_MS) {
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
|
||||
await fetchStatus()
|
||||
|
||||
if (isActiveSubscription.value) {
|
||||
stopPolling()
|
||||
emit('subscribed')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[SubscribeButton] Error polling subscription status:',
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
void poll()
|
||||
pollInterval = window.setInterval(poll, POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval)
|
||||
pollInterval = null
|
||||
}
|
||||
isPolling.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
await subscribe()
|
||||
|
||||
startPollingSubscriptionStatus()
|
||||
} catch (error) {
|
||||
console.error('[SubscribeButton] Error initiating subscription:', error)
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.bottom="{
|
||||
value: $t('subscription.subscribeToRun'),
|
||||
showDelay: 600
|
||||
}"
|
||||
class="subscribe-to-run-button"
|
||||
:label="$t('subscription.subscribeToRun')"
|
||||
icon="pi pi-lock"
|
||||
severity="primary"
|
||||
size="small"
|
||||
data-testid="subscribe-to-run-button"
|
||||
@click="showSubscriptionDialog"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
|
||||
const { showSubscriptionDialog } = useSubscription()
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="pi pi-check mt-1 text-sm" />
|
||||
<span class="text-sm">
|
||||
{{ $t('subscription.benefits.benefit1') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="pi pi-check mt-1 text-sm" />
|
||||
<span class="text-sm">
|
||||
{{ $t('subscription.benefits.benefit2') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
:label="$t('subscription.viewMoreDetails')"
|
||||
text
|
||||
icon="pi pi-external-link"
|
||||
icon-pos="left"
|
||||
size="small"
|
||||
class="self-start !p-0 text-sm hover:!bg-transparent [&]:!text-[inherit]"
|
||||
@click="handleViewMoreDetails"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const handleViewMoreDetails = () => {
|
||||
window.open('https://www.comfy.org/cloud', '_blank')
|
||||
}
|
||||
</script>
|
||||
276
src/platform/cloud/subscription/components/SubscriptionPanel.vue
Normal file
276
src/platform/cloud/subscription/components/SubscriptionPanel.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<TabPanel value="PlanCredits" class="subscription-container h-full">
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-2xl">
|
||||
{{ $t('subscription.title') }}
|
||||
</h2>
|
||||
<TopbarBadges reverse-order />
|
||||
</div>
|
||||
|
||||
<div class="grow overflow-auto">
|
||||
<div class="rounded-lg border border-charcoal-400 p-4">
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-2xl font-bold">{{
|
||||
formattedMonthlyPrice
|
||||
}}</span>
|
||||
<span>{{ $t('subscription.perMonth') }}</span>
|
||||
</div>
|
||||
<div v-if="isActiveSubscription" class="text-xs text-muted">
|
||||
{{
|
||||
$t('subscription.renewsDate', {
|
||||
date: formattedRenewalDate
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="isActiveSubscription"
|
||||
:label="$t('subscription.manageSubscription')"
|
||||
severity="secondary"
|
||||
class="text-xs"
|
||||
@click="manageSubscription"
|
||||
/>
|
||||
<SubscribeButton
|
||||
v-else
|
||||
:label="$t('subscription.subscribeNow')"
|
||||
size="small"
|
||||
button-class="text-xs"
|
||||
@subscribed="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 rounded-lg pt-10 lg:grid-cols-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm">
|
||||
{{ $t('subscription.apiNodesBalance') }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="text-xs text-muted">
|
||||
{{ $t('subscription.apiNodesDescription') }}
|
||||
</div>
|
||||
<Button
|
||||
icon="pi pi-question-circle"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-3 rounded-lg border p-4 dark-theme:border-0 dark-theme:bg-charcoal-600"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-xs text-muted">
|
||||
{{ $t('subscription.totalCredits') }}
|
||||
</div>
|
||||
<div class="text-2xl font-bold">${{ totalCredits }}</div>
|
||||
</div>
|
||||
<Button
|
||||
icon="pi pi-sync"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
:loading="isLoadingBalance"
|
||||
@click="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="latestEvents.length > 0"
|
||||
class="flex flex-col gap-2 pt-3 text-xs"
|
||||
>
|
||||
<div
|
||||
v-for="event in latestEvents"
|
||||
:key="event.event_id"
|
||||
class="flex items-center justify-between py-1"
|
||||
>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-medium">
|
||||
{{
|
||||
event.event_type
|
||||
? customerEventService.formatEventType(
|
||||
event.event_type
|
||||
)
|
||||
: ''
|
||||
}}
|
||||
</span>
|
||||
<span class="text-muted">
|
||||
{{
|
||||
event.createdAt
|
||||
? customerEventService.formatDate(event.createdAt)
|
||||
: ''
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="event.params?.amount !== undefined"
|
||||
class="font-bold"
|
||||
>
|
||||
${{
|
||||
customerEventService.formatAmount(
|
||||
event.params.amount as number
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<Button
|
||||
:label="$t('subscription.viewUsageHistory')"
|
||||
text
|
||||
severity="secondary"
|
||||
class="p-0 text-xs text-muted"
|
||||
@click="handleViewUsageHistory"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('subscription.addApiCredits')"
|
||||
severity="secondary"
|
||||
class="text-xs"
|
||||
@click="handleAddApiCredits"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="text-sm">
|
||||
{{ $t('subscription.yourPlanIncludes') }}
|
||||
</div>
|
||||
|
||||
<SubscriptionBenefits />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between border-t border-charcoal-400 pt-3"
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
:label="$t('subscription.learnMore')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-question-circle"
|
||||
class="text-xs"
|
||||
@click="handleLearnMore"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('subscription.messageSupport')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-comment"
|
||||
class="text-xs"
|
||||
@click="handleMessageSupport"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
:label="$t('subscription.invoiceHistory')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-external-link"
|
||||
icon-pos="right"
|
||||
class="text-xs"
|
||||
@click="handleInvoiceHistory"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import SubscriptionBenefits from '@/platform/cloud/subscription/components/SubscriptionBenefits.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import type { AuditLog } from '@/services/customerEventsService'
|
||||
import { useCustomerEventsService } from '@/services/customerEventsService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
const dialogService = useDialogService()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const customerEventService = useCustomerEventsService()
|
||||
|
||||
const {
|
||||
isActiveSubscription,
|
||||
formattedRenewalDate,
|
||||
formattedMonthlyPrice,
|
||||
manageSubscription,
|
||||
handleViewUsageHistory,
|
||||
handleLearnMore,
|
||||
handleInvoiceHistory,
|
||||
fetchStatus
|
||||
} = useSubscription()
|
||||
|
||||
const latestEvents = ref<AuditLog[]>([])
|
||||
|
||||
const totalCredits = computed(() => {
|
||||
if (!authStore.balance) return '0.00'
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
})
|
||||
|
||||
const isLoadingBalance = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
const fetchLatestEvents = async () => {
|
||||
try {
|
||||
const response = await customerEventService.getMyEvents({
|
||||
page: 1,
|
||||
limit: 2
|
||||
})
|
||||
if (response?.events) {
|
||||
latestEvents.value = response.events
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SubscriptionPanel] Error fetching latest events:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void handleRefresh()
|
||||
})
|
||||
|
||||
const handleAddApiCredits = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
const handleMessageSupport = async () => {
|
||||
await commandStore.execute('Comfy.ContactSupport')
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await Promise.all([
|
||||
authActions.fetchBalance(),
|
||||
fetchStatus(),
|
||||
fetchLatestEvents()
|
||||
])
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.bg-comfy-menu-secondary) {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="grid h-full grid-cols-5 px-10 pb-10">
|
||||
<div
|
||||
class="relative col-span-2 flex items-center justify-center overflow-hidden rounded-sm"
|
||||
>
|
||||
<video
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
class="h-full min-w-[125%] object-cover"
|
||||
style="margin-left: -20%"
|
||||
>
|
||||
<source
|
||||
src="/assets/images/cloud-subscription.webm"
|
||||
type="video/webm"
|
||||
/>
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 flex flex-col justify-between pl-8">
|
||||
<div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<div class="text-sm text-muted">
|
||||
{{ $t('subscription.required.title') }}
|
||||
</div>
|
||||
<TopbarBadges
|
||||
reverse-order
|
||||
no-padding
|
||||
text-class="!text-sm !font-normal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-4xl font-bold">{{ formattedMonthlyPrice }}</span>
|
||||
<span class="text-xl">{{ $t('subscription.perMonth') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SubscriptionBenefits class="mt-6 text-muted" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<SubscribeButton @subscribed="handleSubscribed" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import SubscriptionBenefits from '@/platform/cloud/subscription/components/SubscriptionBenefits.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [subscribed: boolean]
|
||||
}>()
|
||||
|
||||
const { formattedMonthlyPrice } = useSubscription()
|
||||
|
||||
const handleSubscribed = () => {
|
||||
emit('close', true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.bg-comfy-menu-secondary) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:deep(.p-button) {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
208
src/platform/cloud/subscription/composables/useSubscription.ts
Normal file
208
src/platform/cloud/subscription/composables/useSubscription.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { COMFY_API_BASE_URL } from '@/config/comfyApi'
|
||||
import { MONTHLY_SUBSCRIPTION_PRICE } from '@/config/subscriptionPricesConfig'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import {
|
||||
FirebaseAuthStoreError,
|
||||
useFirebaseAuthStore
|
||||
} from '@/stores/firebaseAuthStore'
|
||||
|
||||
interface CloudSubscriptionCheckoutResponse {
|
||||
checkout_url: string
|
||||
}
|
||||
|
||||
interface CloudSubscriptionStatusResponse {
|
||||
is_active: boolean
|
||||
subscription_id: string
|
||||
renewal_date: string
|
||||
}
|
||||
|
||||
const subscriptionStatus = ref<CloudSubscriptionStatusResponse | null>(null)
|
||||
|
||||
const isActiveSubscription = computed(() => {
|
||||
if (!isCloud) return true
|
||||
|
||||
return subscriptionStatus.value?.is_active ?? false
|
||||
})
|
||||
|
||||
let isWatchSetup = false
|
||||
|
||||
export function useSubscription() {
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
const { getAuthHeader } = useFirebaseAuthStore()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
const { reportError } = useFirebaseAuthActions()
|
||||
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
|
||||
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 formattedMonthlyPrice = computed(
|
||||
() => `$${MONTHLY_SUBSCRIPTION_PRICE.toFixed(0)}`
|
||||
)
|
||||
|
||||
const fetchStatus = wrapWithErrorHandlingAsync(async () => {
|
||||
return await 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 = () => {
|
||||
dialogService.showSubscriptionRequiredDialog()
|
||||
}
|
||||
|
||||
const manageSubscription = async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
}
|
||||
|
||||
const requireActiveSubscription = async (): Promise<void> => {
|
||||
await fetchSubscriptionStatus()
|
||||
|
||||
if (!isActiveSubscription.value) {
|
||||
showSubscriptionDialog()
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewUsageHistory = () => {
|
||||
window.open('https://platform.comfy.org/profile/usage', '_blank')
|
||||
}
|
||||
|
||||
const handleLearnMore = () => {
|
||||
window.open('https://docs.comfy.org', '_blank')
|
||||
}
|
||||
|
||||
const handleInvoiceHistory = async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current cloud subscription status for the authenticated user
|
||||
* @returns Subscription status or null if no subscription exists
|
||||
*/
|
||||
const fetchSubscriptionStatus =
|
||||
async (): Promise<CloudSubscriptionStatusResponse | null> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${COMFY_API_BASE_URL}/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
|
||||
}
|
||||
|
||||
if (!isWatchSetup) {
|
||||
isWatchSetup = true
|
||||
watch(
|
||||
() => isLoggedIn.value,
|
||||
async (loggedIn) => {
|
||||
if (loggedIn) {
|
||||
await fetchSubscriptionStatus()
|
||||
} else {
|
||||
subscriptionStatus.value = null
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
const initiateSubscriptionCheckout =
|
||||
async (): Promise<CloudSubscriptionCheckoutResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${COMFY_API_BASE_URL}/customers/cloud-subscription-checkout`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToInitiateSubscription', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isActiveSubscription,
|
||||
formattedRenewalDate,
|
||||
formattedMonthlyPrice,
|
||||
|
||||
// Actions
|
||||
subscribe,
|
||||
fetchStatus,
|
||||
showSubscriptionDialog,
|
||||
manageSubscription,
|
||||
requireActiveSubscription,
|
||||
handleViewUsageHistory,
|
||||
handleLearnMore,
|
||||
handleInvoiceHistory
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user