feat(billing): Customer Portal + page Paramètres + Standard→Premium via portal

- useCustomerPortal hook (mutation + redirect full-page)
- AccountBillingSection: badge plan + bouton Gérer mon abonnement (Standard/Premium)
- ParametresPage: page conteneur /parametres avec section billing
- PricingPage: Standard→Premium redirige vers Customer Portal (prorata natif Stripe)
- Tests: 212 → 219 verts (+7)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-26 05:38:36 +03:00
parent bda7feb196
commit de16deede3
8 changed files with 453 additions and 17 deletions

View file

@ -0,0 +1,55 @@
/**
* Sprint 5d Hook Stripe Customer Portal.
*
* Wrap la mutation `createCustomerPortalSession` + redirect full-page vers
* la session Customer Portal Stripe. Utilisé par :
* - `AccountBillingSection` (page Paramètres) : bouton « Gérer mon abonnement ».
* - `PricingPage` (StandardPremium) : redirige vers le portal qui gère
* nativement l'upgrade prorata + confirmation du montant.
*
* Erreur 400 `NO_ACTIVE_SUBSCRIPTION` propagée telle quelle (le backend
* fournit déjà un message FR exploitable directement).
*/
import { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import { createCustomerPortalSession } from '../api'
export interface UseCustomerPortalResult {
openPortal: () => void
isLoading: boolean
error: string | null
}
const FALLBACK_ERROR_MESSAGE =
'Impossible douvrir lespace abonnement. Réessayez dans quelques instants.'
export function useCustomerPortal(): UseCustomerPortalResult {
const [error, setError] = useState<string | null>(null)
const mutation = useMutation({
mutationFn: createCustomerPortalSession,
onSuccess: (data) => {
// Redirect full-page : l'utilisateur reviendra sur ${APP_URL}/dashboard
// (cf. backend `return_url`). Le query param `?upgrade=success` n'est PAS
// ajouté par le portal — pas de banner de bienvenue dans ce flow,
// seulement une éventuelle invalidation au refresh manuel.
window.location.href = data.url
},
onError: (err: unknown) => {
const message = err instanceof Error && err.message ? err.message : FALLBACK_ERROR_MESSAGE
setError(message)
},
})
function openPortal(): void {
setError(null)
mutation.mutate()
}
return {
openPortal,
isLoading: mutation.isPending,
error,
}
}