[backport cloud/1.34] feat: add Stripe pricing table integration for subscription dialog (conditional on feature flag) (#7292)

Backport of #7288 to `cloud/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7292-backport-cloud-1-34-feat-add-Stripe-pricing-table-integration-for-subscription-dialog--2c46d73d36508111869ddf32de921b29)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
Comfy Org PR Bot
2025-12-09 21:03:59 +09:00
committed by GitHub
parent d2f5f0dce1
commit 27dcb152ff
14 changed files with 651 additions and 18 deletions

View File

@@ -1,5 +1,68 @@
<template>
<div class="relative grid h-full grid-cols-5">
<div
v-if="showStripePricingTable"
class="flex flex-col gap-6 rounded-[24px] border border-interface-stroke bg-[var(--p-dialog-background)] p-4 shadow-[0_25px_80px_rgba(5,6,12,0.45)] md:p-6"
>
<div
class="flex flex-col gap-6 md:flex-row md:items-start md:justify-between"
>
<div class="flex flex-col gap-2 text-left md:max-w-2xl">
<div
class="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.3em] text-text-secondary"
>
{{ $t('subscription.required.title') }}
<CloudBadge
reverse-order
no-padding
background-color="var(--p-dialog-background)"
use-subscription
/>
</div>
<div class="text-3xl font-semibold leading-tight md:text-4xl">
{{ $t('subscription.description') }}
</div>
</div>
<Button
icon="pi pi-times"
text
rounded
class="h-10 w-10 shrink-0 text-text-secondary hover:bg-white/10"
:aria-label="$t('g.close')"
@click="handleClose"
/>
</div>
<StripePricingTable class="flex-1" />
<!-- Contact and Enterprise Links -->
<div class="flex flex-col items-center">
<p class="text-sm text-text-secondary">
{{ $t('subscription.haveQuestions') }}
</p>
<div class="flex items-center gap-2">
<Button
:label="$t('subscription.contactUs')"
text
severity="secondary"
icon="pi pi-comments"
icon-pos="right"
class="h-6 p-1 text-sm text-text-secondary hover:text-white"
@click="handleContactUs"
/>
<span class="text-sm text-text-secondary">{{ $t('g.or') }}</span>
<Button
:label="$t('subscription.viewEnterprise')"
text
severity="secondary"
icon="pi pi-external-link"
icon-pos="right"
class="h-6 p-1 text-sm text-text-secondary hover:text-white"
@click="handleViewEnterprise"
/>
</div>
</div>
</div>
<div v-else class="legacy-dialog relative grid h-full grid-cols-5">
<!-- Custom close button -->
<Button
icon="pi pi-times"
@@ -7,7 +70,7 @@
rounded
class="absolute top-2.5 right-2.5 z-10 h-8 w-8 p-0 text-white hover:bg-white/20"
:aria-label="$t('g.close')"
@click="onClose"
@click="handleClose"
/>
<div
@@ -72,13 +135,19 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { computed, onBeforeUnmount, watch } from 'vue'
import CloudBadge from '@/components/topbar/CloudBadge.vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import StripePricingTable from '@/platform/cloud/subscription/components/StripePricingTable.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'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'
defineProps<{
const props = defineProps<{
onClose: () => void
}>()
@@ -86,19 +155,119 @@ const emit = defineEmits<{
close: [subscribed: boolean]
}>()
const { formattedMonthlyPrice } = useSubscription()
const { formattedMonthlyPrice, fetchStatus, isActiveSubscription } =
useSubscription()
const { featureFlag } = useFeatureFlags()
const subscriptionTiersEnabled = featureFlag(
'subscription_tiers_enabled',
false
)
const commandStore = useCommandStore()
const telemetry = useTelemetry()
const showStripePricingTable = computed(
() =>
subscriptionTiersEnabled.value &&
isCloud &&
window.__CONFIG__?.subscription_required
)
const POLL_INTERVAL_MS = 3000
const MAX_POLL_DURATION_MS = 5 * 60 * 1000
let pollInterval: number | null = null
let pollStartTime = 0
const stopPolling = () => {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
}
const startPolling = () => {
stopPolling()
pollStartTime = Date.now()
const poll = async () => {
try {
await fetchStatus()
} catch (error) {
console.error(
'[SubscriptionDialog] Failed to poll subscription status',
error
)
}
}
void poll()
pollInterval = window.setInterval(() => {
if (Date.now() - pollStartTime > MAX_POLL_DURATION_MS) {
stopPolling()
return
}
void poll()
}, POLL_INTERVAL_MS)
}
watch(
showStripePricingTable,
(enabled) => {
if (enabled) {
startPolling()
} else {
stopPolling()
}
},
{ immediate: true }
)
watch(
() => isActiveSubscription.value,
(isActive) => {
if (isActive && showStripePricingTable.value) {
emit('close', true)
}
}
)
const handleSubscribed = () => {
emit('close', true)
}
const handleClose = () => {
stopPolling()
props.onClose()
}
const handleContactUs = async () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'help_feedback',
is_external: true,
source: 'subscription'
})
await commandStore.execute('Comfy.ContactSupport')
}
const handleViewEnterprise = () => {
telemetry?.trackHelpResourceClicked({
resource_type: 'docs',
is_external: true,
source: 'subscription'
})
window.open('https://www.comfy.org/cloud/enterprise', '_blank')
}
onBeforeUnmount(() => {
stopPolling()
})
</script>
<style scoped>
:deep(.bg-comfy-menu-secondary) {
.legacy-dialog :deep(.bg-comfy-menu-secondary) {
background-color: transparent;
}
:deep(.p-button) {
.legacy-dialog :deep(.p-button) {
color: white;
}
</style>