diff --git a/src/modules/client/components/PlanComparisonTable.jsx b/src/modules/client/components/PlanComparisonTable.jsx index 42e37d2..99fe211 100644 --- a/src/modules/client/components/PlanComparisonTable.jsx +++ b/src/modules/client/components/PlanComparisonTable.jsx @@ -23,7 +23,9 @@ export default function PlanComparisonTable({ plans, myTier, tierMap, fmtCurrenc {plans.map((plan) => { const { label, cls } = resolveTierBadge(plan.tier, tierMap); const Icon = LucideIcons[tierMap[plan.tier]?.badge_icon] ?? Tag; - const isCurrent = myTier?.tier === plan.tier && myTier?.status === "active"; + // A user can hold more than one active tier concurrently — check + // membership in the full active set, not just the best one. + const isCurrent = (myTier?.active_tiers ?? []).some((t) => t.tier === plan.tier); return (
@@ -74,7 +76,9 @@ export default function PlanComparisonTable({ plans, myTier, tierMap, fmtCurrenc {plans.map((plan) => { - const isCurrent = myTier?.tier === plan.tier && myTier?.status === "active"; + // A user can hold more than one active tier concurrently — check + // membership in the full active set, not just the best one. + const isCurrent = (myTier?.active_tiers ?? []).some((t) => t.tier === plan.tier); return (
) : tierBadge && ( -
- {badgeImgUrl - ? - : TierIcon && - } - {tierBadge.label} +
+
+ {badgeImgUrl + ? + : TierIcon && + } + {tierBadge.label} +
+ {extraActiveTiers.length > 0 && ( + + )}
)}
@@ -344,6 +403,12 @@ function ClientNav() { + ) } diff --git a/src/modules/client/pages/Checkout.jsx b/src/modules/client/pages/Checkout.jsx index 5224868..0e60835 100644 --- a/src/modules/client/pages/Checkout.jsx +++ b/src/modules/client/pages/Checkout.jsx @@ -132,7 +132,9 @@ const Checkout = () => { const email = user?.email ?? ""; const isLoading = plansLoading || tierLoading; - const isCurrent = plan && myTier?.tier === plan.tier && myTier?.status === "active"; + // A user can hold more than one active tier concurrently — check membership + // in the full active set, not just the best one. + const isCurrent = !!plan && (myTier?.active_tiers ?? []).some((t) => t.tier === plan.tier); const style = TIER_STYLES[plan?.tier] ?? TIER_STYLES.free; const Icon = style.icon; const effectivePrice = plan ? Number(plan.price) : 0; diff --git a/src/modules/client/pages/PlanList.jsx b/src/modules/client/pages/PlanList.jsx index b4cf78f..528694a 100644 --- a/src/modules/client/pages/PlanList.jsx +++ b/src/modules/client/pages/PlanList.jsx @@ -89,7 +89,9 @@ const PlanCard = ({ plan, myTier, tierMap, onSelect, onView, onRefund, refundSec const { label: tierLabel, cls: badgeCls, rank } = resolveTierBadge(plan.tier, tierMap); const Icon = LucideIcons[tierMap[plan.tier]?.badge_icon] ?? Tag; const ring = rank > 0 ? "ring-2 ring-primary/30" : ""; - const isCurrent = myTier?.tier === plan.tier && myTier?.status === "active"; + // A user can hold more than one active tier concurrently (e.g. premium + + // exclusive) — check membership in the full active set, not just the best one. + const isCurrent = (myTier?.active_tiers ?? []).some((t) => t.tier === plan.tier); const duration = formatDuration(plan.duration_days, plan.duration_unit); const features = plan.features ?? []; const previewCourses = plan.courses?.slice(0, PREVIEW_COURSE_LIMIT) ?? []; @@ -349,7 +351,9 @@ export default function PlanList() { const [view, setView] = useState("grid"); const [refundPlan, setRefundPlan] = useState(null); // plan being refunded const [refundLoading, setRefundLoading] = useState(false); - const [refundSecsLeft, setRefundSecsLeft] = useState(0); + // Keyed by tier slug — a user can hold more than one active tier concurrently, + // and each one has its own independent refund window based on its own starts_at. + const [refundSecsLeftByTier, setRefundSecsLeftByTier] = useState({}); const refundTimerRef = useRef(null); useEffect(() => { @@ -364,19 +368,25 @@ export default function PlanList() { useEffect(() => { clearInterval(refundTimerRef.current); - if (!myTier?.starts_at) { setRefundSecsLeft(0); return; } + const activeTiers = myTier?.active_tiers ?? []; + if (!activeTiers.length) { setRefundSecsLeftByTier({}); return; } + const compute = () => { - const elapsed = Math.floor((Date.now() - new Date(myTier.starts_at).getTime()) / 1000); - return Math.max(0, REFUND_WINDOW_SECS - elapsed); + const map = {}; + for (const t of activeTiers) { + if (!t.starts_at) continue; + const elapsed = Math.floor((Date.now() - new Date(t.starts_at).getTime()) / 1000); + map[t.tier] = Math.max(0, REFUND_WINDOW_SECS - elapsed); + } + return map; }; - setRefundSecsLeft(compute()); + + setRefundSecsLeftByTier(compute()); refundTimerRef.current = setInterval(() => { - const left = compute(); - setRefundSecsLeft(left); - if (left === 0) clearInterval(refundTimerRef.current); + setRefundSecsLeftByTier(compute()); }, 1000); return () => clearInterval(refundTimerRef.current); - }, [myTier?.starts_at]); + }, [myTier?.active_tiers]); const handleViewPlan = (plan) => navigate(`/plans/view/${plan.plan_id}`); @@ -384,10 +394,19 @@ export default function PlanList() { const handleRefundClick = (plan) => setRefundPlan(plan); + // The plan being refunded may not be the user's "best" tier (myTier), since + // more than one can be active at once — resolve its own record for its own + // expires_at/refund window instead of assuming it matches myTier. + const refundPlanTier = (myTier?.active_tiers ?? []).find((t) => t.tier === refundPlan?.tier) ?? null; + const refundPlanSecsLeft = refundSecsLeftByTier[refundPlan?.tier] ?? 0; + const isOnlyActiveTier = (myTier?.active_tiers ?? []).length <= 1; + const handleConfirmRefund = async () => { setRefundLoading(true); try { - const { data } = await api.post("/client/tiers/checkout/refund"); + // plan_id disambiguates which active subscription to refund now that a + // user can hold more than one concurrently. + const { data } = await api.post("/client/tiers/checkout/refund", { plan_id: refundPlan?.plan_id }); toast(data.message ?? "Refund processed. Your access has been revoked."); setRefundPlan(null); resetMyTier(); @@ -461,7 +480,7 @@ export default function PlanList() { onSelect={handleSelectPlan} onView={handleViewPlan} onRefund={handleRefundClick} - refundSecsLeft={refundSecsLeft} + refundSecsLeft={refundSecsLeftByTier[plan.tier] ?? 0} /> ))} @@ -490,7 +509,7 @@ export default function PlanList() {