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:
ordinarthur 2026-04-08 14:18:12 +02:00
parent b783627890
commit 1b3f53c086

View File

@ -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) => {