← Files WixARCHIVED FILE

skills/wix-headless-templates/pricing-plans/project/src/components/pricing-plans/RequirePlan.tsx

3.33 KB · Oct 8, 2026 · 12:02 UTC

↓ Download file

See the change to this file →

// The gate for plan-holders-only content — wire as-is around anything a plan unlocks. It owns the
// rules a paywall gets wrong when rewritten: nothing gated renders while the session or the orders
// read is unsettled; a visitor gets the way in (log in, with a return to this page); a member
// without an active order for one of the plans gets the way to the plans; a failed read keeps the
// content closed and shows the message. Access itself is usePlanAccess → hasActiveOrderFor: an
// ACTIVE order for one of `planIds`, nothing here decides that.
//
// The session comes from the `members` vertical (its useMember()) — pass it in; this vertical does
// not ship it. Routing-free: plain <a> links, both hrefs overridable. Client-side only (the
// session is a browser cookie) — mount it client:only="react" on Astro, like members' RequireAuth.
import type { ReactNode } from "react";
import { usePlanAccess, type PlanAccessSession, type UsePlanAccessOptions } from "../../hooks/pricing-plans/usePlanAccess";

export interface RequirePlanProps extends UsePlanAccessOptions {
  /** The plans that unlock the content (any one of them). */
  planIds: string[];
  /** The members session: `useMember()` from the members vertical. */
  session: PlanAccessSession;
  children: ReactNode;
  /** Where a visitor logs in; `{returnTo}` is replaced with the current path. */
  loginHref?: string;
  /** Where a member without a plan goes to get one. */
  plansHref?: string;
  /** Rendered for a member without an active plan instead of the default prompt. */
  fallback?: ReactNode;
}

export default function RequirePlan({
  planIds,
  session,
  children,
  loginHref = "/login?returnTo={returnTo}",
  plansHref = "/plans",
  fallback,
  ...options
}: RequirePlanProps) {
  const { hasAccess, loading, error } = usePlanAccess(planIds, session, options);

  if (loading) {
    return (
      <div className="py-16 text-center text-muted-foreground" aria-busy="true">
        Loading…
      </div>
    );
  }

  if (hasAccess) return <>{children}</>;

  if (error) {
    return (
      <p className="py-8 text-center text-sm text-destructive" role="alert">
        {error}
      </p>
    );
  }

  if (!session.loggedIn) {
    const returnTo = typeof window !== "undefined" ? window.location.pathname : "/";
    return (
      <div className="mx-auto max-w-md rounded-lg border border-border p-8 text-center">
        <p className="text-sm text-muted-foreground">This content is for plan members. Log in to continue.</p>
        <a
          href={loginHref.replace("{returnTo}", encodeURIComponent(returnTo))}
          className="mt-4 inline-block rounded-control bg-primary px-5 py-2 text-sm font-medium text-primary-foreground no-underline transition-opacity hover:opacity-90"
        >
          Log in / Sign up
        </a>
      </div>
    );
  }

  return (
    <>
      {fallback ?? (
        <div className="mx-auto max-w-md rounded-lg border border-border p-8 text-center">
          <p className="text-sm text-muted-foreground">This content is included with a plan you don't have yet.</p>
          <a
            href={plansHref}
            className="mt-4 inline-block rounded-control bg-primary px-5 py-2 text-sm font-medium text-primary-foreground no-underline transition-opacity hover:opacity-90"
          >
            See plans
          </a>
        </div>
      )}
    </>
  );
}

SHA-256: b555b73f72babe86ccf6c453a5b3de134d9a00e8840e85a7f4c494c3df8c3b2b