Guard downgrades via billing portal (#7813)

- add a reusable subscription tier ranking helper + unit test
- send pricing-table downgrades to the generic billing portal until
backend proration is fixed

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7813-Guard-downgrades-via-billing-portal-2da6d73d365081f0a202dd5699143332)
by [Unito](https://www.unito.io)
This commit is contained in:
Christian Byrne
2025-12-31 17:16:27 -08:00
committed by GitHub
parent 14528aad6e
commit 91f7a64513
3 changed files with 135 additions and 3 deletions

View File

@@ -258,6 +258,8 @@ import type {
TierKey,
TierPricing
} from '@/platform/cloud/subscription/constants/tierPricing'
import { isPlanDowngrade } from '@/platform/cloud/subscription/utils/subscriptionTierRank'
import type { BillingCycle } from '@/platform/cloud/subscription/utils/subscriptionTierRank'
import { isCloud } from '@/platform/distribution/types'
import {
FirebaseAuthStoreError,
@@ -269,8 +271,6 @@ type SubscriptionTier = components['schemas']['SubscriptionTier']
type CheckoutTierKey = Exclude<TierKey, 'founder'>
type CheckoutTier = CheckoutTierKey | `${CheckoutTierKey}-yearly`
type BillingCycle = 'monthly' | 'yearly'
const getCheckoutTier = (
tierKey: CheckoutTierKey,
billingCycle: BillingCycle
@@ -342,6 +342,15 @@ const currentTierKey = computed<TierKey | null>(() =>
subscriptionTier.value ? TIER_TO_KEY[subscriptionTier.value] : null
)
const currentPlanDescriptor = computed(() => {
if (!currentTierKey.value) return null
return {
tierKey: currentTierKey.value,
billingCycle: isYearlySubscription.value ? 'yearly' : 'monthly'
} as const
})
const isCurrentPlan = (tierKey: CheckoutTierKey): boolean => {
if (!currentTierKey.value) return false
@@ -443,7 +452,23 @@ const handleSubscribe = wrapWithErrorHandlingAsync(
if (isActiveSubscription.value) {
// Pass the target tier to create a deep link to subscription update confirmation
const checkoutTier = getCheckoutTier(tierKey, currentBillingCycle.value)
await accessBillingPortal(checkoutTier)
const targetPlan = {
tierKey,
billingCycle: currentBillingCycle.value
}
const downgrade =
currentPlanDescriptor.value &&
isPlanDowngrade({
current: currentPlanDescriptor.value,
target: targetPlan
})
if (downgrade) {
// TODO(COMFY-StripeProration): Remove once backend checkout creation mirrors portal proration ("change at billing end")
await accessBillingPortal()
} else {
await accessBillingPortal(checkoutTier)
}
} else {
const response = await initiateCheckout(tierKey)
if (response.checkout_url) {