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:
Christian Byrne
2025-12-09 03:45:45 -08:00
committed by GitHub
parent 77e453db36
commit 8209f5a108
14 changed files with 651 additions and 18 deletions

View File

@@ -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
)

View File

@@ -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 =>

View File

@@ -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
}
})
}