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();
|
const recipes = await recipeService.getRecipes();
|
||||||
setUserRecipes(recipes);
|
setUserRecipes(recipes);
|
||||||
|
|
||||||
// Charge aussi l'état d'abonnement (non-bloquant)
|
// État d'abonnement en 2 temps (ambos non-bloquants) :
|
||||||
stripeService.getSubscription().then(setSubscription).catch(() => {/* ignore */});
|
// 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) {
|
} catch (err) {
|
||||||
console.error("Erreur lors du chargement du profil:", err);
|
console.error("Erreur lors du chargement du profil:", err);
|
||||||
setError("Impossible de charger les données du profil");
|
setError("Impossible de charger les données du profil");
|
||||||
@ -151,6 +177,21 @@ export default function Profile() {
|
|||||||
return () => clearTimeout(id);
|
return () => clearTimeout(id);
|
||||||
}, [success]);
|
}, [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 ---
|
// --- Handlers ---
|
||||||
|
|
||||||
const handleProfileSubmit = async (e: React.FormEvent) => {
|
const handleProfileSubmit = async (e: React.FormEvent) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user