+ )}
+
{build === "enterprise" && (
)}
+
{!isSidebarCollapsed && (
{loadFooterLinks() ? (
diff --git a/src/components/PaidFeaturesAlert.tsx b/src/components/PaidFeaturesAlert.tsx
index 95179ea78..933a9f5b9 100644
--- a/src/components/PaidFeaturesAlert.tsx
+++ b/src/components/PaidFeaturesAlert.tsx
@@ -10,7 +10,8 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { Tier } from "@server/types/Tiers";
import { useParams } from "next/navigation";
-const TIER_ORDER: Tier[] = ["tier1", "tier2", "tier3", "enterprise"];
+// const TIER_ORDER: Tier[] = ["tier1", "tier2", "tier3", "enterprise"];
+const TIER_ORDER: Tier[] = ["tier2", "tier3", "enterprise"];
const TIER_TRANSLATION_KEYS: Record<
Tier,
diff --git a/src/components/ShowTrialCard.tsx b/src/components/ShowTrialCard.tsx
new file mode 100644
index 000000000..1cc8e79f1
--- /dev/null
+++ b/src/components/ShowTrialCard.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { cn } from "@app/lib/cn";
+import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
+import { useParams } from "next/navigation";
+import Link from "next/link";
+import { ClockIcon, ArrowRight } from "lucide-react";
+import { ProgressBackwards } from "@app/components/ui/progress-backwards";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger
+} from "@app/components/ui/tooltip";
+import { useTranslations } from "next-intl";
+
+const TRIAL_DURATION_DAYS = 14;
+
+export default function ShowTrialCard({
+ isCollapsed
+}: {
+ isCollapsed?: boolean;
+}) {
+ const context = useSubscriptionStatusContext();
+ const params = useParams();
+ const orgId = params?.orgId as string | undefined;
+ const t = useTranslations();
+
+ const trialExpiresAt = context?.trialExpiresAt ?? null;
+
+ if (trialExpiresAt == null) return null;
+
+ const now = Date.now();
+ const remainingMs = trialExpiresAt - now;
+ const remainingDays = Math.max(0, Math.ceil(remainingMs / (1000 * 60 * 60 * 24)));
+ const totalMs = TRIAL_DURATION_DAYS * 24 * 60 * 60 * 1000;
+ const progressPct = Math.min(100, Math.max(0, ((now - (trialExpiresAt - totalMs)) / totalMs) * 100));
+ // Inverted: full bar at start, drains to empty as trial ends
+ const displayPct = 100 - progressPct;
+
+ const billingHref = orgId ? `/${orgId}/settings/billing` : "/";
+
+ if (isCollapsed) {
+ return (
+
+
+
+
+
+
+
+
+
+ {remainingDays === 0
+ ? t("trialExpired")
+ : t("trialDaysLeftShort", { days: remainingDays })}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {remainingDays === 0
+ ? t("trialExpired")
+ : t("trialActive")}
+
+
+
+
+
+ {remainingDays === 0
+ ? t("trialHasEnded")
+ : t("trialDaysRemaining", { count: remainingDays })}
+
+
+
{t("trialGoToBilling")}
+
+
+
+
+ );
+}
diff --git a/src/components/ui/progress-backwards.tsx b/src/components/ui/progress-backwards.tsx
new file mode 100644
index 000000000..e2482f0e2
--- /dev/null
+++ b/src/components/ui/progress-backwards.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import * as React from "react";
+import * as ProgressPrimitive from "@radix-ui/react-progress";
+import { cn } from "@app/lib/cn";
+import { cva, type VariantProps } from "class-variance-authority";
+
+const progressVariants = cva(
+ "border relative h-2 w-full overflow-hidden rounded-full",
+ {
+ variants: {
+ variant: {
+ default: "bg-muted",
+ success: "bg-muted",
+ warning: "bg-muted",
+ danger: "bg-muted"
+ }
+ },
+ defaultVariants: {
+ variant: "default"
+ }
+ }
+);
+
+const indicatorVariants = cva("h-full w-full flex-1 transition-all", {
+ variants: {
+ variant: {
+ default: "bg-primary",
+ success: "bg-green-500",
+ warning: "bg-yellow-500",
+ danger: "bg-red-500"
+ }
+ },
+ defaultVariants: {
+ variant: "default"
+ }
+});
+
+type ProgressProps = React.ComponentProps
&
+ VariantProps;
+
+function ProgressBackwards({ className, value, variant, ...props }: ProgressProps) {
+ return (
+
+
+
+ );
+}
+
+export { ProgressBackwards };
\ No newline at end of file
diff --git a/src/contexts/subscriptionStatusContext.ts b/src/contexts/subscriptionStatusContext.ts
index 73503da4f..6ba30fe42 100644
--- a/src/contexts/subscriptionStatusContext.ts
+++ b/src/contexts/subscriptionStatusContext.ts
@@ -10,6 +10,10 @@ type SubscriptionStatusContextType = {
subscribed: boolean;
/** True when org has exceeded plan limits (sites, users, etc.). Only set when build === saas. */
limitsExceeded: boolean;
+ /** Unix timestamp (ms) when the trial expires, or null if not in trial. */
+ trialExpiresAt: number | null;
+ /** True if the organization is currently in a trial period. */
+ isTrial: boolean;
};
const SubscriptionStatusContext = createContext<
diff --git a/src/providers/SubscriptionStatusProvider.tsx b/src/providers/SubscriptionStatusProvider.tsx
index a105e5d58..e1e781193 100644
--- a/src/providers/SubscriptionStatusProvider.tsx
+++ b/src/providers/SubscriptionStatusProvider.tsx
@@ -71,6 +71,19 @@ export function SubscriptionStatusProvider({
const limitsExceeded = subscriptionStatusState?.limitsExceeded ?? false;
+ const trialExpiresAt = (() => {
+ if (subscriptionStatusState?.subscriptions) {
+ for (const { subscription } of subscriptionStatusState.subscriptions) {
+ if (subscription.expiresAt != null) {
+ return subscription.expiresAt * 1000; // convert seconds to ms
+ }
+ }
+ }
+ return null;
+ })();
+
+ const isTrial = subscriptionStatusState?.subscriptions?.some(({ subscription }) => subscription.trial) ?? false;
+
return (
{children}