Compare commits

...

2 Commits

Author SHA1 Message Date
Joel
3ed10a2774 fix: route billing interval changes to checkout 2026-09-30 15:59:50 +08:00
Joel
bb25b55e30 fix: route billing interval changes to checkout 2026-09-30 15:58:26 +08:00
4 changed files with 62 additions and 6 deletions

View File

@@ -91,7 +91,7 @@ const renderCloudPlanItem = ({
<CloudPlanItem
plan={plan}
billingInterval={billingInterval}
billing={{ currentPlan, isEducationDiscountEligible }}
billing={{ currentPlan, currentBillingInterval: 'month', isEducationDiscountEligible }}
/>
</>,
{ wrapper },

View File

@@ -179,7 +179,7 @@ it('keeps the current paid plan billing action available while education loads o
queries: { retry: false, retryOnMount: false, staleTime: Infinity },
})
seedFeatures(queryClient, {
billing: { subscription: { plan: 'professional' } },
billing: { subscription: { plan: 'professional', interval: 'month' } },
education: { enabled: true },
})
let rejectEducation!: (error: Error) => void
@@ -230,3 +230,56 @@ it('uses the visible billing label to name and toggle the switch', async () => {
expect(billingSwitch).not.toBeChecked()
expect(screen.getByText('$59')).toBeVisible()
})
it.each([
['professional', 'month', 'year'],
['professional', 'year', 'month'],
['team', 'month', 'year'],
['team', 'year', 'month'],
] as const)(
'checks out %s when changing from %s to %s',
async (plan, currentInterval, selectedInterval) => {
const user = userEvent.setup()
const { queryClient, show } = setup()
seedFeatures(queryClient, {
billing: { subscription: { plan, interval: currentInterval } },
})
const navigate = vi.spyOn(window.location, 'href', 'set').mockImplementation(() => {})
const fetchCheckout = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(JSON.stringify({ url: 'https://checkout.example.com' }), {
headers: { 'Content-Type': 'application/json' },
}),
)
show()
if (selectedInterval === 'year') await user.click(screen.getByRole('switch'))
await user.click(screen.getByRole('button', { name: 'billing.plansCommon.currentPlan' }))
await waitFor(() => expect(navigate).toHaveBeenCalledWith('https://checkout.example.com'))
const request = fetchCheckout.mock.calls[0]?.[0]
const requestUrl = request instanceof Request ? request.url : String(request)
expect(requestUrl).toContain('/billing/subscription')
expect(requestUrl).toContain(`plan=${plan}`)
expect(requestUrl).toContain(`interval=${selectedInterval}`)
expect(openBillingWindow).not.toHaveBeenCalled()
},
)
it.each(['month', 'year'] as const)(
'opens the portal for the current %s subscription',
async (interval) => {
const user = userEvent.setup()
const { queryClient, show } = setup()
seedFeatures(queryClient, {
billing: { subscription: { plan: 'professional', interval } },
})
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(JSON.stringify({ url: 'https://billing.example.com' }), {
headers: { 'Content-Type': 'application/json' },
}),
)
openBillingWindow.mockImplementation((getUrl: () => Promise<string>) => getUrl())
show()
if (interval === 'year') await user.click(screen.getByRole('switch'))
await user.click(screen.getByRole('button', { name: 'billing.plansCommon.currentPlan' }))
await waitFor(() => expect(openBillingWindow).toHaveResolvedWith('https://billing.example.com'))
},
)

View File

@@ -48,6 +48,7 @@ export function PricingContent() {
const billing = currentCloudPlan
? {
currentPlan: currentCloudPlan,
currentBillingInterval: features.billing.subscription.interval,
isEducationDiscountEligible,
}
: undefined

View File

@@ -36,6 +36,7 @@ type CloudPlanItemProps = {
billing:
| {
currentPlan: CloudPlan
currentBillingInterval: string
isEducationDiscountEligible: boolean | undefined
}
| undefined
@@ -54,11 +55,12 @@ export function CloudPlanItem({ plan, billingInterval, billing }: CloudPlanItemP
const planInfo = ALL_PLANS[plan]
const isCurrent = plan === currentPlan
const isCurrentPaidPlan = isCurrent && !isFreePlan
const isCurrentSubscription =
isCurrentPaidPlan && billingInterval === billing?.currentBillingInterval
const isPlanDisabled =
!billing ||
(!isCurrentPaidPlan &&
(billing.isEducationDiscountEligible === undefined ||
planInfo.level <= ALL_PLANS[billing.currentPlan].level))
(!isCurrentSubscription && billing.isEducationDiscountEligible === undefined) ||
(!isCurrentPaidPlan && planInfo.level <= ALL_PLANS[billing.currentPlan].level)
const isEducationDiscountSupportedPlan = plan === 'professional' && isYearly
const educationDiscountWarningText =
canManageBilling &&
@@ -92,7 +94,7 @@ export function CloudPlanItem({ plan, billingInterval, billing }: CloudPlanItemP
setIsPlanActionPending(true)
try {
if (isCurrentPaidPlan) {
if (isCurrentSubscription) {
if (!canManageBilling) {
toast.error(t(($) => $.buyPermissionDeniedTip, { ns: 'billing' }))
return