mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-26 01:09:46 +00:00
feat: pass target tier to billing portal for subscription updates (#7692)
## Summary Pass target tier to billing portal API for deep linking to Stripe's subscription update confirmation screen when user has an active subscription. ## Changes - **What**: When a user with an active subscription clicks a tier in PricingTable, pass the target tier (including billing cycle) to `accessBillingPortal` which sends it as `target_tier` in the request body. This enables the backend to create a Stripe billing portal deep link directly to the subscription update confirmation screen. - **Dependencies**: Requires comfy-api PR for `POST /customers/billing` `target_tier` support ## Review Focus - PricingTable now differentiates between new subscriptions (checkout flow) and existing subscriptions (billing portal with deep link) - Type derivation uses `Parameters<typeof authStore.accessBillingPortal>[0]` to avoid duplicating the tier union (matches codebase pattern) - Registry types manually updated to include `target_tier` field (will be regenerated when API is deployed) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7692-feat-pass-target-tier-to-billing-portal-for-subscription-updates-2d06d73d365081b38fe4c81e95dce58c) by [Unito](https://www.unito.io) --------- Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -30,7 +30,9 @@ const mockAddCreditsResponse = {
|
||||
|
||||
const mockAccessBillingPortalResponse = {
|
||||
ok: true,
|
||||
statusText: 'OK'
|
||||
statusText: 'OK',
|
||||
json: () =>
|
||||
Promise.resolve({ billing_portal_url: 'https://billing.stripe.com/test' })
|
||||
}
|
||||
|
||||
vi.mock('vuefire', () => ({
|
||||
@@ -129,7 +131,7 @@ describe('useFirebaseAuthStore', () => {
|
||||
if (url.endsWith('/customers/credit')) {
|
||||
return Promise.resolve(mockAddCreditsResponse)
|
||||
}
|
||||
if (url.endsWith('/customers/billing-portal')) {
|
||||
if (url.endsWith('/customers/billing')) {
|
||||
return Promise.resolve(mockAccessBillingPortalResponse)
|
||||
}
|
||||
return Promise.reject(new Error('Unexpected API call'))
|
||||
@@ -542,4 +544,75 @@ describe('useFirebaseAuthStore', () => {
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('accessBillingPortal', () => {
|
||||
it('should call billing endpoint without body when no targetTier provided', async () => {
|
||||
const result = await store.accessBillingPortal()
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/customers/billing'),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer mock-id-token',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const callArgs = mockFetch.mock.calls.find((call) =>
|
||||
(call[0] as string).endsWith('/customers/billing')
|
||||
)
|
||||
expect(callArgs?.[1]).not.toHaveProperty('body')
|
||||
expect(result).toEqual({
|
||||
billing_portal_url: 'https://billing.stripe.com/test'
|
||||
})
|
||||
})
|
||||
|
||||
it('should include target_tier in request body when targetTier provided', async () => {
|
||||
await store.accessBillingPortal('creator')
|
||||
|
||||
const callArgs = mockFetch.mock.calls.find((call) =>
|
||||
(call[0] as string).endsWith('/customers/billing')
|
||||
)
|
||||
expect(callArgs?.[1]).toHaveProperty('body')
|
||||
expect(JSON.parse(callArgs?.[1]?.body as string)).toEqual({
|
||||
target_tier: 'creator'
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle different checkout tier formats', async () => {
|
||||
const tiers = [
|
||||
'standard',
|
||||
'creator',
|
||||
'pro',
|
||||
'standard-yearly',
|
||||
'creator-yearly',
|
||||
'pro-yearly'
|
||||
] as const
|
||||
|
||||
for (const tier of tiers) {
|
||||
mockFetch.mockClear()
|
||||
await store.accessBillingPortal(tier)
|
||||
|
||||
const callArgs = mockFetch.mock.calls.find((call) =>
|
||||
(call[0] as string).endsWith('/customers/billing')
|
||||
)
|
||||
expect(JSON.parse(callArgs?.[1]?.body as string)).toEqual({
|
||||
target_tier: tier
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error when API returns error response', async () => {
|
||||
mockFetch.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ message: 'Billing portal unavailable' })
|
||||
})
|
||||
)
|
||||
|
||||
await expect(store.accessBillingPortal()).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user