useTimedRelease
TheuseTimedRelease hook provides reactive state and methods for time-window based access, such as flash sales or limited-time shopping windows.
Signature
Copy
function useTimedRelease(timedReleaseId: string): UseTimedReleaseReturn;
Return Type
Copy
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
Copy
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.Copy
interface TimedRelease {
id: string;
openAt?: string;
closeAt?: string;
timeZone: string;
supportsGuest: boolean;
}
consumerState
The raw consumer state from the API.Copy
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.Copy
enter(variantId?: string): Promise<TimedReleaseConsumerState>
leave()
Leave the timed release before completing.Copy
leave(): Promise<void>
complete()
Mark the timed release as completed (after successful purchase).Copy
complete(): Promise<void>
refreshStatus()
Refresh the consumer state from the server.Copy
refreshStatus(): Promise<TimedReleaseConsumerState | null>
Basic Usage
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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 enteredtimed_release:left- Left the releasetimed_release:completed- Marked as completedtimed_release:status-updated- Status changedtimed_release:error- Error occurred
Copy
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
Copy
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;
}