Skip to main content

useTimedRelease

The useTimedRelease hook provides reactive state and methods for time-window based access, such as flash sales or limited-time shopping windows.

Signature

function useTimedRelease(timedReleaseId: string): UseTimedReleaseReturn;

Return Type

interface UseTimedReleaseReturn {
  // State
  timedRelease: TimedRelease | null;
  consumerState: TimedReleaseConsumerState | null;
  status: UseTimedReleaseClientStatus;
  endTime: Date | null;
  timeRemaining: number | null;
  isEntered: boolean;
  hasCompleted: boolean;
  isLoading: boolean;
  error: Error | null;

  // Actions
  enter: (variantId?: string) => Promise<TimedReleaseConsumerState>;
  leave: () => Promise<void>;
  complete: () => Promise<void>;
  refreshStatus: () => Promise<TimedReleaseConsumerState | null>;
}

Client Status

type UseTimedReleaseClientStatus =
  | "idle" // Initial state
  | "open" // Window is open, can enter
  | "entered" // Currently in the release window
  | "completed" // Successfully completed
  | "left" // Left the release
  | "expired" // Entry expired
  | "ended" // Time window ended
  | "loading" // Loading state
  | "error"; // Error state

State Properties

timedRelease

The timed release details from the API.
interface TimedRelease {
  id: string;
  openAt?: string;
  closeAt?: string;
  timeZone: string;
  supportsGuest: boolean;
}

consumerState

The raw consumer state from the API.
type TimedReleaseConsumerState =
  | TimedReleaseNotEnteredState
  | TimedReleaseEnteredState
  | TimedReleaseCompletedState
  | TimedReleaseLeftState;

timeRemaining

Milliseconds until the release ends. Updated every second.

isEntered / hasCompleted

Convenience booleans for checking status.

Actions

enter(variantId?)

Enter the timed release. Optionally specify a variant.
enter(variantId?: string): Promise<TimedReleaseConsumerState>

leave()

Leave the timed release before completing.
leave(): Promise<void>

complete()

Mark the timed release as completed (after successful purchase).
complete(): Promise<void>

refreshStatus()

Refresh the consumer state from the server.
refreshStatus(): Promise<TimedReleaseConsumerState | null>

Basic Usage

import { useTimedRelease, useFanfareAuth } from "@waitify-io/fanfare-sdk-react";

function FlashSalePage() {
  const { isAuthenticated, guest } = useFanfareAuth();
  const { timedRelease, status, timeRemaining, isEntered, hasCompleted, isLoading, error, enter, leave, complete } =
    useTimedRelease("tr_flash_sale");

  const handleEnter = async () => {
    if (!isAuthenticated) {
      await guest();
    }
    await enter();
  };

  if (error) {
    return <div className="error">Error: {error.message}</div>;
  }

  return (
    <div className="flash-sale">
      <h1>Flash Sale</h1>

      {status === "open" && (
        <div className="open-state">
          <p>The sale is live!</p>
          <button onClick={handleEnter}>Start Shopping</button>
        </div>
      )}

      {status === "entered" && (
        <div className="shopping-state">
          <Countdown timeRemaining={timeRemaining} />
          <p>You are in! Shop now before time runs out.</p>
          <a href="/shop" className="shop-button">
            Go to Shop
          </a>
          <button onClick={leave} className="secondary">
            Exit Sale
          </button>
        </div>
      )}

      {status === "completed" && (
        <div className="completed-state">
          <h2>Thank you!</h2>
          <p>Your purchase is confirmed.</p>
        </div>
      )}

      {status === "ended" && (
        <div className="ended-state">
          <p>This flash sale has ended.</p>
        </div>
      )}
    </div>
  );
}

function Countdown({ timeRemaining }: { timeRemaining: number | null }) {
  if (!timeRemaining || timeRemaining <= 0) {
    return <div className="countdown expired">Time is up!</div>;
  }

  const minutes = Math.floor(timeRemaining / 60000);
  const seconds = Math.floor((timeRemaining % 60000) / 1000);

  return (
    <div className="countdown">
      <span className="label">Time Remaining:</span>
      <span className="time">
        {minutes}:{seconds.toString().padStart(2, "0")}
      </span>
    </div>
  );
}

Integration with Checkout

function TimedReleaseCheckout({ timedReleaseId }: { timedReleaseId: string }) {
  const { status, isEntered, complete } = useTimedRelease(timedReleaseId);
  const [checkoutComplete, setCheckoutComplete] = useState(false);

  const handleCheckoutSuccess = async () => {
    // After successful payment
    await complete();
    setCheckoutComplete(true);
  };

  if (!isEntered) {
    return <div>You need to enter the timed release first.</div>;
  }

  if (checkoutComplete) {
    return (
      <div className="success">
        <h2>Order Confirmed!</h2>
        <p>Thank you for your purchase.</p>
      </div>
    );
  }

  return (
    <div className="checkout">
      <CheckoutForm onSuccess={handleCheckoutSuccess} />
    </div>
  );
}

Product Variant Selection

function VariantSelection({
  timedReleaseId,
  variants,
}: {
  timedReleaseId: string;
  variants: Array<{ id: string; name: string }>;
}) {
  const { enter, isLoading } = useTimedRelease(timedReleaseId);
  const [selectedVariant, setSelectedVariant] = useState<string | null>(null);

  const handleSelectAndEnter = async (variantId: string) => {
    setSelectedVariant(variantId);
    await enter(variantId);
  };

  return (
    <div className="variant-selection">
      <h3>Select Your Option</h3>
      <div className="variants">
        {variants.map((variant) => (
          <button
            key={variant.id}
            onClick={() => handleSelectAndEnter(variant.id)}
            disabled={isLoading}
            className={selectedVariant === variant.id ? "selected" : ""}
          >
            {variant.name}
          </button>
        ))}
      </div>
    </div>
  );
}

Urgency Display

function UrgencyBanner({ timedReleaseId }: { timedReleaseId: string }) {
  const { status, timeRemaining } = useTimedRelease(timedReleaseId);

  if (status !== "entered" || !timeRemaining) {
    return null;
  }

  const minutes = Math.floor(timeRemaining / 60000);

  let urgencyClass = "";
  let message = "";

  if (minutes <= 1) {
    urgencyClass = "critical";
    message = "Less than 1 minute left!";
  } else if (minutes <= 5) {
    urgencyClass = "warning";
    message = `Only ${minutes} minutes remaining!`;
  } else {
    urgencyClass = "normal";
    message = `${minutes} minutes to complete your purchase`;
  }

  return (
    <div className={`urgency-banner ${urgencyClass}`}>
      <span className="icon">Clock</span>
      <span className="message">{message}</span>
    </div>
  );
}

Pre-Sale Countdown

function PreSaleCountdown({ timedReleaseId }: { timedReleaseId: string }) {
  const { timedRelease, status } = useTimedRelease(timedReleaseId);
  const [countdown, setCountdown] = useState<string>("");

  useEffect(() => {
    if (!timedRelease?.openAt) return;

    const openTime = new Date(timedRelease.openAt).getTime();

    const updateCountdown = () => {
      const now = Date.now();
      const diff = openTime - now;

      if (diff <= 0) {
        setCountdown("Starting now!");
        return;
      }

      const days = Math.floor(diff / (1000 * 60 * 60 * 24));
      const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
      const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
      const seconds = Math.floor((diff % (1000 * 60)) / 1000);

      if (days > 0) {
        setCountdown(`${days}d ${hours}h ${minutes}m`);
      } else {
        setCountdown(`${hours}h ${minutes}m ${seconds}s`);
      }
    };

    updateCountdown();
    const interval = setInterval(updateCountdown, 1000);

    return () => clearInterval(interval);
  }, [timedRelease?.openAt]);

  if (status !== "idle") {
    return null; // Release has started
  }

  return (
    <div className="pre-sale-countdown">
      <h3>Sale Starts In</h3>
      <div className="countdown-display">{countdown}</div>
    </div>
  );
}

Event Handling

The hook subscribes to these events:
  • timed_release:entered - Successfully entered
  • timed_release:left - Left the release
  • timed_release:completed - Marked as completed
  • timed_release:status-updated - Status changed
  • timed_release:error - Error occurred
function TimedReleaseWithNotifications({ timedReleaseId }: { timedReleaseId: string }) {
  const { status, timeRemaining } = useTimedRelease(timedReleaseId);

  useEffect(() => {
    if (status === "entered" && timeRemaining && timeRemaining < 60000) {
      // Show warning when less than 1 minute
      showNotification("Less than 1 minute remaining!");
    }
  }, [status, timeRemaining]);

  return <TimedReleaseDisplay id={timedReleaseId} />;
}

TypeScript

import { useTimedRelease } from "@waitify-io/fanfare-sdk-react";
import type {
  TimedRelease,
  TimedReleaseConsumerState,
  UseTimedReleaseClientStatus,
} from "@waitify-io/fanfare-sdk-react";

function TypedTimedRelease({ id }: { id: string }) {
  const {
    timedRelease,
    status,
    enter,
    complete,
  }: {
    timedRelease: TimedRelease | null;
    status: UseTimedReleaseClientStatus;
    enter: (variantId?: string) => Promise<TimedReleaseConsumerState>;
    complete: () => Promise<void>;
  } = useTimedRelease(id);

  return null;
}