← Files WixARCHIVED FILE
skills/wix-headless/references/astro/templates/stores/BackInStockForm.tsx
5.67 KB · Oct 4, 2026 · 12:02 UTC
import { useState } from "react";
import { backInStockNotifications } from "@wix/ecom";
import { WIX_STORES_BACK_IN_STOCK_APP_ID } from "../utils/back-in-stock";
interface BackInStockFormProps {
productId: string;
variantId?: string;
productName: string;
// Numeric price (number or numeric string). The Wix backend validates
// `itemDetails.price >= 0` against the parsed decimal, so currency-
// formatted strings ("$695") are stripped here before submission.
productPrice: number | string;
variantLabel?: string;
}
type Status =
| { kind: "idle" }
| { kind: "submitting" }
| { kind: "success"; alreadySubscribed?: boolean }
| { kind: "error"; message: string };
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function normalizePrice(raw: number | string): string {
const s =
typeof raw === "number" ? String(raw) : String(raw).replace(/[^0-9.]/g, "");
const n = Number(s);
return Number.isFinite(n) && n >= 0 ? s : "0";
}
function readErrorCode(err: unknown): string | undefined {
if (!err || typeof err !== "object") return undefined;
// Wix SDK errors expose details.applicationError.code; some shapes also
// surface a top-level details.code or http status. Try the common ones.
const e = err as Record<string, any>;
return (
e?.details?.applicationError?.code ??
e?.details?.code ??
e?.applicationError?.code ??
undefined
);
}
export default function BackInStockForm({
productId,
variantId,
productName,
productPrice,
variantLabel,
}: BackInStockFormProps) {
const [email, setEmail] = useState("");
const [status, setStatus] = useState<Status>({ kind: "idle" });
const submitting = status.kind === "submitting";
const success = status.kind === "success";
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
if (submitting) return;
if (!EMAIL_RE.test(email)) {
setStatus({ kind: "error", message: "Enter a valid email." });
return;
}
setStatus({ kind: "submitting" });
// Bare-fields rule: the SDK throws an opaque
// "TypeError: Failed to construct 'URL': Invalid URL" when the optional
// request.itemUrl or itemDetails.image fields are present, even with
// browser-valid URLs. Send only the four required fields:
// catalogReference, email, name, price. The dashboard's Back-in-Stock
// Requests view + the customer notification email both work without
// them; the email just won't have a thumbnail and the dashboard uses
// its default item URL.
const request = {
catalogReference: {
appId: WIX_STORES_BACK_IN_STOCK_APP_ID,
catalogItemId: productId,
...(variantId ? { options: { variantId } } : {}),
},
email,
};
const itemDetails = {
name: productName,
price: normalizePrice(productPrice),
};
try {
await backInStockNotifications.createBackInStockNotificationRequest(
request,
itemDetails,
);
setStatus({ kind: "success" });
} catch (err) {
const code = readErrorCode(err);
if (code === "BACK_IN_STOCK_NOTIFICATION_REQUEST_ALREADY_EXISTS") {
setStatus({ kind: "success", alreadySubscribed: true });
return;
}
if (code === "REQUEST_COLLECTION_DISABLED") {
setStatus({
kind: "error",
message: "Notifications aren't being collected right now.",
});
return;
}
console.error("[back-in-stock] subscribe failed:", err);
setStatus({
kind: "error",
message: "Something went wrong. Please try again.",
});
}
}
if (success) {
return (
<div className="back-in-stock-success" role="status" aria-live="polite">
<p className="back-in-stock-success-headline">
{status.alreadySubscribed
? "You're already on the list."
: "We'll let you know."}
</p>
<p className="back-in-stock-success-detail">
{status.alreadySubscribed
? `We had your note for ${productName}${variantLabel ? ` (${variantLabel})` : ""}. We'll email ${email} the moment it's back.`
: `We'll email ${email} the moment ${productName}${variantLabel ? ` (${variantLabel})` : ""} is back in stock.`}
</p>
</div>
);
}
return (
<form className="back-in-stock-form" onSubmit={handleSubmit} noValidate>
<div className="back-in-stock-headline">
<span className="back-in-stock-eyebrow">Currently sold out</span>
<p className="back-in-stock-lede">
Leave your email and we'll notify you the moment
{variantLabel ? ` ${variantLabel.toLowerCase()}` : ""} is back in
stock.
</p>
</div>
<label className="back-in-stock-label" htmlFor="back-in-stock-email">
Email
</label>
<div className="back-in-stock-row">
<input
id="back-in-stock-email"
type="email"
inputMode="email"
autoComplete="email"
required
placeholder="you@example.com"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (status.kind === "error") setStatus({ kind: "idle" });
}}
className="back-in-stock-input"
aria-invalid={status.kind === "error"}
disabled={submitting}
/>
<button
type="submit"
className="back-in-stock-submit"
disabled={submitting || email.trim().length === 0}
>
{submitting ? "Sending…" : "Notify me"}
</button>
</div>
{status.kind === "error" && (
<p className="back-in-stock-error" role="alert">
{status.message}
</p>
)}
</form>
);
}
SHA-256: bcb9c4303e732b07786058a3ed6af7f435735f0867a1c46e2ea3d2782fe0a45e