feat(billing): useStripeCheckout hook + post-redirect upgrade success

- useStripeCheckout: mutation + redirect full-page, pendingPriceType exposed
- PricingPage migré vers useStripeCheckout (suppression useMutation inline)
- useUpgradeSuccessHandler: détecte ?upgrade=success, invalide plan cache, clean URL
- UpgradeSuccessBanner: callout success dans DashboardPage
- Tests: 203 → 212 verts (+9)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-26 05:19:18 +03:00
parent 9edfbb3c95
commit bda7feb196
7 changed files with 371 additions and 25 deletions

View file

@ -17,9 +17,9 @@
*/
import { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import { usePlan } from '@/features/dashboard/hooks/usePlan'
import { createCheckoutSession, type PriceType } from '../api'
import { type PriceType } from '../api'
import { useStripeCheckout } from '../hooks/useStripeCheckout'
import { PlanCard, type PlanCardCta } from '../components/PlanCard'
type Plan = 'free' | 'standard' | 'premium'
@ -148,34 +148,21 @@ function buildCtaConfigs(
export function PricingPage() {
const { data: planData, isLoading } = usePlan()
const [pendingType, setPendingType] = useState<PriceType | null>(null)
const [errorByType, setErrorByType] = useState<Partial<Record<PriceType, string>>>({})
const mutation = useMutation({
mutationFn: createCheckoutSession,
onSuccess: (data) => {
// Redirection full-page vers Stripe Checkout. L'utilisateur reviendra
// sur /dashboard?upgrade=success après paiement (cf. backend success_url).
window.location.href = data.url
},
onError: (err: Error, priceType) => {
setErrorByType((prev) => ({
...prev,
[priceType]:
err.message || 'Impossible de démarrer le paiement. Réessayez dans quelques instants.',
}))
setPendingType(null)
},
})
const { checkout, pendingPriceType, error } = useStripeCheckout()
// Mémorise le dernier priceType cliqué pour rattacher l'erreur globale du
// hook à la bonne carte. Sprint 5c — `useStripeCheckout` n'expose qu'un
// `error` global (l'utilisateur ne clique qu'un CTA à la fois).
const [lastClicked, setLastClicked] = useState<PriceType | null>(null)
function handleUpgrade(priceType: PriceType) {
setErrorByType((prev) => ({ ...prev, [priceType]: undefined }))
setPendingType(priceType)
mutation.mutate(priceType)
setLastClicked(priceType)
checkout(priceType)
}
const plan = (planData?.plan as Plan | undefined) ?? 'free'
const ctaConfigs = buildCtaConfigs(plan, pendingType, handleUpgrade)
const ctaConfigs = buildCtaConfigs(plan, pendingPriceType, handleUpgrade)
const errorByType: Partial<Record<PriceType, string>> =
error && lastClicked ? { [lastClicked]: error } : {}
return (
<div className="mx-auto w-full max-w-[1100px] px-5 py-6 lg:px-9 lg:py-9">