fix(web): hoist hooks above early returns in TrialUrgencyBanner

useEffect and useCallback were declared after three `return null` guards,
so when a user's subscription transitioned into the trial+≤7-days window
React saw more hooks than the previous render and crashed with #310.
Hoisted all hooks above the single guard return; render and dismiss
behavior unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-05-13 21:41:16 +00:00
parent 98e6ac815e
commit 5b9af5a6a5

View File

@@ -36,71 +36,60 @@ export function TrialUrgencyBanner() {
});
const subscription = subData?.subscription;
const endDate = subscription?.endDate;
const days =
subscription?.status === "trial" && endDate
? Math.max(
0,
Math.ceil((new Date(endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)),
)
: null;
const visible = !isLoading && days !== null && days <= 7;
const dismissKey = endDate ? getDismissKey(endDate) : null;
// Hydrate dismissal state from localStorage on mount
useEffect(() => {
setMounted(true);
}, []);
// Guard: not loaded yet, or no subscription
if (isLoading || !subscription) return null;
// Guard: not trial or no end date
if (subscription.status !== "trial" || !subscription.endDate) return null;
const days = Math.max(
0,
Math.ceil((new Date(subscription.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)),
);
// Only show when 7 or fewer days remain
if (days > 7) return null;
const dismissKey = getDismissKey(subscription.endDate);
// Check localStorage on mount and when endDate changes
useEffect(() => {
if (!mounted) return;
if (!mounted || !dismissKey) return;
const stored = localStorage.getItem(dismissKey);
if (stored === "true") {
setDismissed(true);
}
}, [mounted, dismissKey]);
// Fire trial_urgency_banner_viewed once when the banner becomes visible
useEffect(() => {
if (!subscription || subscription.status !== "trial" || !subscription.endDate) return;
if (days > 7) return;
if (dismissed) return;
if (viewedRef.current) return;
if (!visible || dismissed || viewedRef.current || days === null) return;
viewedRef.current = true;
capture("trial_urgency_banner_viewed", {
days_remaining: days,
plan_name: subscription.plan?.name,
billing_period: subscription.billingPeriod,
plan_name: subscription?.plan?.name,
billing_period: subscription?.billingPeriod,
});
}, [subscription, days, dismissed]);
}, [visible, dismissed, days, subscription?.plan?.name, subscription?.billingPeriod]);
if (dismissed) return null;
const handleCTAClick = useCallback(() => {
if (days === null) return;
capture("trial_urgency_banner_cta_clicked", {
days_remaining: days,
plan_name: subscription?.plan?.name,
billing_period: subscription?.billingPeriod,
});
}, [days, subscription?.plan?.name, subscription?.billingPeriod]);
if (!visible || dismissed || days === null || !dismissKey) return null;
const handleDismiss = () => {
capture("trial_urgency_banner_dismissed", {
days_remaining: days,
plan_name: subscription.plan?.name,
billing_period: subscription.billingPeriod,
plan_name: subscription?.plan?.name,
billing_period: subscription?.billingPeriod,
});
localStorage.setItem(dismissKey, "true");
setDismissed(true);
};
const handleCTAClick = useCallback(() => {
capture("trial_urgency_banner_cta_clicked", {
days_remaining: days,
plan_name: subscription.plan?.name,
billing_period: subscription.billingPeriod,
});
}, [days, subscription.plan?.name, subscription.billingPeriod]);
return (
<div
role="alert"