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:
parent
9edfbb3c95
commit
bda7feb196
7 changed files with 371 additions and 25 deletions
60
src/features/dashboard/hooks/useUpgradeSuccessHandler.ts
Normal file
60
src/features/dashboard/hooks/useUpgradeSuccessHandler.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Sprint 5c — Détection retour Stripe Checkout réussi.
|
||||
*
|
||||
* Lit `?upgrade=success` au mount de la page Dashboard, déclenche :
|
||||
* 1. invalidation du cache plan (`PLAN_QUERY_KEY`) → refetch automatique
|
||||
* du plan mis à jour par le webhook backend `checkout.session.completed`,
|
||||
* 2. affichage d'un banner de succès (consommé par DashboardPage),
|
||||
* 3. nettoyage du query param via `history.replaceState` (un refresh ne
|
||||
* doit pas re-déclencher le banner).
|
||||
*
|
||||
* Indépendant de react-router (lit `window.location.search` directement)
|
||||
* pour faciliter les tests sans MemoryRouter.
|
||||
*
|
||||
* Race connue (Sprint 5c) : le webhook Stripe peut arriver après le
|
||||
* redirect frontend (latence ~1-3 s). Si l'invalidation refetch trop tôt,
|
||||
* `usePlan()` retourne encore l'ancien plan. Mitigation MVP : message
|
||||
* neutre + refresh manuel résoud. Polling/retry à tracer en FTD si
|
||||
* problème observé en production.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { PLAN_QUERY_KEY } from '@/entities/user/query-keys'
|
||||
|
||||
export interface UseUpgradeSuccessHandlerResult {
|
||||
showSuccess: boolean
|
||||
dismiss: () => void
|
||||
}
|
||||
|
||||
const QUERY_PARAM = 'upgrade'
|
||||
const SUCCESS_VALUE = 'success'
|
||||
|
||||
export function useUpgradeSuccessHandler(): UseUpgradeSuccessHandlerResult {
|
||||
const [showSuccess, setShowSuccess] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get(QUERY_PARAM) !== SUCCESS_VALUE) return
|
||||
|
||||
setShowSuccess(true)
|
||||
void queryClient.invalidateQueries({ queryKey: PLAN_QUERY_KEY })
|
||||
|
||||
// Nettoyage URL : retire UNIQUEMENT le param `upgrade`, conserve les autres
|
||||
// (utm_*, etc.). `replaceState` ne déclenche pas de remount React Router.
|
||||
params.delete(QUERY_PARAM)
|
||||
const remaining = params.toString()
|
||||
const newSearch = remaining.length > 0 ? `?${remaining}` : ''
|
||||
const newUrl = window.location.pathname + newSearch + window.location.hash
|
||||
window.history.replaceState(null, '', newUrl)
|
||||
}, [queryClient])
|
||||
|
||||
function dismiss(): void {
|
||||
setShowSuccess(false)
|
||||
}
|
||||
|
||||
return { showSuccess, dismiss }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue