Skip to content

Commit 8209f5a

Browse files
feat: add Stripe pricing table integration for subscription dialog (conditional on feature flag) (#7288)
Integrates Stripe's pricing table web component into the subscription dialog when the subscription_tiers_enabled feature flag is active. The implementation includes a new StripePricingTable component that loads Stripe's pricing table script and renders the table with proper error handling and loading states. The subscription dialog now displays the Stripe pricing table with contact us and enterprise links, using a 1100px width that balances multi-column layout with visual design. Configuration supports environment variables, remote config, and window config for the Stripe publishable key and pricing table ID. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7288-feat-add-Stripe-pricing-table-integration-for-subscription-dialog-conditional-on-featur-2c46d73d365081fa9d93c213df118996) by [Unito](https://www.unito.io)
1 parent 77e453d commit 8209f5a

File tree

14 files changed

+651
-18
lines changed

14 files changed

+651
-18
lines changed

.env_example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,7 @@ ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
4242
# SENTRY_AUTH_TOKEN=private-token # get from sentry
4343
# SENTRY_ORG=comfy-org
4444
# SENTRY_PROJECT=cloud-frontend-staging
45+
46+
# Stripe pricing table configuration (used by feature-flagged subscription tiers UI)
47+
# VITE_STRIPE_PUBLISHABLE_KEY=pk_test_123
48+
# VITE_STRIPE_PRICING_TABLE_ID=prctbl_123

global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ interface Window {
1313
max_upload_size?: number
1414
comfy_api_base_url?: string
1515
comfy_platform_base_url?: string
16+
stripe_publishable_key?: string
17+
stripe_pricing_table_id?: string
1618
firebase_config?: {
1719
apiKey: string
1820
authDomain: string
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
2+
3+
export const STRIPE_PRICING_TABLE_SCRIPT_SRC =
4+
'https://js.stripe.com/v3/pricing-table.js'
5+
6+
interface StripePricingTableConfig {
7+
publishableKey: string
8+
pricingTableId: string
9+
}
10+
11+
function getEnvValue(
12+
key: 'VITE_STRIPE_PUBLISHABLE_KEY' | 'VITE_STRIPE_PRICING_TABLE_ID'
13+
) {
14+
return import.meta.env[key]
15+
}
16+
17+
export function getStripePricingTableConfig(): StripePricingTableConfig {
18+
const publishableKey =
19+
remoteConfig.value.stripe_publishable_key ||
20+
window.__CONFIG__?.stripe_publishable_key ||
21+
getEnvValue('VITE_STRIPE_PUBLISHABLE_KEY') ||
22+
''
23+
24+
const pricingTableId =
25+
remoteConfig.value.stripe_pricing_table_id ||
26+
window.__CONFIG__?.stripe_pricing_table_id ||
27+
getEnvValue('VITE_STRIPE_PRICING_TABLE_ID') ||
28+
''
29+
30+
return {
31+
publishableKey,
32+
pricingTableId
33+
}
34+
}

src/locales/en/main.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"no": "No",
9898
"cancel": "Cancel",
9999
"close": "Close",
100+
"or": "or",
100101
"pressKeysForNewBinding": "Press keys for new binding",
101102
"defaultBanner": "default banner",
102103
"enableOrDisablePack": "Enable or disable pack",
@@ -1894,10 +1895,20 @@
18941895
"waitingForSubscription": "Complete your subscription in the new tab. We'll automatically detect when you're done!",
18951896
"subscribe": "Subscribe"
18961897
},
1898+
"pricingTable": {
1899+
"description": "Access cloud-powered ComfyUI workflows with straightforward, usage-based pricing.",
1900+
"loading": "Loading pricing options...",
1901+
"loadError": "We couldn't load the pricing table. Please refresh and try again.",
1902+
"missingConfig": "Stripe pricing table configuration missing. Provide the publishable key and pricing table ID via remote config or .env."
1903+
},
18971904
"subscribeToRun": "Subscribe",
18981905
"subscribeToRunFull": "Subscribe to Run",
18991906
"subscribeNow": "Subscribe Now",
19001907
"subscribeToComfyCloud": "Subscribe to Comfy Cloud",
1908+
"description": "Choose the best plan for you",
1909+
"haveQuestions": "Have questions or wondering about enterprise?",
1910+
"contactUs": "Contact us",
1911+
"viewEnterprise": "view enterprise",
19011912
"partnerNodesCredits": "Partner Nodes pricing table"
19021913
},
19031914
"userSettings": {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<template>
2+
<div
3+
ref="tableContainer"
4+
class="relative w-full rounded-[20px] border border-interface-stroke bg-interface-panel-background"
5+
>
6+
<div
7+
v-if="!hasValidConfig"
8+
class="absolute inset-0 flex items-center justify-center px-6 text-center text-sm text-text-secondary"
9+
data-testid="stripe-table-missing-config"
10+
>
11+
{{ $t('subscription.pricingTable.missingConfig') }}
12+
</div>
13+
<div
14+
v-else-if="loadError"
15+
class="absolute inset-0 flex items-center justify-center px-6 text-center text-sm text-text-secondary"
16+
data-testid="stripe-table-error"
17+
>
18+
{{ $t('subscription.pricingTable.loadError') }}
19+
</div>
20+
<div
21+
v-else-if="!isReady"
22+
class="absolute inset-0 flex items-center justify-center px-6 text-center text-sm text-text-secondary"
23+
data-testid="stripe-table-loading"
24+
>
25+
{{ $t('subscription.pricingTable.loading') }}
26+
</div>
27+
</div>
28+
</template>
29+
30+
<script setup lang="ts">
31+
import { computed, onBeforeUnmount, ref, watch } from 'vue'
32+
33+
import { getStripePricingTableConfig } from '@/config/stripePricingTableConfig'
34+
import { useStripePricingTableLoader } from '@/platform/cloud/subscription/composables/useStripePricingTableLoader'
35+
36+
const props = defineProps<{
37+
pricingTableId?: string
38+
publishableKey?: string
39+
}>()
40+
41+
const tableContainer = ref<HTMLDivElement | null>(null)
42+
const isReady = ref(false)
43+
const loadError = ref<string | null>(null)
44+
const lastRenderedKey = ref('')
45+
const stripeElement = ref<HTMLElement | null>(null)
46+
47+
const resolvedConfig = computed(() => {
48+
const fallback = getStripePricingTableConfig()
49+
50+
return {
51+
publishableKey: props.publishableKey || fallback.publishableKey,
52+
pricingTableId: props.pricingTableId || fallback.pricingTableId
53+
}
54+
})
55+
56+
const hasValidConfig = computed(() => {
57+
const { publishableKey, pricingTableId } = resolvedConfig.value
58+
return Boolean(publishableKey && pricingTableId)
59+
})
60+
61+
const { loadScript } = useStripePricingTableLoader()
62+
63+
const renderPricingTable = async () => {
64+
if (!tableContainer.value) return
65+
66+
const { publishableKey, pricingTableId } = resolvedConfig.value
67+
if (!publishableKey || !pricingTableId) {
68+
return
69+
}
70+
71+
const renderKey = `${publishableKey}:${pricingTableId}`
72+
if (renderKey === lastRenderedKey.value && isReady.value) {
73+
return
74+
}
75+
76+
try {
77+
await loadScript()
78+
loadError.value = null
79+
if (!tableContainer.value) {
80+
return
81+
}
82+
if (stripeElement.value) {
83+
stripeElement.value.remove()
84+
stripeElement.value = null
85+
}
86+
const stripeTable = document.createElement('stripe-pricing-table')
87+
stripeTable.setAttribute('publishable-key', publishableKey)
88+
stripeTable.setAttribute('pricing-table-id', pricingTableId)
89+
stripeTable.style.display = 'block'
90+
stripeTable.style.width = '100%'
91+
stripeTable.style.minHeight = '420px'
92+
tableContainer.value.appendChild(stripeTable)
93+
stripeElement.value = stripeTable
94+
lastRenderedKey.value = renderKey
95+
isReady.value = true
96+
} catch (error) {
97+
console.error('[StripePricingTable] Failed to load pricing table', error)
98+
loadError.value = (error as Error).message
99+
isReady.value = false
100+
}
101+
}
102+
103+
watch(
104+
[resolvedConfig, () => tableContainer.value],
105+
() => {
106+
if (!hasValidConfig.value) return
107+
if (!tableContainer.value) return
108+
void renderPricingTable()
109+
},
110+
{ immediate: true }
111+
)
112+
113+
onBeforeUnmount(() => {
114+
stripeElement.value?.remove()
115+
stripeElement.value = null
116+
})
117+
</script>

src/platform/cloud/subscription/components/SubscribeButton.vue

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424

2525
<script setup lang="ts">
2626
import Button from 'primevue/button'
27-
import { computed, onBeforeUnmount, ref } from 'vue'
27+
import { computed, onBeforeUnmount, ref, watch } from 'vue'
2828
29+
import { useFeatureFlags } from '@/composables/useFeatureFlags'
2930
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
3031
import { isCloud } from '@/platform/distribution/types'
3132
import { useTelemetry } from '@/platform/telemetry'
@@ -51,12 +52,18 @@ const emit = defineEmits<{
5152
subscribed: []
5253
}>()
5354
54-
const { subscribe, isActiveSubscription, fetchStatus } = useSubscription()
55+
const { subscribe, isActiveSubscription, fetchStatus, showSubscriptionDialog } =
56+
useSubscription()
57+
const { flags } = useFeatureFlags()
58+
const shouldUseStripePricing = computed(
59+
() => isCloud && Boolean(flags.subscriptionTiersEnabled)
60+
)
5561
const telemetry = useTelemetry()
5662
5763
const isLoading = ref(false)
5864
const isPolling = ref(false)
5965
let pollInterval: number | null = null
66+
const isAwaitingStripeSubscription = ref(false)
6067
6168
const POLL_INTERVAL_MS = 3000 // Poll every 3 seconds
6269
const MAX_POLL_DURATION_MS = 5 * 60 * 1000 // Stop polling after 5 minutes
@@ -102,11 +109,27 @@ const stopPolling = () => {
102109
isLoading.value = false
103110
}
104111
112+
watch(
113+
[isAwaitingStripeSubscription, isActiveSubscription],
114+
([awaiting, isActive]) => {
115+
if (shouldUseStripePricing.value && awaiting && isActive) {
116+
emit('subscribed')
117+
isAwaitingStripeSubscription.value = false
118+
}
119+
}
120+
)
121+
105122
const handleSubscribe = async () => {
106123
if (isCloud) {
107124
useTelemetry()?.trackSubscription('subscribe_clicked')
108125
}
109126
127+
if (shouldUseStripePricing.value) {
128+
isAwaitingStripeSubscription.value = true
129+
showSubscriptionDialog()
130+
return
131+
}
132+
110133
isLoading.value = true
111134
try {
112135
await subscribe()
@@ -120,5 +143,6 @@ const handleSubscribe = async () => {
120143
121144
onBeforeUnmount(() => {
122145
stopPolling()
146+
isAwaitingStripeSubscription.value = false
123147
})
124148
</script>

0 commit comments

Comments
 (0)