"use client"; import { useMemo } from "react"; import { useCurrentUserContext } from "../../user/contexts/CurrentUserContext"; import { getRoleId, isRolesConfigured } from "../../../roles"; import { getStripePublishableKey } from "../../../client/config"; export interface TrialSubscriptionStatus { status: "loading" | "trial" | "active" | "expired"; trialEndsAt: Date | null; daysRemaining: number; isGracePeriod: boolean; // Last 3 days isBlocked: boolean; } const TRIAL_DAYS = 14; const GRACE_DAYS = 3; const isAdministrator = (currentUser: any): boolean => { if (!currentUser || !isRolesConfigured()) return false; const adminRoleId = getRoleId().Administrator; return !!currentUser.roles?.some((role: any) => role.id === adminRoleId); }; export function useSubscriptionStatus(): TrialSubscriptionStatus { const { company, currentUser } = useCurrentUserContext(); return useMemo(() => { // Still loading user data - don't block yet if (currentUser === null) { return { status: "loading", trialEndsAt: null, daysRemaining: 0, isGracePeriod: false, isBlocked: false, }; } // If Stripe is not configured, treat as active subscription (no trial/blocking features) if (!getStripePublishableKey()) { return { status: "active", trialEndsAt: null, daysRemaining: 0, isGracePeriod: false, isBlocked: false, }; } // Administrator users are never blocked by trial if (isAdministrator(currentUser)) { return { status: "active", trialEndsAt: null, daysRemaining: 0, isGracePeriod: false, isBlocked: false, }; } // No company after loading is an AUTHENTICATION fault, not a billing one. // Reporting it as a block would hide the real problem, so fail open — the // same reasoning CreditsGuard documents on the API side. if (!company) { return { status: "expired", trialEndsAt: null, daysRemaining: 0, isGracePeriod: false, isBlocked: false, }; } // Has active subscription = never blocked if (company.isActiveSubscription) { return { status: "active", trialEndsAt: null, daysRemaining: 0, isGracePeriod: false, isBlocked: false, }; } // No active subscription — but that alone is NOT a block. The trial window // has to have elapsed too, and this mirrors the server rule in // apps/api .../campaign/entities/campaign-lock.cypher.ts. Keep the two in // step: if TRIAL_DAYS changes here it must change there. // // Blocking on the flag alone is wrong in practice. isActiveSubscription is // only written true once a Stripe subscription actually reaches // trialing/active, and that is not guaranteed — a real signup produced a // StripeSubscription stuck at status "incomplete", leaving the flag NULL and // a zero-day-old company blocked out of its own trial. const createdAt = new Date(company.createdAt); const trialEndsAt = new Date(createdAt); trialEndsAt.setDate(trialEndsAt.getDate() + TRIAL_DAYS); const now = new Date(); const msRemaining = trialEndsAt.getTime() - now.getTime(); const daysRemaining = Math.ceil(msRemaining / (1000 * 60 * 60 * 24)); const trialElapsed = daysRemaining <= 0; return { status: trialElapsed ? "expired" : "trial", trialEndsAt, daysRemaining: Math.max(0, daysRemaining), isGracePeriod: daysRemaining > 0 && daysRemaining <= GRACE_DAYS, isBlocked: trialElapsed, }; }, [company, currentUser]); }