@wix/agent-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +29 -0
- package/skills/wix-app/SKILL.md +344 -0
- package/skills/wix-app/references/APP_IDENTIFIERS.md +35 -0
- package/skills/wix-app/references/APP_MARKET_REVIEW.md +311 -0
- package/skills/wix-app/references/APP_VALIDATION.md +171 -0
- package/skills/wix-app/references/BACKEND_API.md +233 -0
- package/skills/wix-app/references/BACKEND_EVENT.md +116 -0
- package/skills/wix-app/references/CODE_QUALITY.md +33 -0
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +587 -0
- package/skills/wix-app/references/DASHBOARD_MENU_PLUGIN.md +157 -0
- package/skills/wix-app/references/DASHBOARD_MODAL.md +283 -0
- package/skills/wix-app/references/DASHBOARD_PAGE.md +403 -0
- package/skills/wix-app/references/DASHBOARD_PLUGIN.md +205 -0
- package/skills/wix-app/references/DATA_COLLECTION.md +451 -0
- package/skills/wix-app/references/DOCUMENTATION.md +42 -0
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +65 -0
- package/skills/wix-app/references/EMBEDDED_SCRIPT.md +468 -0
- package/skills/wix-app/references/EXTENSION_REGISTRATION.md +96 -0
- package/skills/wix-app/references/SERVICE_PLUGIN.md +235 -0
- package/skills/wix-app/references/SITE_PLUGIN.md +436 -0
- package/skills/wix-app/references/STORES_VERSIONING.md +334 -0
- package/skills/wix-app/references/backend-event/COMMON-EVENTS.md +84 -0
- package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +345 -0
- package/skills/wix-app/references/dashboard-menu-plugin/blog-slots.md +71 -0
- package/skills/wix-app/references/dashboard-menu-plugin/bookings-slots.md +58 -0
- package/skills/wix-app/references/dashboard-menu-plugin/crm-slots.md +8 -0
- package/skills/wix-app/references/dashboard-menu-plugin/ecommerce-slots.md +17 -0
- package/skills/wix-app/references/dashboard-menu-plugin/events-slots.md +29 -0
- package/skills/wix-app/references/dashboard-menu-plugin/restaurants-slots.md +27 -0
- package/skills/wix-app/references/dashboard-menu-plugin/stores-slots.md +35 -0
- package/skills/wix-app/references/dashboard-page/API_SPEC.md +138 -0
- package/skills/wix-app/references/dashboard-page/DASHBOARD_API.md +573 -0
- package/skills/wix-app/references/dashboard-page/DYNAMIC_PARAMETERS.md +269 -0
- package/skills/wix-app/references/dashboard-page/ECOM_NAVIGATION.md +164 -0
- package/skills/wix-app/references/dashboard-plugin/SLOTS.md +294 -0
- package/skills/wix-app/references/data-collection/WIX_DATA.md +270 -0
- package/skills/wix-app/references/data-collection/extension-template.ts +107 -0
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +138 -0
- package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +454 -0
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +165 -0
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +576 -0
- package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +170 -0
- package/skills/wix-app/references/editor-react-component/EDIT-FLOW.md +6 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +25 -0
- package/skills/wix-app/references/editor-react-component/PROPS-VS-CSS.md +203 -0
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +167 -0
- package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +137 -0
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +112 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +118 -0
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +87 -0
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +86 -0
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +135 -0
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +79 -0
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +68 -0
- package/skills/wix-app/references/site-plugin/EXAMPLES.md +311 -0
- package/skills/wix-app/references/site-plugin/SLOTS.md +263 -0
- package/skills/wix-design-system/SKILL.md +129 -0
- package/skills/wix-design-system/references/file-structure.md +56 -0
- package/skills/wix-design-system/scripts/wds.cjs +531 -0
- package/skills/wix-headless/SKILL.md +418 -0
- package/skills/wix-headless/references/DISCOVERY.md +332 -0
- package/skills/wix-headless/references/ORCHESTRATION.md +269 -0
- package/skills/wix-headless/references/SETUP.md +154 -0
- package/skills/wix-headless/references/blog/BLOG_CONTENT.md +152 -0
- package/skills/wix-headless/references/blog/BLOG_PAGES.md +299 -0
- package/skills/wix-headless/references/blog/BLOG_SETUP.md +218 -0
- package/skills/wix-headless/references/blog/INSTRUCTIONS.md +32 -0
- package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +373 -0
- package/skills/wix-headless/references/cms/FAQ_KNOWLEDGE_BASE.md +229 -0
- package/skills/wix-headless/references/cms/INSTRUCTIONS.md +40 -0
- package/skills/wix-headless/references/cms/PORTFOLIO.md +454 -0
- package/skills/wix-headless/references/cms/RESOURCE_LIBRARY.md +356 -0
- package/skills/wix-headless/references/cms/TEAM_DIRECTORY.md +299 -0
- package/skills/wix-headless/references/commands/install-app.md +42 -0
- package/skills/wix-headless/references/commands/known-apps.json +5 -0
- package/skills/wix-headless/references/commands/mcp-bootstrap.md +28 -0
- package/skills/wix-headless/references/designer/INSTRUCTIONS.md +828 -0
- package/skills/wix-headless/references/ecom/CART_PAGES.md +120 -0
- package/skills/wix-headless/references/ecom/CART_WIRING.md +103 -0
- package/skills/wix-headless/references/ecom/COMPONENTS_CSS.md +132 -0
- package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +59 -0
- package/skills/wix-headless/references/forms/CONTACT_FORM.md +477 -0
- package/skills/wix-headless/references/forms/INSTRUCTIONS.md +43 -0
- package/skills/wix-headless/references/gift-cards/COMPONENTS.md +119 -0
- package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +51 -0
- package/skills/wix-headless/references/gift-cards/PAGES.md +129 -0
- package/skills/wix-headless/references/images/INSTRUCTIONS.md +359 -0
- package/skills/wix-headless/references/shared/BUILD_NOISE.md +8 -0
- package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +185 -0
- package/skills/wix-headless/references/shared/IMPLEMENTER.md +155 -0
- package/skills/wix-headless/references/shared/MCP_PREFIX.md +105 -0
- package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +97 -0
- package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +423 -0
- package/skills/wix-headless/references/shared/STYLING.md +117 -0
- package/skills/wix-headless/references/stores/BACK_IN_STOCK.md +178 -0
- package/skills/wix-headless/references/stores/CATEGORY_PAGES.md +86 -0
- package/skills/wix-headless/references/stores/COMPONENTS_CSS.md +129 -0
- package/skills/wix-headless/references/stores/HOME_AND_NAV.md +227 -0
- package/skills/wix-headless/references/stores/INSTRUCTIONS.md +96 -0
- package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +249 -0
- package/skills/wix-headless/references/stores/PRODUCT_PAGES.md +213 -0
- package/skills/wix-headless/references/stores/SHARED_WIRING.md +164 -0
- package/skills/wix-headless/references/verticals/_schema.json +160 -0
- package/skills/wix-headless/references/verticals/_schema.md +185 -0
- package/skills/wix-headless/references/verticals/blog.md +125 -0
- package/skills/wix-headless/references/verticals/cms.md +121 -0
- package/skills/wix-headless/references/verticals/ecom.md +96 -0
- package/skills/wix-headless/references/verticals/forms.md +106 -0
- package/skills/wix-headless/references/verticals/gift-cards.md +121 -0
- package/skills/wix-headless/references/verticals/stores.md +167 -0
- package/skills/wix-headless/scripts/check-manifest.mjs +198 -0
- package/skills/wix-headless/scripts/copy-utility-templates.mjs +153 -0
- package/skills/wix-headless/scripts/emit-design-tokens.mjs +77 -0
- package/skills/wix-headless/scripts/finalize-run-json.mjs +141 -0
- package/skills/wix-headless/scripts/init-site-json.mjs +63 -0
- package/skills/wix-headless/scripts/patch-decorative-slots.mjs +124 -0
- package/skills/wix-headless/scripts/plan-entity-image-waves.mjs +190 -0
- package/skills/wix-headless/scripts/preview.sh +45 -0
- package/skills/wix-headless/scripts/release.sh +73 -0
- package/skills/wix-headless/scripts/scaffold.sh +53 -0
- package/skills/wix-headless/scripts/seed-utilities.sh +55 -0
- package/skills/wix-headless/shared-utilities/analytics.ts +40 -0
- package/skills/wix-headless/shared-utilities/ricos.ts +179 -0
- package/skills/wix-headless/shared-utilities/wix-image.ts +72 -0
- package/skills/wix-headless/templates/ecom/CartBadge.tsx +84 -0
- package/skills/wix-headless/templates/ecom/CartView.tsx +543 -0
- package/skills/wix-headless/templates/ecom/cart.astro +13 -0
- package/skills/wix-headless/templates/ecom/components-ecom.css +215 -0
- package/skills/wix-headless/templates/ecom/discounts.ts +71 -0
- package/skills/wix-headless/templates/ecom/thank-you.astro +24 -0
- package/skills/wix-headless/templates/gift-cards/GiftCardPurchase.tsx +172 -0
- package/skills/wix-headless/templates/gift-cards/_home-teaser-snippet.astro +42 -0
- package/skills/wix-headless/templates/gift-cards/_nav-snippet.astro +11 -0
- package/skills/wix-headless/templates/gift-cards/components-gift-cards.css +219 -0
- package/skills/wix-headless/templates/gift-cards/gift-cards.astro +51 -0
- package/skills/wix-headless/templates/gift-cards/gift-cards.ts +75 -0
- package/skills/wix-headless/templates/stores/AddToCartButton.tsx +77 -0
- package/skills/wix-headless/templates/stores/BackInStockForm.tsx +176 -0
- package/skills/wix-headless/templates/stores/CategoryRail.astro +69 -0
- package/skills/wix-headless/templates/stores/ProductCard.astro +79 -0
- package/skills/wix-headless/templates/stores/ProductPurchase.tsx +397 -0
- package/skills/wix-headless/templates/stores/SeoTags.astro +21 -0
- package/skills/wix-headless/templates/stores/back-in-stock.ts +67 -0
- package/skills/wix-headless/templates/stores/categories.ts +155 -0
- package/skills/wix-headless/templates/stores/category/[slug].astro +130 -0
- package/skills/wix-headless/templates/stores/components-stores.css +295 -0
- package/skills/wix-headless/templates/stores/products/[slug].astro +128 -0
- package/skills/wix-headless/templates/stores/products/index.astro +101 -0
- package/skills/wix-manage/SKILL.md +280 -0
- package/skills/wix-manage/references/app-installation/install-wix-apps.md +155 -0
- package/skills/wix-manage/references/app-installation/list-installed-apps.md +124 -0
- package/skills/wix-manage/references/blog/how-to-create-blog-posts.md +226 -0
- package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +95 -0
- package/skills/wix-manage/references/bookings/booking-system-integration-gaps.md +416 -0
- package/skills/wix-manage/references/bookings/bookings-staff-setup.md +255 -0
- package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +462 -0
- package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +278 -0
- package/skills/wix-manage/references/bookings/external-calendar-integration.md +258 -0
- package/skills/wix-manage/references/bookings/multi-resource-service-creation.md +231 -0
- package/skills/wix-manage/references/calendar/configure-default-business-hours.md +226 -0
- package/skills/wix-manage/references/cms/cms-data-items-crud.md +422 -0
- package/skills/wix-manage/references/cms/cms-data-operations-extended.md +263 -0
- package/skills/wix-manage/references/cms/cms-ecommerce-catalog-integration.md +136 -0
- package/skills/wix-manage/references/cms/cms-references-and-relationships.md +160 -0
- package/skills/wix-manage/references/cms/cms-schema-management.md +143 -0
- package/skills/wix-manage/references/contacts/bulk-delete-contacts.md +73 -0
- package/skills/wix-manage/references/contacts/bulk-label-and-unlabel-contacts.md +67 -0
- package/skills/wix-manage/references/domains/domain-search-and-purchase.md +434 -0
- package/skills/wix-manage/references/ecommerce/api-recommendation-tracking.md +420 -0
- package/skills/wix-manage/references/ecommerce/flow-add-free-shipping.md +267 -0
- package/skills/wix-manage/references/ecommerce/flow-bundle-and-save.md +290 -0
- package/skills/wix-manage/references/ecommerce/flow-fix-coverage-gaps.md +300 -0
- package/skills/wix-manage/references/ecommerce/flow-optimize-shipping-rates.md +364 -0
- package/skills/wix-manage/references/ecommerce/flow-seasonal-promotion.md +315 -0
- package/skills/wix-manage/references/ecommerce/flow-stock-mover.md +308 -0
- package/skills/wix-manage/references/ecommerce/flow-upsell-boost.md +271 -0
- package/skills/wix-manage/references/ecommerce/goal-clear-inventory.md +118 -0
- package/skills/wix-manage/references/ecommerce/goal-drive-cross-sells.md +122 -0
- package/skills/wix-manage/references/ecommerce/goal-increase-aov.md +132 -0
- package/skills/wix-manage/references/ecommerce/goal-reduce-cart-abandonment.md +148 -0
- package/skills/wix-manage/references/ecommerce/goal-seasonal-revenue.md +146 -0
- package/skills/wix-manage/references/ecommerce/guardrail-discount-conflicts.md +118 -0
- package/skills/wix-manage/references/ecommerce/guardrail-margin-protection.md +88 -0
- package/skills/wix-manage/references/ecommerce/guardrail-rate-pricing-sanity.md +108 -0
- package/skills/wix-manage/references/ecommerce/guardrail-shipping-health.md +124 -0
- package/skills/wix-manage/references/ecommerce/recipe-apply-shipping-recommendations.md +299 -0
- package/skills/wix-manage/references/ecommerce/recommend-ecommerce-strategy.md +582 -0
- package/skills/wix-manage/references/ecommerce/setup-coupons.md +338 -0
- package/skills/wix-manage/references/ecommerce/setup-discount-rules.md +444 -0
- package/skills/wix-manage/references/ecommerce/setup-shipping-rates.md +86 -0
- package/skills/wix-manage/references/ecommerce/setup-shipping-regions.md +56 -0
- package/skills/wix-manage/references/ecommerce/setup-store-pickup-location.md +209 -0
- package/skills/wix-manage/references/ecommerce/skill-graph.md +139 -0
- package/skills/wix-manage/references/ecommerce/troubleshoot-checkout-delivery-dropoff.md +124 -0
- package/skills/wix-manage/references/ecommerce/troubleshoot-discount-not-applying.md +95 -0
- package/skills/wix-manage/references/events/list-events.md +203 -0
- package/skills/wix-manage/references/forms/create-form.md +260 -0
- package/skills/wix-manage/references/get-paid/create-payment-links.md +212 -0
- package/skills/wix-manage/references/get-paid/how-to-setup-wix-payments.md +57 -0
- package/skills/wix-manage/references/get-paid/payment-links-for-bookings.md +101 -0
- package/skills/wix-manage/references/media/upload-media-to-wix.md +216 -0
- package/skills/wix-manage/references/pricing-plans/create-and-update-pricing-plans.md +77 -0
- package/skills/wix-manage/references/pricing-plans/pricing-plans-bookings-integration.md +166 -0
- package/skills/wix-manage/references/restaurants/wix-restaurants-setup.md +336 -0
- package/skills/wix-manage/references/rich-content/ricos-converter-service.md +295 -0
- package/skills/wix-manage/references/site-properties/change-payment-currency-site-properties.md +59 -0
- package/skills/wix-manage/references/sites/create-site-from-template.md +183 -0
- package/skills/wix-manage/references/sites/query-sites.md +125 -0
- package/skills/wix-manage/references/stores/add-store-pages-to-site.md +57 -0
- package/skills/wix-manage/references/stores/bulk-create-products-with-options.md +1041 -0
- package/skills/wix-manage/references/stores/create-product-catalog-v1.md +125 -0
- package/skills/wix-manage/references/stores/create-product-from-image.md +824 -0
- package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +566 -0
- package/skills/wix-manage/references/stores/find-products-query-and-search-catalog-v3.md +194 -0
- package/skills/wix-manage/references/stores/query-products-catalog-v1.md +80 -0
- package/skills/wix-manage/references/stores/setup-online-store-catalog-v3.md +99 -0
- package/skills/wix-manage/references/stores/update-product-pre-order.md +213 -0
- package/skills/wix-manage/references/stores/update-product-with-options.md +318 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { resolveWixImageUrl } from "../utils/wix-image";
|
|
3
|
+
import { fetchLiveOffers, offersForProduct } from "../utils/discounts";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
product: {
|
|
7
|
+
_id?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
slug?: string;
|
|
10
|
+
media?: { main?: { image?: string }; items?: any[] };
|
|
11
|
+
actualPriceRange?: {
|
|
12
|
+
minValue?: { formattedAmount?: string; amount?: string; currency?: string };
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
// Optional editorial ordinal (01 / 02 / 03). Listings that want it should
|
|
16
|
+
// pass their own index; home grids typically omit it.
|
|
17
|
+
index?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { product, index } = Astro.props;
|
|
21
|
+
|
|
22
|
+
// fetchLiveOffers is memoized per request via auth.elevate's caching, so
|
|
23
|
+
// rendering N ProductCards in one page does NOT fan out N discount-rule
|
|
24
|
+
// queries. Pages that mount ProductCard never fetch offers themselves —
|
|
25
|
+
// the component owns that concern.
|
|
26
|
+
let primaryOffer: { name: string; offer?: string } | undefined;
|
|
27
|
+
try {
|
|
28
|
+
const offers = await fetchLiveOffers();
|
|
29
|
+
primaryOffer = offersForProduct(offers, product._id)[0];
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error("[ProductCard] fetchLiveOffers failed:", err);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const imageUrl = resolveWixImageUrl(product.media?.main?.image, 600, 800);
|
|
35
|
+
const price = product.actualPriceRange?.minValue?.formattedAmount;
|
|
36
|
+
const amount = product.actualPriceRange?.minValue?.amount;
|
|
37
|
+
const currency = product.actualPriceRange?.minValue?.currency;
|
|
38
|
+
const indexLabel =
|
|
39
|
+
typeof index === "number" ? String(index + 1).padStart(2, "0") : null;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<a
|
|
43
|
+
href={`/products/${product.slug}`}
|
|
44
|
+
class="product-card"
|
|
45
|
+
data-analytics-product
|
|
46
|
+
data-id={product._id}
|
|
47
|
+
data-name={product.name}
|
|
48
|
+
data-price={amount}
|
|
49
|
+
data-currency={currency}
|
|
50
|
+
>
|
|
51
|
+
<div class="product-card-media">
|
|
52
|
+
{imageUrl && <img src={imageUrl} alt={product.name ?? "Product"} loading="lazy" />}
|
|
53
|
+
{indexLabel && <span class="product-card-index">N° {indexLabel}</span>}
|
|
54
|
+
{primaryOffer && (
|
|
55
|
+
<span class="product-card-ribbon" title={primaryOffer.offer}>
|
|
56
|
+
{primaryOffer.name}
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
<div class="product-card-body">
|
|
61
|
+
<h3 class="product-card-name">{product.name}</h3>
|
|
62
|
+
{price && <p class="product-card-price">{price}</p>}
|
|
63
|
+
</div>
|
|
64
|
+
</a>
|
|
65
|
+
|
|
66
|
+
<script>
|
|
67
|
+
import { trackEvent } from "../utils/analytics";
|
|
68
|
+
document.addEventListener("click", (event) => {
|
|
69
|
+
const anchor = (event.target as HTMLElement)?.closest("a[data-analytics-product]") as HTMLAnchorElement | null;
|
|
70
|
+
if (!anchor) return;
|
|
71
|
+
trackEvent("ClickProduct", {
|
|
72
|
+
id: anchor.dataset.id,
|
|
73
|
+
name: anchor.dataset.name,
|
|
74
|
+
price: anchor.dataset.price ? Number(anchor.dataset.price) : undefined,
|
|
75
|
+
currency: anchor.dataset.currency,
|
|
76
|
+
origin: "Product Listing",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { useState, useMemo, useEffect } from "react";
|
|
2
|
+
import { currentCart } from "@wix/ecom";
|
|
3
|
+
import AddToCartButton from "./AddToCartButton";
|
|
4
|
+
import BackInStockForm from "./BackInStockForm";
|
|
5
|
+
|
|
6
|
+
interface Choice { choiceId?: string | null; name?: string | null; key?: string | null; }
|
|
7
|
+
interface Option { _id?: string | null; name?: string | null; choicesSettings?: { choices?: Choice[]; }; }
|
|
8
|
+
// Modifiers: customization choices without separate variants/inventory/price.
|
|
9
|
+
interface FreeTextSettings { title?: string | null; key?: string | null; maxLength?: number | null; }
|
|
10
|
+
interface Modifier {
|
|
11
|
+
// SDK convention: outer entity IDs are `_id` (same as Variant/Option/Product).
|
|
12
|
+
// REST returns `id`; @wix/stores remaps to `_id` in the SDK response.
|
|
13
|
+
_id?: string | null;
|
|
14
|
+
name?: string | null;
|
|
15
|
+
key?: string | null;
|
|
16
|
+
mandatory?: boolean;
|
|
17
|
+
modifierRenderType?: string | null; // "TEXT_CHOICES" | "SWATCH_CHOICES" | "FREE_TEXT"
|
|
18
|
+
choicesSettings?: { choices?: Choice[] };
|
|
19
|
+
freeTextSettings?: FreeTextSettings;
|
|
20
|
+
}
|
|
21
|
+
interface VariantChoice { optionChoiceIds?: { optionId?: string; choiceId?: string; }; }
|
|
22
|
+
interface Variant {
|
|
23
|
+
_id?: string | null;
|
|
24
|
+
choices?: VariantChoice[];
|
|
25
|
+
price?: { actual?: { amount?: string; formattedAmount?: string; }; };
|
|
26
|
+
// productsV3 also exposes a per-variant inventoryStatus, but it's a STALE
|
|
27
|
+
// cached flag — a variant with live quantity 0 can still report
|
|
28
|
+
// `inStock: true`. Only used as a last-resort fallback when the
|
|
29
|
+
// inventoryByVariant prop is empty. The authoritative signal is
|
|
30
|
+
// inventoryItemsV3 — see the page-level query in PRODUCT_PAGES.md.
|
|
31
|
+
inventoryStatus?: { inStock?: boolean; preorderEnabled?: boolean };
|
|
32
|
+
// Future-proofing: if a future wiring passes real quantities in props
|
|
33
|
+
// (e.g. via @wix/inventory join by variantId), maxQuantity honors them.
|
|
34
|
+
stock?: { quantity?: number | null; inStock?: boolean; };
|
|
35
|
+
inventoryItem?: { quantity?: number | null; inStock?: boolean; trackQuantity?: boolean };
|
|
36
|
+
}
|
|
37
|
+
interface Stock { trackInventory?: boolean; inventoryStatus?: string; quantity?: number | null; }
|
|
38
|
+
interface VariantStock { quantity: number; trackQuantity: boolean; preorderEnabled: boolean }
|
|
39
|
+
|
|
40
|
+
// Single `product` prop (the full productsV3 object) mirrors ProductCard's
|
|
41
|
+
// contract so both components take the same shape. Keeps `[slug].astro`
|
|
42
|
+
// mount sites greppable: <ProductPurchase client:load product={product}
|
|
43
|
+
// inventoryByVariant={inventoryByVariant} />.
|
|
44
|
+
interface ProductLike {
|
|
45
|
+
_id?: string | null;
|
|
46
|
+
name?: string | null;
|
|
47
|
+
actualPriceRange?: { minValue?: { currency?: string | null } | null } | null;
|
|
48
|
+
options?: Option[] | null;
|
|
49
|
+
modifiers?: Modifier[] | null;
|
|
50
|
+
variantsInfo?: { variants?: Variant[] } | null;
|
|
51
|
+
stock?: Stock | null;
|
|
52
|
+
}
|
|
53
|
+
interface ProductPurchaseProps {
|
|
54
|
+
product: ProductLike;
|
|
55
|
+
// Live per-variant stock from inventoryItemsV3, keyed by variantId.
|
|
56
|
+
// AUTHORITATIVE source for OOS gating. productsV3's cached inStock flag is
|
|
57
|
+
// not reliable — see PRODUCT_PAGES.md anti-patterns table.
|
|
58
|
+
inventoryByVariant?: Record<string, VariantStock>;
|
|
59
|
+
// SSR probe result from getBackInStockEnabled() — gates the OOS subscribe
|
|
60
|
+
// form. When false, OOS branches render only the existing "Out of Stock"
|
|
61
|
+
// / "Sold Out" stock-status copy. See references/BACK_IN_STOCK.md.
|
|
62
|
+
backInStockEnabled?: boolean;
|
|
63
|
+
// Numeric product price for the back-in-stock subscribe call. The Wix
|
|
64
|
+
// endpoint validates `itemDetails.price >= 0` and rejects formatted
|
|
65
|
+
// strings; pass a number, not "$695".
|
|
66
|
+
priceAmount?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default function ProductPurchase({
|
|
70
|
+
product,
|
|
71
|
+
inventoryByVariant = {},
|
|
72
|
+
backInStockEnabled = false,
|
|
73
|
+
priceAmount,
|
|
74
|
+
}: ProductPurchaseProps) {
|
|
75
|
+
const productId = product._id ?? "";
|
|
76
|
+
const productName = product.name ?? "";
|
|
77
|
+
const currency = product.actualPriceRange?.minValue?.currency ?? undefined;
|
|
78
|
+
const options = product.options ?? [];
|
|
79
|
+
const modifiers = product.modifiers ?? [];
|
|
80
|
+
const variantsInfo = product.variantsInfo ?? { variants: [] };
|
|
81
|
+
const stock = product.stock ?? undefined;
|
|
82
|
+
|
|
83
|
+
const variants = variantsInfo.variants ?? [];
|
|
84
|
+
const hasMeaningfulOptions = options.length > 0 && options.some(
|
|
85
|
+
(opt) => (opt.choicesSettings?.choices?.length ?? 0) > 1,
|
|
86
|
+
);
|
|
87
|
+
const [selections, setSelections] = useState<Record<string, string>>({});
|
|
88
|
+
const handleSelect = (optionId: string, choiceId: string) =>
|
|
89
|
+
setSelections((prev) => ({ ...prev, [optionId]: choiceId }));
|
|
90
|
+
const allSelected = hasMeaningfulOptions && options.every((opt) => opt._id && selections[opt._id]);
|
|
91
|
+
|
|
92
|
+
// Modifier state. Two parallel maps keyed by modifier._id, flattened below
|
|
93
|
+
// into the shapes cart expects.
|
|
94
|
+
const [modifierChoice, setModifierChoice] = useState<Record<string, string>>({});
|
|
95
|
+
const [modifierText, setModifierText] = useState<Record<string, string>>({});
|
|
96
|
+
const handleModifierChoice = (id: string, key: string) => setModifierChoice((p) => ({ ...p, [id]: key }));
|
|
97
|
+
const handleModifierText = (id: string, v: string) => setModifierText((p) => ({ ...p, [id]: v }));
|
|
98
|
+
|
|
99
|
+
const mandatoryModifiersSatisfied = useMemo(() => modifiers.every((m) => {
|
|
100
|
+
if (!m.mandatory) return true;
|
|
101
|
+
const id = m._id ?? "";
|
|
102
|
+
if (m.modifierRenderType === "FREE_TEXT") return (modifierText[id] ?? "").trim().length > 0;
|
|
103
|
+
return !!modifierChoice[id];
|
|
104
|
+
}), [modifiers, modifierChoice, modifierText]);
|
|
105
|
+
|
|
106
|
+
// Flatten to catalogReference shapes:
|
|
107
|
+
// options → { [modifier.key]: choice.key } for TEXT_CHOICES / SWATCH_CHOICES
|
|
108
|
+
// customTextFields → { [freeTextSettings.key]: text } for FREE_TEXT
|
|
109
|
+
const catalogModifierChoices = useMemo(() => {
|
|
110
|
+
const out: Record<string, string> = {};
|
|
111
|
+
for (const m of modifiers) {
|
|
112
|
+
if (!m.key || !m._id || m.modifierRenderType === "FREE_TEXT") continue;
|
|
113
|
+
const c = modifierChoice[m._id];
|
|
114
|
+
if (c) out[m.key] = c;
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}, [modifiers, modifierChoice]);
|
|
118
|
+
const catalogCustomTextFields = useMemo(() => {
|
|
119
|
+
const out: Record<string, string> = {};
|
|
120
|
+
for (const m of modifiers) {
|
|
121
|
+
if (!m._id || m.modifierRenderType !== "FREE_TEXT") continue;
|
|
122
|
+
const k = m.freeTextSettings?.key;
|
|
123
|
+
const v = (modifierText[m._id] ?? "").trim();
|
|
124
|
+
if (k && v) out[k] = v;
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}, [modifiers, modifierText]);
|
|
128
|
+
|
|
129
|
+
const resolvedVariant = useMemo(() => {
|
|
130
|
+
if (!hasMeaningfulOptions) return variants[0] ?? null;
|
|
131
|
+
if (!allSelected) return null;
|
|
132
|
+
return variants.find((v) =>
|
|
133
|
+
(v.choices ?? []).every(
|
|
134
|
+
(c) => c.optionChoiceIds?.optionId &&
|
|
135
|
+
selections[c.optionChoiceIds.optionId] === c.optionChoiceIds.choiceId,
|
|
136
|
+
),
|
|
137
|
+
) ?? null;
|
|
138
|
+
}, [hasMeaningfulOptions, allSelected, selections, variants]);
|
|
139
|
+
|
|
140
|
+
const variantLabel = useMemo(() => {
|
|
141
|
+
if (!hasMeaningfulOptions) return undefined;
|
|
142
|
+
return options.map((opt) => {
|
|
143
|
+
const choiceId = opt._id ? selections[opt._id] : undefined;
|
|
144
|
+
const choice = opt.choicesSettings?.choices?.find((c) => c.choiceId === choiceId);
|
|
145
|
+
return choice?.name ? `${opt.name}: ${choice.name}` : null;
|
|
146
|
+
}).filter(Boolean).join(", ") || undefined;
|
|
147
|
+
}, [hasMeaningfulOptions, options, selections]);
|
|
148
|
+
|
|
149
|
+
const variantPrice = resolvedVariant?.price?.actual?.amount
|
|
150
|
+
? Number(resolvedVariant.price.actual.amount)
|
|
151
|
+
: undefined;
|
|
152
|
+
|
|
153
|
+
// Binary OOS gate:
|
|
154
|
+
// - Source of truth = inventoryByVariant (from inventoryItemsV3).
|
|
155
|
+
// - Fallback = productsV3's variantsInfo[].inventoryStatus.inStock
|
|
156
|
+
// (stale, don't trust it unless the live map is empty).
|
|
157
|
+
// - productOutOfStock short-circuits the whole render (applies to the
|
|
158
|
+
// entire product e.g. unpublished or fully sold out).
|
|
159
|
+
// - variantOutOfStock keeps option pills visible so the shopper can
|
|
160
|
+
// pick a different finish; hides the stepper + button.
|
|
161
|
+
const isVariantSoldOut = (variant: Variant | null | undefined): boolean => {
|
|
162
|
+
if (!variant?._id) return false;
|
|
163
|
+
const live = inventoryByVariant[variant._id];
|
|
164
|
+
if (live) {
|
|
165
|
+
if (!live.trackQuantity) return false;
|
|
166
|
+
if (live.preorderEnabled) return false;
|
|
167
|
+
return live.quantity <= 0;
|
|
168
|
+
}
|
|
169
|
+
return (
|
|
170
|
+
variant.inventoryStatus?.inStock === false &&
|
|
171
|
+
variant.inventoryStatus?.preorderEnabled !== true
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
const productOutOfStock = stock?.inventoryStatus === "OUT_OF_STOCK";
|
|
175
|
+
const variantOutOfStock = isVariantSoldOut(resolvedVariant);
|
|
176
|
+
const isOutOfStock = productOutOfStock;
|
|
177
|
+
// maxQuantity caps the stepper. Primary source: inventoryByVariant (live
|
|
178
|
+
// from inventoryItemsV3). Fallbacks cover partial wirings. The cart
|
|
179
|
+
// backend still enforces stock on add-to-cart — this clamp just avoids
|
|
180
|
+
// letting shoppers build a quantity they can't actually buy.
|
|
181
|
+
const maxQuantity = useMemo(() => {
|
|
182
|
+
const live = resolvedVariant?._id ? inventoryByVariant[resolvedVariant._id] : undefined;
|
|
183
|
+
if (live) {
|
|
184
|
+
if (!live.trackQuantity) return 99;
|
|
185
|
+
if (live.preorderEnabled) return 99;
|
|
186
|
+
return Math.max(1, live.quantity);
|
|
187
|
+
}
|
|
188
|
+
if (stock?.trackInventory === false) return 99;
|
|
189
|
+
if (resolvedVariant?.stock?.quantity != null) return resolvedVariant.stock.quantity;
|
|
190
|
+
if (resolvedVariant?.inventoryItem?.quantity != null) return resolvedVariant.inventoryItem.quantity;
|
|
191
|
+
if (stock?.quantity != null) return stock.quantity;
|
|
192
|
+
return 99;
|
|
193
|
+
}, [inventoryByVariant, stock, resolvedVariant]);
|
|
194
|
+
// Count how much of each variant is already in the cart so the stepper cap
|
|
195
|
+
// reflects "stock minus already-in-cart" instead of raw stock. Wix enforces
|
|
196
|
+
// the same rule at add-to-cart time — this is just so the UI doesn't let
|
|
197
|
+
// the shopper think they can add "max" after already adding "max".
|
|
198
|
+
const [qtyInCartByVariant, setQtyInCartByVariant] = useState<Record<string, number>>({});
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
let cancelled = false;
|
|
201
|
+
const syncCart = async () => {
|
|
202
|
+
try {
|
|
203
|
+
const cart = await currentCart.getCurrentCart();
|
|
204
|
+
if (cancelled) return;
|
|
205
|
+
const tally: Record<string, number> = {};
|
|
206
|
+
for (const item of cart.lineItems ?? []) {
|
|
207
|
+
const ref = item.catalogReference as any;
|
|
208
|
+
if (ref?.catalogItemId !== productId) continue;
|
|
209
|
+
const vId = ref?.options?.variantId as string | undefined;
|
|
210
|
+
if (!vId) continue;
|
|
211
|
+
tally[vId] = (tally[vId] ?? 0) + (item.quantity ?? 0);
|
|
212
|
+
}
|
|
213
|
+
setQtyInCartByVariant(tally);
|
|
214
|
+
} catch { if (!cancelled) setQtyInCartByVariant({}); }
|
|
215
|
+
};
|
|
216
|
+
syncCart();
|
|
217
|
+
const handler = () => syncCart();
|
|
218
|
+
window.addEventListener("cart-updated", handler);
|
|
219
|
+
return () => { cancelled = true; window.removeEventListener("cart-updated", handler); };
|
|
220
|
+
}, [productId]);
|
|
221
|
+
|
|
222
|
+
const qtyInCartForResolved = resolvedVariant?._id ? qtyInCartByVariant[resolvedVariant._id] ?? 0 : 0;
|
|
223
|
+
const effectiveMax = Math.max(0, maxQuantity - qtyInCartForResolved);
|
|
224
|
+
const alreadyMaxedInCart = qtyInCartForResolved > 0 && effectiveMax === 0;
|
|
225
|
+
|
|
226
|
+
const [quantity, setQuantity] = useState(1);
|
|
227
|
+
// When max shrinks (variant switch, or cart sync), clamp the current selection.
|
|
228
|
+
useEffect(() => { setQuantity((q) => Math.max(1, Math.min(q, Math.max(1, effectiveMax)))); }, [effectiveMax]);
|
|
229
|
+
|
|
230
|
+
const quantitySelector = !isOutOfStock && (
|
|
231
|
+
<div className="quantity-selector">
|
|
232
|
+
<button className="quantity-btn" onClick={() => setQuantity((q) => Math.max(1, q - 1))} disabled={quantity <= 1}>−</button>
|
|
233
|
+
<span className="quantity-value">{quantity}</span>
|
|
234
|
+
<button className="quantity-btn" onClick={() => setQuantity((q) => Math.min(effectiveMax, q + 1))} disabled={quantity >= effectiveMax}>+</button>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const inCartNote = qtyInCartForResolved > 0 && (
|
|
239
|
+
<p className="stock-status" aria-live="polite">{qtyInCartForResolved} already in cart</p>
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Modifier selectors — rendered between options and quantity in both branches.
|
|
243
|
+
const modifierSelectors = modifiers.length > 0 && (
|
|
244
|
+
<>
|
|
245
|
+
{modifiers.map((m) => {
|
|
246
|
+
const id = m._id ?? "";
|
|
247
|
+
const req = m.mandatory === true;
|
|
248
|
+
if (m.modifierRenderType === "FREE_TEXT") {
|
|
249
|
+
return (
|
|
250
|
+
<div key={id} className="option-group">
|
|
251
|
+
<div className="option-label">{m.name}{req && <span aria-hidden="true"> *</span>}</div>
|
|
252
|
+
<textarea className="modifier-text"
|
|
253
|
+
value={modifierText[id] ?? ""}
|
|
254
|
+
onChange={(e) => handleModifierText(id, e.target.value)}
|
|
255
|
+
placeholder={m.freeTextSettings?.title ?? ""}
|
|
256
|
+
maxLength={m.freeTextSettings?.maxLength ?? undefined}
|
|
257
|
+
rows={2} aria-label={m.name ?? ""} aria-required={req || undefined}
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const choices = m.choicesSettings?.choices ?? [];
|
|
263
|
+
return (
|
|
264
|
+
<div key={id} className="option-group">
|
|
265
|
+
<div className="option-label">{m.name}{req && <span aria-hidden="true"> *</span>}</div>
|
|
266
|
+
<div className="option-choices">
|
|
267
|
+
{choices.map((c) => {
|
|
268
|
+
const k = c.key ?? c.choiceId ?? "";
|
|
269
|
+
const sel = modifierChoice[id] === k;
|
|
270
|
+
return (
|
|
271
|
+
<button key={k} type="button" onClick={() => handleModifierChoice(id, k)}
|
|
272
|
+
className={`option-pill${sel ? " selected" : ""}`} aria-pressed={sel}>
|
|
273
|
+
{c.name}
|
|
274
|
+
</button>
|
|
275
|
+
);
|
|
276
|
+
})}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
})}
|
|
281
|
+
</>
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Render the back-in-stock subscribe form in OOS branches when the
|
|
285
|
+
// dashboard probe is on. Variant-aware: pass the resolved variantId when
|
|
286
|
+
// a single variant is OOS, omit it when the whole product is OOS. The
|
|
287
|
+
// form sends only required fields per the bare-fields rule
|
|
288
|
+
// (BACK_IN_STOCK.md) — no itemUrl, no image.
|
|
289
|
+
const renderBackInStockForm = (variantIdForForm?: string) =>
|
|
290
|
+
backInStockEnabled ? (
|
|
291
|
+
<BackInStockForm
|
|
292
|
+
productId={productId}
|
|
293
|
+
variantId={variantIdForForm}
|
|
294
|
+
productName={productName}
|
|
295
|
+
productPrice={variantPrice ?? priceAmount ?? 0}
|
|
296
|
+
variantLabel={variantIdForForm ? variantLabel : undefined}
|
|
297
|
+
/>
|
|
298
|
+
) : null;
|
|
299
|
+
|
|
300
|
+
if (isOutOfStock) {
|
|
301
|
+
return (
|
|
302
|
+
<div className="purchase-area">
|
|
303
|
+
<p className="stock-status">Out of Stock</p>
|
|
304
|
+
{renderBackInStockForm()}
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!hasMeaningfulOptions) {
|
|
310
|
+
const singleVariant = variants[0];
|
|
311
|
+
const singleVariantOOS = isVariantSoldOut(singleVariant);
|
|
312
|
+
if (singleVariantOOS) {
|
|
313
|
+
return (
|
|
314
|
+
<div className="purchase-area">
|
|
315
|
+
<p className="stock-status">Sold Out</p>
|
|
316
|
+
{renderBackInStockForm(singleVariant?._id ?? undefined)}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return (
|
|
321
|
+
<div className="purchase-area">
|
|
322
|
+
{modifierSelectors}
|
|
323
|
+
{inCartNote}
|
|
324
|
+
{alreadyMaxedInCart ? (
|
|
325
|
+
<p className="stock-status">Maximum quantity already in cart</p>
|
|
326
|
+
) : (
|
|
327
|
+
<>
|
|
328
|
+
{quantitySelector}
|
|
329
|
+
<AddToCartButton
|
|
330
|
+
productId={productId} productName={productName}
|
|
331
|
+
price={variantPrice} currency={currency}
|
|
332
|
+
variantId={singleVariant?._id ?? undefined}
|
|
333
|
+
quantity={quantity}
|
|
334
|
+
modifierChoices={catalogModifierChoices}
|
|
335
|
+
customTextFields={catalogCustomTextFields}
|
|
336
|
+
disabled={!mandatoryModifiersSatisfied}
|
|
337
|
+
/>
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div className="purchase-area">
|
|
346
|
+
{options.map((option) => {
|
|
347
|
+
const choices = option.choicesSettings?.choices ?? [];
|
|
348
|
+
const optionId = option._id ?? "";
|
|
349
|
+
return (
|
|
350
|
+
<div key={optionId} className="option-group">
|
|
351
|
+
<div className="option-label">{option.name}</div>
|
|
352
|
+
<div className="option-choices">
|
|
353
|
+
{choices.map((choice) => {
|
|
354
|
+
const choiceId = choice.choiceId ?? "";
|
|
355
|
+
const isSelected = selections[optionId] === choiceId;
|
|
356
|
+
return (
|
|
357
|
+
<button key={choiceId} onClick={() => handleSelect(optionId, choiceId)}
|
|
358
|
+
className={`option-pill${isSelected ? " selected" : ""}`}>
|
|
359
|
+
{choice.name}
|
|
360
|
+
</button>
|
|
361
|
+
);
|
|
362
|
+
})}
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
})}
|
|
367
|
+
{modifierSelectors}
|
|
368
|
+
{variantOutOfStock ? (
|
|
369
|
+
<>
|
|
370
|
+
{renderBackInStockForm(resolvedVariant?._id ?? undefined)}
|
|
371
|
+
<p className="stock-status back-in-stock-alt">
|
|
372
|
+
Or try another option above.
|
|
373
|
+
</p>
|
|
374
|
+
</>
|
|
375
|
+
) : alreadyMaxedInCart ? (
|
|
376
|
+
<>
|
|
377
|
+
{inCartNote}
|
|
378
|
+
<p className="stock-status">Maximum quantity already in cart</p>
|
|
379
|
+
</>
|
|
380
|
+
) : (
|
|
381
|
+
<>
|
|
382
|
+
{inCartNote}
|
|
383
|
+
{quantitySelector}
|
|
384
|
+
<AddToCartButton
|
|
385
|
+
productId={productId} productName={productName}
|
|
386
|
+
price={variantPrice} currency={currency}
|
|
387
|
+
variantId={resolvedVariant?._id ?? undefined}
|
|
388
|
+
variantName={variantLabel} quantity={quantity}
|
|
389
|
+
modifierChoices={catalogModifierChoices}
|
|
390
|
+
customTextFields={catalogCustomTextFields}
|
|
391
|
+
disabled={!resolvedVariant || !mandatoryModifiersSatisfied}
|
|
392
|
+
/>
|
|
393
|
+
</>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface SeoTag {
|
|
3
|
+
type?: string;
|
|
4
|
+
props?: Record<string, any> | null;
|
|
5
|
+
children?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface Props { tags?: SeoTag[]; }
|
|
9
|
+
const { tags = [] } = Astro.props;
|
|
10
|
+
const visibleTags = tags.filter((t) => !t.disabled);
|
|
11
|
+
---
|
|
12
|
+
{
|
|
13
|
+
visibleTags.map((tag) => {
|
|
14
|
+
const props = tag.props ?? {};
|
|
15
|
+
if (tag.type === "title") return <title set:html={tag.children ?? ""} />;
|
|
16
|
+
if (tag.type === "meta") return <meta {...props} />;
|
|
17
|
+
if (tag.type === "link") return <link {...props} />;
|
|
18
|
+
if (tag.type === "script") return <script {...props} is:inline set:html={tag.children ?? ""} />;
|
|
19
|
+
return null;
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// SSR-only probe for the Wix back-in-stock service. Returns true when the
|
|
2
|
+
// dashboard's "Start Collecting Requests" toggle is on for Wix Stores;
|
|
3
|
+
// `[slug].astro` passes the boolean through to ProductPurchase, which
|
|
4
|
+
// renders the subscribe form in OOS branches when the prop is true.
|
|
5
|
+
//
|
|
6
|
+
// Wix's back-in-stock service identifies the Stores integration by the
|
|
7
|
+
// Stores SUB-PAGE registration id (1380b703-…), NOT by the install/catalog
|
|
8
|
+
// appId (215238eb-…) that the public docs and other catalog endpoints use.
|
|
9
|
+
// Reproduced 2026-04-28 against a Catalog-V3 site:
|
|
10
|
+
// - PUT /settings returned `collectionStates: [{appId: "1380b703-…",
|
|
11
|
+
// collectingRequests: true}]`.
|
|
12
|
+
// - POST /settings/start-collecting with `215238eb-…` returned
|
|
13
|
+
// 428 NOT_SUPPORTED_APP_DEF_ID.
|
|
14
|
+
// - POST /back-in-stock-notification-requests with catalogReference.appId
|
|
15
|
+
// `215238eb-…` returned 428 REQUEST_COLLECTION_DISABLED.
|
|
16
|
+
// - POST /back-in-stock-notification-requests with catalogReference.appId
|
|
17
|
+
// `1380b703-…` succeeded and returned a RECEIVED request.
|
|
18
|
+
// Treat 1380b703-… as the canonical id for this service. The install/
|
|
19
|
+
// catalog id 215238eb-… is exported in case other endpoints need it.
|
|
20
|
+
//
|
|
21
|
+
// The matching subscribe call goes through @wix/ecom's
|
|
22
|
+
// backInStockNotifications.createBackInStockNotificationRequest from the
|
|
23
|
+
// React island directly — visitor scope is enough, no client-side
|
|
24
|
+
// elevation, no Astro API route. The cloud-provider-fetch-adapter that
|
|
25
|
+
// ships with the Wix scaffold returns 403 to the browser on app-defined
|
|
26
|
+
// POST routes; the SDK call avoids that surface entirely. So this file
|
|
27
|
+
// exports only the SSR probe — there's no REQUESTS_URL constant.
|
|
28
|
+
|
|
29
|
+
import { auth, httpClient } from "@wix/essentials";
|
|
30
|
+
|
|
31
|
+
export const WIX_STORES_BACK_IN_STOCK_APP_ID =
|
|
32
|
+
"1380b703-ce81-ff05-f115-39571d94dfcd";
|
|
33
|
+
export const WIX_STORES_INSTALL_APP_ID =
|
|
34
|
+
"215238eb-22a5-4c36-9e7b-e7c08025e04e";
|
|
35
|
+
|
|
36
|
+
export const SETTINGS_URL =
|
|
37
|
+
"https://www.wixapis.com/back-in-stock-service/v1/back-in-stock-notification-requests/settings";
|
|
38
|
+
|
|
39
|
+
interface BackInStockCollectionState {
|
|
40
|
+
appId?: string;
|
|
41
|
+
collectingRequests?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let cached: Promise<boolean> | null = null;
|
|
45
|
+
|
|
46
|
+
export async function getBackInStockEnabled(): Promise<boolean> {
|
|
47
|
+
if (cached) return cached;
|
|
48
|
+
cached = (async () => {
|
|
49
|
+
try {
|
|
50
|
+
const elevated = auth.elevate(httpClient.fetchWithAuth);
|
|
51
|
+
const res = await elevated(SETTINGS_URL, { method: "PUT" });
|
|
52
|
+
if (!res.ok) return false;
|
|
53
|
+
const data = await res.json();
|
|
54
|
+
const states: BackInStockCollectionState[] =
|
|
55
|
+
data?.settings?.collectionStates ?? [];
|
|
56
|
+
return states.some(
|
|
57
|
+
(s) =>
|
|
58
|
+
s.appId === WIX_STORES_BACK_IN_STOCK_APP_ID &&
|
|
59
|
+
s.collectingRequests === true,
|
|
60
|
+
);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error("[back-in-stock] getSettings probe failed:", err);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
return cached;
|
|
67
|
+
}
|