fix(profile): force Stripe sync on mount + on window focus
Problem reported: user cancelled their subscription via the generic
'Gérer' portal, but the backend still showed the old subscription.
Root cause: stripe listen wasn't running, so the
customer.subscription.updated webhook never reached the server. The
Profile page was only reading from the DB (getSubscription), so it
stayed stale forever.
Fix: call stripeService.syncSubscription() alongside getSubscription()
at mount time. The fast DB read still happens first (instant display),
then the Stripe API call updates the state if anything has drifted.
Also add a window.addEventListener('focus', ...) listener that re-syncs
every time the user tabs back to the app — handles the common pattern
of opening Stripe portal in a new tab, doing something, then coming
back.
This makes the Profile self-healing even without a webhook setup in dev.
Production should still run the webhook for other apps/users, but this
fallback ensures individual users see the truth on their next visit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b783627890
commit
1b3f53c086
@ -128,8 +128,34 @@ export default function Profile() {
|
||||
const recipes = await recipeService.getRecipes();
|
||||
setUserRecipes(recipes);
|
||||
|
||||
// Charge aussi l'état d'abonnement (non-bloquant)
|
||||
stripeService.getSubscription().then(setSubscription).catch(() => {/* ignore */});
|
||||
// État d'abonnement en 2 temps (ambos non-bloquants) :
|
||||
// 1. Lecture rapide depuis la DB (affichage immédiat)
|
||||
// 2. Sync authoritative avec Stripe en arrière-plan. Garantit que
|
||||
// l'UI reflète l'état réel même si un webhook a été raté
|
||||
// (ex : stripe listen pas démarré en dev).
|
||||
stripeService
|
||||
.getSubscription()
|
||||
.then(setSubscription)
|
||||
.catch(() => {/* ignore */});
|
||||
|
||||
stripeService
|
||||
.syncSubscription()
|
||||
.then((synced) => {
|
||||
setSubscription((prev) => {
|
||||
// Ne met à jour que si quelque chose a vraiment changé,
|
||||
// pour éviter un flash de re-render
|
||||
if (
|
||||
!prev ||
|
||||
prev.plan !== synced.plan ||
|
||||
prev.status !== synced.status ||
|
||||
prev.hasActiveSubscription !== synced.hasActiveSubscription
|
||||
) {
|
||||
return synced;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
})
|
||||
.catch(() => {/* ignore */});
|
||||
} catch (err) {
|
||||
console.error("Erreur lors du chargement du profil:", err);
|
||||
setError("Impossible de charger les données du profil");
|
||||
@ -151,6 +177,21 @@ export default function Profile() {
|
||||
return () => clearTimeout(id);
|
||||
}, [success]);
|
||||
|
||||
// Resync l'abonnement quand l'utilisateur revient sur l'onglet. Gère
|
||||
// le cas classique : ouvre Stripe portal dans un nouvel onglet, annule,
|
||||
// ferme l'onglet, revient sur l'app → on veut refléter l'annulation
|
||||
// immédiatement.
|
||||
useEffect(() => {
|
||||
const onFocus = () => {
|
||||
stripeService
|
||||
.syncSubscription()
|
||||
.then(setSubscription)
|
||||
.catch(() => {/* ignore */});
|
||||
};
|
||||
window.addEventListener("focus", onFocus);
|
||||
return () => window.removeEventListener("focus", onFocus);
|
||||
}, []);
|
||||
|
||||
// --- Handlers ---
|
||||
|
||||
const handleProfileSubmit = async (e: React.FormEvent) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user