Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/assets/images/cloud-subscription.webm
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christian-byrne do we want assets here or should this just get uploaded to GCS and we serve up a publicly signed link?

Binary file not shown.
5 changes: 3 additions & 2 deletions src/components/actionbar/ComfyActionbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
)
"
/>
<ComfyQueueButton />

<ComfyRunButton />
</div>
</Panel>
</div>
Expand All @@ -55,7 +56,7 @@ import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { cn } from '@/utils/tailwindUtil'

import ComfyQueueButton from './ComfyQueueButton.vue'
import ComfyRunButton from './ComfyRunButton'

const settingsStore = useSettingStore()

Expand Down
19 changes: 19 additions & 0 deletions src/components/actionbar/ComfyRunButton/CloudRunButtonWrapper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<component
:is="currentButton"
:key="isActiveSubscription ? 'queue' : 'subscribe'"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'

import ComfyQueueButton from '@/components/actionbar/ComfyRunButton/ComfyQueueButton.vue'
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'

const { isActiveSubscription } = useSubscription()

const currentButton = computed(() =>
isActiveSubscription.value ? ComfyQueueButton : SubscribeToRunButton
)
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ import {
} from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'

import BatchCountEdit from './BatchCountEdit.vue'
import BatchCountEdit from '../BatchCountEdit.vue'

const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
Expand Down
7 changes: 7 additions & 0 deletions src/components/actionbar/ComfyRunButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineAsyncComponent } from 'vue'

import { isCloud } from '@/platform/distribution/types'

export default isCloud
? defineAsyncComponent(() => import('./CloudRunButtonWrapper.vue'))
: defineAsyncComponent(() => import('./ComfyQueueButton.vue'))
29 changes: 24 additions & 5 deletions src/components/topbar/TopbarBadge.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
<template>
<div class="flex items-center gap-2 bg-comfy-menu-secondary px-3">
<div
class="flex items-center gap-2 bg-comfy-menu-secondary"
:class="[{ 'flex-row-reverse': reverseOrder }, noPadding ? '' : 'px-3']"
>
<div
v-if="badge.label"
class="rounded-full bg-white px-1.5 py-0.5 text-xxxs font-semibold text-black"
:class="labelClass"
>
{{ badge.label }}
</div>
<div class="font-inter text-sm font-extrabold text-slate-100">
<div
class="font-inter text-sm font-extrabold text-slate-100"
:class="textClass"
>
{{ badge.text }}
</div>
</div>
</template>
<script setup lang="ts">
import type { TopbarBadge } from '@/types/comfy'

defineProps<{
badge: TopbarBadge
}>()
withDefaults(
defineProps<{
badge: TopbarBadge
reverseOrder?: boolean
noPadding?: boolean
labelClass?: string
textClass?: string
}>(),
{
reverseOrder: false,
noPadding: false,
labelClass: '',
textClass: ''
}
)
</script>
19 changes: 19 additions & 0 deletions src/components/topbar/TopbarBadges.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
v-for="badge in topbarBadgeStore.badges"
:key="badge.text"
:badge
:reverse-order="reverseOrder"
:no-padding="noPadding"
:label-class="labelClass"
:text-class="textClass"
/>
</div>
</template>
Expand All @@ -13,5 +17,20 @@ import { useTopbarBadgeStore } from '@/stores/topbarBadgeStore'

import TopbarBadge from './TopbarBadge.vue'

withDefaults(
defineProps<{
reverseOrder?: boolean
noPadding?: boolean
labelClass?: string
textClass?: string
}>(),
{
reverseOrder: false,
noPadding: false,
labelClass: '',
textClass: ''
}
)

const topbarBadgeStore = useTopbarBadgeStore()
</script>
3 changes: 2 additions & 1 deletion src/composables/auth/useFirebaseAuthActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export const useFirebaseAuthActions = () => {
signUpWithEmail,
updatePassword,
deleteAccount,
accessError
accessError,
reportError
}
}
1 change: 1 addition & 0 deletions src/config/subscriptionPricesConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MONTHLY_SUBSCRIPTION_PRICE = 20
24 changes: 24 additions & 0 deletions src/extensions/core/cloudSubscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { watch } from 'vue'

import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useExtensionService } from '@/services/extensionService'

useExtensionService().registerExtension({
name: 'Comfy.CloudSubscription',

setup: async () => {
const { isLoggedIn } = useCurrentUser()
const { requireActiveSubscription } = useSubscription()

const checkSubscriptionStatus = () => {
if (!isLoggedIn.value) return

void requireActiveSubscription()
}

watch(() => isLoggedIn.value, checkSubscriptionStatus, {
immediate: true
})
}
})
1 change: 1 addition & 0 deletions src/extensions/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ import './widgetInputs'

if (isCloud) {
import('./cloudBadge')
import('./cloudSubscription')
}
34 changes: 33 additions & 1 deletion src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1334,7 +1334,8 @@
"Notification Preferences": "Notification Preferences",
"3DViewer": "3DViewer",
"Vue Nodes": "Vue Nodes",
"Canvas Navigation": "Canvas Navigation"
"Canvas Navigation": "Canvas Navigation",
"PlanCredits": "Plan & Credits"
},
"serverConfigItems": {
"listen": {
Expand Down Expand Up @@ -1774,6 +1775,8 @@
"failedToInitiateCreditPurchase": "Failed to initiate credit purchase: {error}",
"failedToAccessBillingPortal": "Failed to access billing portal: {error}",
"failedToPurchaseCredits": "Failed to purchase credits: {error}",
"failedToFetchSubscription": "Failed to fetch subscription status: {error}",
"failedToInitiateSubscription": "Failed to initiate subscription: {error}",
"unauthorizedDomain": "Your domain {domain} is not authorized to use this service. Please contact {email} to add your domain to the whitelist.",
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.",
"nothingSelected": "Nothing selected",
Expand Down Expand Up @@ -1914,6 +1917,35 @@
"added": "Added",
"accountInitialized": "Account initialized"
},
"subscription": {
"title": "Subscription",
"comfyCloud": "Comfy Cloud",
"beta": "BETA",
"perMonth": "USD / month",
"renewsDate": "Renews {date}",
"manageSubscription": "Manage subscription",
"apiNodesBalance": "\"API Nodes\" Credit Balance",
"apiNodesDescription": "For running commercial/proprietary models",
"totalCredits": "Total credits",
"viewUsageHistory": "View usage history",
"addApiCredits": "Add API credits",
"yourPlanIncludes": "Your plan includes:",
"viewMoreDetails": "View more details",
"learnMore": "Learn more",
"messageSupport": "Message support",
"invoiceHistory": "Invoice history",
"benefits": {
"benefit1": "$10 in monthly credits for API models — top up when needed",
"benefit2": "Up to 30 min runtime per job"
},
"required": {
"title": "Subscribe to",
"waitingForSubscription": "Complete your subscription in the new tab. We'll automatically detect when you're done!",
"subscribe": "Subscribe"
},
"subscribeToRun": "Subscribe to Run",
"subscribeNow": "Subscribe Now"
},
"userSettings": {
"title": "User Settings",
"name": "Name",
Expand Down
99 changes: 99 additions & 0 deletions src/platform/cloud/subscription/components/SubscribeButton.vue
Original file line number Diff line number Diff line change
@@ -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>
23 changes: 23 additions & 0 deletions src/platform/cloud/subscription/components/SubscribeToRun.vue
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Loading
Loading