mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-26 09:19:43 +00:00
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)
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { STRIPE_PRICING_TABLE_SCRIPT_SRC } from '@/config/stripePricingTableConfig'
|
||||
|
||||
function useStripePricingTableLoaderInternal() {
|
||||
const isLoaded = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
let pendingPromise: Promise<void> | null = null
|
||||
|
||||
const resolveLoaded = () => {
|
||||
isLoaded.value = true
|
||||
isLoading.value = false
|
||||
pendingPromise = null
|
||||
}
|
||||
|
||||
const resolveError = (err: Error) => {
|
||||
error.value = err
|
||||
isLoading.value = false
|
||||
pendingPromise = null
|
||||
}
|
||||
|
||||
const loadScript = (): Promise<void> => {
|
||||
if (isLoaded.value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
if (pendingPromise) {
|
||||
return pendingPromise
|
||||
}
|
||||
|
||||
const existingScript = document.querySelector<HTMLScriptElement>(
|
||||
`script[src="${STRIPE_PRICING_TABLE_SCRIPT_SRC}"]`
|
||||
)
|
||||
|
||||
if (existingScript) {
|
||||
isLoading.value = true
|
||||
|
||||
pendingPromise = new Promise<void>((resolve, reject) => {
|
||||
existingScript.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
existingScript.dataset.loaded = 'true'
|
||||
resolveLoaded()
|
||||
resolve()
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
existingScript.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
const err = new Error('Stripe pricing table script failed to load')
|
||||
resolveError(err)
|
||||
reject(err)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
|
||||
// Check if script already loaded after attaching listeners
|
||||
if (
|
||||
existingScript.dataset.loaded === 'true' ||
|
||||
(existingScript as any).readyState === 'complete' ||
|
||||
(existingScript as any).complete
|
||||
) {
|
||||
existingScript.dataset.loaded = 'true'
|
||||
resolveLoaded()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
||||
return pendingPromise
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
pendingPromise = new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = STRIPE_PRICING_TABLE_SCRIPT_SRC
|
||||
script.async = true
|
||||
script.dataset.loaded = 'false'
|
||||
|
||||
script.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
script.dataset.loaded = 'true'
|
||||
resolveLoaded()
|
||||
resolve()
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
|
||||
script.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
const err = new Error('Stripe pricing table script failed to load')
|
||||
resolveError(err)
|
||||
reject(err)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
|
||||
return pendingPromise
|
||||
}
|
||||
|
||||
return {
|
||||
loadScript,
|
||||
isLoaded,
|
||||
isLoading,
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
export const useStripePricingTableLoader = createSharedComposable(
|
||||
useStripePricingTableLoaderInternal
|
||||
)
|
||||
@@ -9,11 +9,11 @@ import { MONTHLY_SUBSCRIPTION_PRICE } from '@/config/subscriptionPricesConfig'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import {
|
||||
FirebaseAuthStoreError,
|
||||
useFirebaseAuthStore
|
||||
} from '@/stores/firebaseAuthStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useSubscriptionCancellationWatcher } from './useSubscriptionCancellationWatcher'
|
||||
|
||||
type CloudSubscriptionCheckoutResponse = {
|
||||
@@ -37,7 +37,7 @@ function useSubscriptionInternal() {
|
||||
return subscriptionStatus.value?.is_active ?? false
|
||||
})
|
||||
const { reportError, accessBillingPortal } = useFirebaseAuthActions()
|
||||
const dialogService = useDialogService()
|
||||
const { showSubscriptionRequiredDialog } = useDialogService()
|
||||
|
||||
const { getAuthHeader } = useFirebaseAuthStore()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
@@ -102,7 +102,7 @@ function useSubscriptionInternal() {
|
||||
useTelemetry()?.trackSubscription('modal_opened')
|
||||
}
|
||||
|
||||
void dialogService.showSubscriptionRequiredDialog()
|
||||
void showSubscriptionRequiredDialog()
|
||||
}
|
||||
|
||||
const shouldWatchCancellation = (): boolean =>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { computed, defineAsyncComponent } from 'vue'
|
||||
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -7,6 +10,14 @@ const DIALOG_KEY = 'subscription-required'
|
||||
export const useSubscriptionDialog = () => {
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
const { flags } = useFeatureFlags()
|
||||
|
||||
const showStripeDialog = computed(
|
||||
() =>
|
||||
flags.subscriptionTiersEnabled &&
|
||||
isCloud &&
|
||||
window.__CONFIG__?.subscription_required
|
||||
)
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
@@ -25,7 +36,19 @@ export const useSubscriptionDialog = () => {
|
||||
onClose: hide
|
||||
},
|
||||
dialogComponentProps: {
|
||||
style: 'width: 700px;'
|
||||
style: showStripeDialog.value
|
||||
? 'width: min(1100px, 90vw); max-height: 90vh;'
|
||||
: 'width: 700px;',
|
||||
pt: showStripeDialog.value
|
||||
? {
|
||||
root: {
|
||||
class: '!rounded-[32px] overflow-visible'
|
||||
},
|
||||
content: {
|
||||
class: '!p-0 bg-transparent'
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user