From 40a86a948dc64c60fe3e84e4436357ed758c9639 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Mon, 26 May 2025 14:13:09 +0300 Subject: [PATCH 01/42] Add buy now component --- .../headless-components/stores/package.json | 4 + .../stores/src/react/index.tsx | 62 ++ yarn.lock | 617 +++++++++++++++++- 3 files changed, 649 insertions(+), 34 deletions(-) diff --git a/packages/headless-components/stores/package.json b/packages/headless-components/stores/package.json index e9fb3351f..119f7c7fd 100644 --- a/packages/headless-components/stores/package.json +++ b/packages/headless-components/stores/package.json @@ -10,5 +10,9 @@ "devDependencies": { "@types/node": "^20.9.0", "typescript": "^5.7.3" + }, + "dependencies": { + "@wix/ecom": "^1.0.1169", + "@wix/redirects": "^1.0.79" } } diff --git a/packages/headless-components/stores/src/react/index.tsx b/packages/headless-components/stores/src/react/index.tsx index 4f334ad42..3fba9bef0 100644 --- a/packages/headless-components/stores/src/react/index.tsx +++ b/packages/headless-components/stores/src/react/index.tsx @@ -1,3 +1,65 @@ +import { cart } from "@wix/ecom"; +import { redirects } from "@wix/redirects"; +import { useState } from "react"; + +export function BuyNow(props: { + productId: string; + variant?: Record; + children: (props: { + isLoading: boolean; + redirectToCheckout: () => void; + }) => React.ReactNode; +}) { + const [isLoading, setIsLoading] = useState(false); + + const redirectToCheckout = async () => { + try { + setIsLoading(true); + const createdCart = await cart.createCart({ + lineItems: [ + { + catalogReference: { + catalogItemId: props.productId, + appId: "1380b703-ce81-ff05-f115-39571d94dfcd", + options: { + options: props.variant, + } + }, + quantity: 1 + }, + ], + }); + + if (!createdCart || !createdCart._id) { + throw new Error("Failed to create cart"); + } + + const { checkoutId } = await cart.createCheckout(createdCart._id, { + channelType: cart.ChannelType.WEB + }); + + const { redirectSession } = await redirects.createRedirectSession({ + ecomCheckout: { checkoutId }, + callbacks: { + postFlowUrl: window.location.href, + }, + }); + + window.location.href = redirectSession?.fullUrl!; + } catch (error) { + console.error("Error during checkout:", error); + } finally { + setIsLoading(false); + } + } + + return props.children({ + isLoading, + redirectToCheckout, + }) +} + export function Stores() { return
Stores
; } + diff --git a/yarn.lock b/yarn.lock index 28c884f5f..11ae089a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -273,12 +273,10 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.18, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.4.5, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.27.0 - resolution: "@babel/runtime@npm:7.27.0" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10c0/35091ea9de48bd7fd26fb177693d64f4d195eb58ab2b142b893b7f3fa0f1d7c677604d36499ae0621a3703f35ba0c6a8f6c572cc8f7dc0317213841e493cf663 +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.18, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.1, @babel/runtime@npm:^7.4.5, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.27.1 + resolution: "@babel/runtime@npm:7.27.1" + checksum: 10c0/530a7332f86ac5a7442250456823a930906911d895c0b743bf1852efc88a20a016ed4cd26d442d0ca40ae6d5448111e02a08dd638a4f1064b47d080e2875dc05 languageName: node linkType: hard @@ -2307,6 +2305,476 @@ __metadata: languageName: node linkType: hard +"@wix/auto_sdk_ecom_abandoned-checkouts@npm:1.0.15": + version: 1.0.15 + resolution: "@wix/auto_sdk_ecom_abandoned-checkouts@npm:1.0.15" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/a635187ee9cebb5ffedca16b0b6f9c2a04591371b0f85f0562a468aec497b672fd7827d27f22642d3e2b991a78f557b3213a1370f628ec205bcc653f408ada52 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_additional-fees@npm:1.0.10": + version: 1.0.10 + resolution: "@wix/auto_sdk_ecom_additional-fees@npm:1.0.10" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/be7a0d8e363a6f0a10448c558c5240dbcef39f6e88a53efb94e279aafcbb601e3e302e12578a4f09e8afa31945e7f46d1d09e70fb8b4e9d17034bbdeb4fa5c00 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_back-in-stock-notifications@npm:1.0.17": + version: 1.0.17 + resolution: "@wix/auto_sdk_ecom_back-in-stock-notifications@npm:1.0.17" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/392c8f12a6b56490c4199cec2728a651139530789acea7ec2193ec569f6dee2fc655aa958c2592cd6d4870c1d882345b9e0b893d88b9eb063179578b65da820d + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_back-in-stock-settings@npm:1.0.10": + version: 1.0.10 + resolution: "@wix/auto_sdk_ecom_back-in-stock-settings@npm:1.0.10" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/924465f84b71248e2efe6074bc784c187e2f6ac8346cc68221b1df907e5211398dd17cc84c0cf040e49806ac4da93c3be38e55b923deee04ef862c7182bf9a5e + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_cart@npm:1.0.30": + version: 1.0.30 + resolution: "@wix/auto_sdk_ecom_cart@npm:1.0.30" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/253fb8a2ac36b7632ed1073ce0efab993c674c62fe3a2f20eb6c24de52e0f9034ad9d373c8d0d18b902c87214b37bbae86e526273bb766e5ef6010cbb2e96a50 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_catalog@npm:1.0.17": + version: 1.0.17 + resolution: "@wix/auto_sdk_ecom_catalog@npm:1.0.17" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/06815147ed114086084bbc01145f120d51e3c2fa21f9b8acd544580adba8a1acd6e7f3d32790007eb5bb4a790896fa78abd0e8f16ce6c0b388327654f11dd693 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_checkout-content@npm:1.0.8": + version: 1.0.8 + resolution: "@wix/auto_sdk_ecom_checkout-content@npm:1.0.8" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/e96a273cf3edbc9d129b7d754e36efdf41d446b9c9e4beb7572bc66bf2d2a6b1963cc1ae3b2152906dd992e7e00cfb7244333091183df2332377c02a04df85fb + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_checkout-settings@npm:1.0.14": + version: 1.0.14 + resolution: "@wix/auto_sdk_ecom_checkout-settings@npm:1.0.14" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/14241c9eeff5ad432e7ea83568f807d903bd2dca4d377d1ffbcacb621c825c019250402278cc282b37829d5e9de4650a87856fd95adb551de3e52fb087b99eba + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_checkout-templates@npm:1.0.30": + version: 1.0.30 + resolution: "@wix/auto_sdk_ecom_checkout-templates@npm:1.0.30" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/3cec6738517b25cf23474f19b2a099671309cbe0584a4d515aa977ff74b614993bceb1887f6acf2ee69877d4204851b5c39d7d092a58d683e4f8d866f48c4aa5 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_checkout@npm:1.0.30": + version: 1.0.30 + resolution: "@wix/auto_sdk_ecom_checkout@npm:1.0.30" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/11d2deace9d9be6b12615835cbe457772188d52fe65db56940f7b618fd9db6c39ae592657704d9eaa6fb6b7fe794f7ef24b911a79c107cc27fefce7f7c7d724d + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_currencies@npm:1.0.10": + version: 1.0.10 + resolution: "@wix/auto_sdk_ecom_currencies@npm:1.0.10" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/944510332cd3f311cc8da08f64b6fe7dc346442a1950d4ffdecf21fed5dec67f91bbd9ed9a5a83779777ed87d70ae15b79f6491f698fc99c786fe761c8a319dd + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_current-cart@npm:1.0.30": + version: 1.0.30 + resolution: "@wix/auto_sdk_ecom_current-cart@npm:1.0.30" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/ab7fa522a0222a0d78b1a97c19d89d59a592f55ffebf63621ed70b59d30b02913f718742e14a052f4122ab1667a3066b0b5cd3c5acd06ccb4fcf678702788f95 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_custom-triggers@npm:1.0.11": + version: 1.0.11 + resolution: "@wix/auto_sdk_ecom_custom-triggers@npm:1.0.11" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/5ba89db3c009ae61f8e9fe3c78abff056b0b5facb6411a0b3c7c025f452bb7a0658a7d405a11ccbfbf18d5e8efe55cf8fd9d24fa6faf5274c26745e7ad02a77d + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_delivery-profile@npm:1.0.41": + version: 1.0.41 + resolution: "@wix/auto_sdk_ecom_delivery-profile@npm:1.0.41" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/873afcff09a169ca9d9fe6cf41a55507a6a07faafe0776a96fade079ab3da5951778ceb99dcc997e2e9cdc51fabc62ab4b7a9b3c09e37d85f39a56c81a1dbac5 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_delivery-solutions@npm:1.0.12": + version: 1.0.12 + resolution: "@wix/auto_sdk_ecom_delivery-solutions@npm:1.0.12" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/2b0678bdcd1c47ce47f3044f49078a9d01c9811db66557ff3e4780857392cd2ca5d2424f654a1208e2f83f071653964db03de82a5cbfe463cc248bb70da531c9 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_discount-rules@npm:1.0.13": + version: 1.0.13 + resolution: "@wix/auto_sdk_ecom_discount-rules@npm:1.0.13" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/0e10cd8d6d93a6b5d3e064301cf99efb4069c628a1547bbb117b91f0d6777482514639ba58659e8a301076a8db74757712746b4bd78219fbc90f686c574b9271 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_discounts-custom-trigger@npm:1.0.12": + version: 1.0.12 + resolution: "@wix/auto_sdk_ecom_discounts-custom-trigger@npm:1.0.12" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/23afd5045179c4ac7b469e6d8b3fdbc0c8fbede3d586c1d15fc0338f6d15b5edcaf1d9c047a0517d8aa4b0e4c37f5f063d0433a9e345ea6cd5d02383b816e161 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_discounts@npm:1.0.10": + version: 1.0.10 + resolution: "@wix/auto_sdk_ecom_discounts@npm:1.0.10" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/bf0c227160ce9f6731218d8623cacfa18cee510a6a8492f64cc449a46e0c2c8ba2f43f2893bcbc1bd96935dc56aa0241bf688122f7e8a6b326e4b169d84456d1 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_draft-orders@npm:1.0.30": + version: 1.0.30 + resolution: "@wix/auto_sdk_ecom_draft-orders@npm:1.0.30" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/97cdd3a4f728d7d8cabcd585cd3c024779ac1684cc9b975de1c922a6b5502c511f83a83d0cea8b15ef4f4b3012e446fc635fbea686cd05aecb18fe746082fb32 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_gift-vouchers-provider@npm:1.0.10": + version: 1.0.10 + resolution: "@wix/auto_sdk_ecom_gift-vouchers-provider@npm:1.0.10" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/f48f2bbf16ab72360baecab01f1c69a2ba3e513b406d89389ca638c8555dc6b86230a1d9c5738aa71938c6257a58f3e983508200bb8c67702e1487c406a886b2 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_gift-vouchers@npm:1.0.11": + version: 1.0.11 + resolution: "@wix/auto_sdk_ecom_gift-vouchers@npm:1.0.11" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/c2b764b509d40a9217fb02a7910fddef68f0deae2adaafd395c8cb96ae10b303da7ce96caac9422530889f20c6f16bcfe9e4efe20732e2e832444004b349c256 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_local-delivery-options@npm:1.0.12": + version: 1.0.12 + resolution: "@wix/auto_sdk_ecom_local-delivery-options@npm:1.0.12" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/15f90c1164275e209c537d2d211824fde4bde4944f2b685184a7e3869331d545830937082f6552b0b1009cbb4e6989a0686ca446e2376dbf908dda199b87453a + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_memberships@npm:1.0.10": + version: 1.0.10 + resolution: "@wix/auto_sdk_ecom_memberships@npm:1.0.10" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/3e9ffeb1a4bc2877f08f20bb2d776753102888bd14e938096103a4eabab1637777f599ff31f46e08440c11b98503d2ba29971f279a8d1f05b2dd306d1097adbd + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_order-billing@npm:1.0.15": + version: 1.0.15 + resolution: "@wix/auto_sdk_ecom_order-billing@npm:1.0.15" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/cbb5dba20001a76a97ef5487cf502b373fa5de524d40e2de5abcf8036b555383e0fbed0e28526ff5a6db8d2e056a99f34a9d962398c3d266c6e66fcd7433a76a + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_order-fulfillments@npm:1.0.15": + version: 1.0.15 + resolution: "@wix/auto_sdk_ecom_order-fulfillments@npm:1.0.15" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/8d55a89a1a29f12eb41561f470ea6aa9624de273d44fef35dcd307c26b8fb23e68a9d9d6e8bc816c8d7d9659ffc7b7f1cc3e4f13cf3f357062f750eeae518233 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_order-invoices@npm:1.0.13": + version: 1.0.13 + resolution: "@wix/auto_sdk_ecom_order-invoices@npm:1.0.13" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/2e7b281dfb1af13bc9651941ec3ddc952fdc25e536b54e0b1caaf126ddac01c110fdbb1b58271eee9347e969c2b12296b3ee5a945bc84a3f973bdd5a9053d8a7 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_order-payment-requests@npm:1.0.14": + version: 1.0.14 + resolution: "@wix/auto_sdk_ecom_order-payment-requests@npm:1.0.14" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/6f654a744056049a3fb408770fab7aad4fdef4fd9e2ff09030e991aa4bc1d7bd907aae398e4f6f084d9f118f77fe9366f4105ac0cbbb2ba5f806fa4b8149e3f5 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_order-transactions@npm:1.0.18": + version: 1.0.18 + resolution: "@wix/auto_sdk_ecom_order-transactions@npm:1.0.18" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/9c9a1f9bc898148c63e1ce48cfa3a817e82de7e15039c883d6b3d2326b525a281dd0754a80830adf212197350cda5084d632f7b7a650e54585bdd5bc2a234dee + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_orders-settings@npm:1.0.9": + version: 1.0.9 + resolution: "@wix/auto_sdk_ecom_orders-settings@npm:1.0.9" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/4cad0451c713d30b5dd74d1b89a6d22ccbfbb0f8e51937b380f1d9353974a32207996716a4afa7bdb6a2513a8943a6180c182401f72774b39e5e224d4fc7cf1e + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_orders@npm:1.0.45": + version: 1.0.45 + resolution: "@wix/auto_sdk_ecom_orders@npm:1.0.45" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/31d4bd65a4939ffb377f102843af8e34ca50bb46b63df8c32440ec7c21b363382f7d522b49116bed90a9e7970e9900ce8c9b22492ccd965e14098f429aad4ddd + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_payment-settings@npm:1.0.28": + version: 1.0.28 + resolution: "@wix/auto_sdk_ecom_payment-settings@npm:1.0.28" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/67eb34242fdc2a2dccb7740acc04c4b13f9f369fc73a82f79aec6c308f3bdc4a347a249907c652610af8af1dd589844c21acac0c37e31380c8dc3b26d3a875d6 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_pickup-locations@npm:1.0.12": + version: 1.0.12 + resolution: "@wix/auto_sdk_ecom_pickup-locations@npm:1.0.12" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/78b21e8efa0dfbb0c2d132a864f8267bad944296933cb6b3ab73017efd7b6f35165e04dcced8eeadc297d228b8913c8958ece79f380087b82f8773d13cfc0847 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_recommendations-provider@npm:1.0.10": + version: 1.0.10 + resolution: "@wix/auto_sdk_ecom_recommendations-provider@npm:1.0.10" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/fcb3ff9e7dd050a705a9f975061f9fd1bd2dc9e7c9f59ba749614e24c423cdebc7ee08d6c4f6c70613dd157d3c345be5df90bb46e13ef8d182ba22a8c89969e4 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_recommendations@npm:1.0.12": + version: 1.0.12 + resolution: "@wix/auto_sdk_ecom_recommendations@npm:1.0.12" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/e61b911e31a0ffe73498f4bac8f87023b5886e392ecd4929d61f2aad7df64c3f7fd7655659b89e71dec00f63379e3799a45d5e98e60b712981944d3eb07f54a3 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_shipping-options@npm:1.0.16": + version: 1.0.16 + resolution: "@wix/auto_sdk_ecom_shipping-options@npm:1.0.16" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/84a9a8a565c7cc53506b53728d3c533ff21fbb9a617c628ed085e8effeb2dc69a08f7208feae97c77c9247f3874e8e59261ac5b7e3218376105a6a8e38ba5ea8 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_shipping-rates@npm:1.0.10": + version: 1.0.10 + resolution: "@wix/auto_sdk_ecom_shipping-rates@npm:1.0.10" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/93dcf05a831e90258d572e6cfd5410cb60926de12aa68c935142f5e6b57d08f9709f046dda674ce65c917e774d932fb3d7b58cdace08492f74893b7e21bf8a03 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_shippo-configurations@npm:1.0.18": + version: 1.0.18 + resolution: "@wix/auto_sdk_ecom_shippo-configurations@npm:1.0.18" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/43cd4dadaa03c58bf03d7c37aed186b921045428b2741b9bbb3050fd70858b41f730b4686c4e7d0c4a7171e67c3ac62a2bde2f49eafd134a288ef429229f911d + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_subscription-contracts@npm:1.0.29": + version: 1.0.29 + resolution: "@wix/auto_sdk_ecom_subscription-contracts@npm:1.0.29" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/ac80f72c4ef04bf1db6ddd650de9d5651d0317100addc0ba22654f3bc9ab4c8e921c7eeca741d0703be41cc49b90eaeb8ebbae3808a584b976b016bbcca1dd28 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_tax-calculation-provider@npm:1.0.8": + version: 1.0.8 + resolution: "@wix/auto_sdk_ecom_tax-calculation-provider@npm:1.0.8" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/a51e3308b6a58a3c5087e3742ec55a8192fdc9ed660640d0c72941f88a0bba68cbd43d8306e2485a6175557086ac2d6e94efa9093421aa077020e951be65d4b8 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_tax-calculation@npm:1.0.9": + version: 1.0.9 + resolution: "@wix/auto_sdk_ecom_tax-calculation@npm:1.0.9" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/3245576a0ea38d56aeabfba69d789c3b76bd06305c6e60b0597e292e6fd72a280a092b0cc2ff41d40c0fad015613516a7bd2aa88f76d7b5a0246810e1247f65c + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_tax-groups@npm:1.0.15": + version: 1.0.15 + resolution: "@wix/auto_sdk_ecom_tax-groups@npm:1.0.15" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/5da082a97cc7826f8beb5d188625dd3f7919f2080528e3a64a7f57450a6bd3339e94990e32a2a68d5924daf08e3d184ee8238e1427cc90dc1a29b97724775f97 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_tax-regions@npm:1.0.11": + version: 1.0.11 + resolution: "@wix/auto_sdk_ecom_tax-regions@npm:1.0.11" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/a37848a67826f0040ecf7147f9bc2ff1f22774cc0e1ebf3e5ff91af46b7cabd2a21494c8de62918fde776900ba0e50992215e367c5d0c96e3c56bcdd6aad1b1a + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_tip-settings@npm:1.0.14": + version: 1.0.14 + resolution: "@wix/auto_sdk_ecom_tip-settings@npm:1.0.14" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/66edcd9e2f8e37b21d959761485d5fee122f30a9266e82122ec2d45f19bc81ead41e56764b9af5e879589a06eba68541b06fc740ac96afc0048fb07ab2ba57ad + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_tippable-staff@npm:1.0.11": + version: 1.0.11 + resolution: "@wix/auto_sdk_ecom_tippable-staff@npm:1.0.11" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/e678b2e399a393f4b63cf326fcf385b9116e782bad749561e2727c70e00793f14be46bc71bc2645c2d524b1bd51550efe248353fc33b3a933eeebf9f63b39e60 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_tips@npm:1.0.19": + version: 1.0.19 + resolution: "@wix/auto_sdk_ecom_tips@npm:1.0.19" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/08cf42118735bf51e5c239b49d8b1ba3f0f15f30ba18bd7011129f9241d2e17bab4d87ea9aeebf2361395ee3cbb9de3ef261228a11a901ba1ae71cc698840f5d + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_totals-calculator@npm:1.0.20": + version: 1.0.20 + resolution: "@wix/auto_sdk_ecom_totals-calculator@npm:1.0.20" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/061a57b46af62f7d71078134f0a3002c853a6fc63581f15323860b66e8012b0f82d61cc7937348fb6889d2ca2198b84ab4532595ecbc9a02974d301bd5e6c7b7 + languageName: node + linkType: hard + +"@wix/auto_sdk_ecom_validations@npm:1.0.15": + version: 1.0.15 + resolution: "@wix/auto_sdk_ecom_validations@npm:1.0.15" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/066eb4aa1da44c663538544b8da8a1ddf9f7a50a97231132c78ddce8e2b0e5c790e852483d5e462c02084b75f82c0884124e1a9b3d386c59e6d85148a19759bc + languageName: node + linkType: hard + "@wix/auto_sdk_identity_authentication@npm:1.0.3": version: 1.0.3 resolution: "@wix/auto_sdk_identity_authentication@npm:1.0.3" @@ -2497,13 +2965,13 @@ __metadata: languageName: node linkType: hard -"@wix/auto_sdk_redirects_redirects@npm:1.0.3": - version: 1.0.3 - resolution: "@wix/auto_sdk_redirects_redirects@npm:1.0.3" +"@wix/auto_sdk_redirects_redirects@npm:1.0.5": + version: 1.0.5 + resolution: "@wix/auto_sdk_redirects_redirects@npm:1.0.5" dependencies: - "@wix/sdk-runtime": "npm:^0.3.35" - "@wix/sdk-types": "npm:^1.12.4" - checksum: 10c0/03309d5d402a74e70c768fbf26ffea8c4e931dc6ff150de351439474b20aac24506538c9ad2a5d08f3c41c97d8a46f62d185729a5499f1f147451a39acd17fb3 + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/e1dd0ea0c0f08a45262f06fd1e08bef706b6c7ee4a244045e429164e6dd74547894a4ecc2280e0c4fb284ccae028420deb9edc0d99e02681aeafd236704dfa75 languageName: node linkType: hard @@ -2643,6 +3111,81 @@ __metadata: languageName: node linkType: hard +"@wix/ecom@npm:^1.0.1169": + version: 1.0.1169 + resolution: "@wix/ecom@npm:1.0.1169" + dependencies: + "@wix/auto_sdk_ecom_abandoned-checkouts": "npm:1.0.15" + "@wix/auto_sdk_ecom_additional-fees": "npm:1.0.10" + "@wix/auto_sdk_ecom_back-in-stock-notifications": "npm:1.0.17" + "@wix/auto_sdk_ecom_back-in-stock-settings": "npm:1.0.10" + "@wix/auto_sdk_ecom_cart": "npm:1.0.30" + "@wix/auto_sdk_ecom_catalog": "npm:1.0.17" + "@wix/auto_sdk_ecom_checkout": "npm:1.0.30" + "@wix/auto_sdk_ecom_checkout-content": "npm:1.0.8" + "@wix/auto_sdk_ecom_checkout-settings": "npm:1.0.14" + "@wix/auto_sdk_ecom_checkout-templates": "npm:1.0.30" + "@wix/auto_sdk_ecom_currencies": "npm:1.0.10" + "@wix/auto_sdk_ecom_current-cart": "npm:1.0.30" + "@wix/auto_sdk_ecom_custom-triggers": "npm:1.0.11" + "@wix/auto_sdk_ecom_delivery-profile": "npm:1.0.41" + "@wix/auto_sdk_ecom_delivery-solutions": "npm:1.0.12" + "@wix/auto_sdk_ecom_discount-rules": "npm:1.0.13" + "@wix/auto_sdk_ecom_discounts": "npm:1.0.10" + "@wix/auto_sdk_ecom_discounts-custom-trigger": "npm:1.0.12" + "@wix/auto_sdk_ecom_draft-orders": "npm:1.0.30" + "@wix/auto_sdk_ecom_gift-vouchers": "npm:1.0.11" + "@wix/auto_sdk_ecom_gift-vouchers-provider": "npm:1.0.10" + "@wix/auto_sdk_ecom_local-delivery-options": "npm:1.0.12" + "@wix/auto_sdk_ecom_memberships": "npm:1.0.10" + "@wix/auto_sdk_ecom_order-billing": "npm:1.0.15" + "@wix/auto_sdk_ecom_order-fulfillments": "npm:1.0.15" + "@wix/auto_sdk_ecom_order-invoices": "npm:1.0.13" + "@wix/auto_sdk_ecom_order-payment-requests": "npm:1.0.14" + "@wix/auto_sdk_ecom_order-transactions": "npm:1.0.18" + "@wix/auto_sdk_ecom_orders": "npm:1.0.45" + "@wix/auto_sdk_ecom_orders-settings": "npm:1.0.9" + "@wix/auto_sdk_ecom_payment-settings": "npm:1.0.28" + "@wix/auto_sdk_ecom_pickup-locations": "npm:1.0.12" + "@wix/auto_sdk_ecom_recommendations": "npm:1.0.12" + "@wix/auto_sdk_ecom_recommendations-provider": "npm:1.0.10" + "@wix/auto_sdk_ecom_shipping-options": "npm:1.0.16" + "@wix/auto_sdk_ecom_shipping-rates": "npm:1.0.10" + "@wix/auto_sdk_ecom_shippo-configurations": "npm:1.0.18" + "@wix/auto_sdk_ecom_subscription-contracts": "npm:1.0.29" + "@wix/auto_sdk_ecom_tax-calculation": "npm:1.0.9" + "@wix/auto_sdk_ecom_tax-calculation-provider": "npm:1.0.8" + "@wix/auto_sdk_ecom_tax-groups": "npm:1.0.15" + "@wix/auto_sdk_ecom_tax-regions": "npm:1.0.11" + "@wix/auto_sdk_ecom_tip-settings": "npm:1.0.14" + "@wix/auto_sdk_ecom_tippable-staff": "npm:1.0.11" + "@wix/auto_sdk_ecom_tips": "npm:1.0.19" + "@wix/auto_sdk_ecom_totals-calculator": "npm:1.0.20" + "@wix/auto_sdk_ecom_validations": "npm:1.0.15" + "@wix/ecom_app-extensions": "npm:1.0.81" + checksum: 10c0/858d25f74b0bcaef291d76a1c2e4b957b9c3464a9f4870c477dbf138fe68ac90af42dffe3b8aecfab43830297752b8dcfec42d681ac313e55591a5f45f93cd20 + languageName: node + linkType: hard + +"@wix/ecom_app-extensions@npm:1.0.81": + version: 1.0.81 + resolution: "@wix/ecom_app-extensions@npm:1.0.81" + dependencies: + "@wix/sdk-runtime": "npm:^0.3.49" + "@wix/sdk-types": "npm:^1.13.23" + checksum: 10c0/ed10a17f9651aa4b020f12209d4b24bf20c49f4f946246c1565d8d63d0713fab8e79f22d6fa0bdcf4869d5e175513522fdacf72153eda660ec1634036793df3b + languageName: node + linkType: hard + +"@wix/error-handler-types@npm:^1.6.0": + version: 1.6.0 + resolution: "@wix/error-handler-types@npm:1.6.0" + dependencies: + "@babel/runtime": "npm:^7.27.1" + checksum: 10c0/0aab8f92434acc173548f8a16220c1159471726f8c932eca24f364852e739a4271f8d1883f095ff109a7bfd2174a47597f6d1721b4318b4505e19ca62c048b5a + languageName: node + linkType: hard + "@wix/filter-builder@npm:1.0.127": version: 1.0.127 resolution: "@wix/filter-builder@npm:1.0.127" @@ -2700,6 +3243,8 @@ __metadata: resolution: "@wix/headless-stores@workspace:packages/headless-components/stores" dependencies: "@types/node": "npm:^20.9.0" + "@wix/ecom": "npm:^1.0.1169" + "@wix/redirects": "npm:^1.0.79" typescript: "npm:^5.7.3" languageName: unknown linkType: soft @@ -2768,12 +3313,12 @@ __metadata: languageName: node linkType: hard -"@wix/redirects@npm:^1.0.70": - version: 1.0.77 - resolution: "@wix/redirects@npm:1.0.77" +"@wix/redirects@npm:^1.0.70, @wix/redirects@npm:^1.0.79": + version: 1.0.79 + resolution: "@wix/redirects@npm:1.0.79" dependencies: - "@wix/auto_sdk_redirects_redirects": "npm:1.0.3" - checksum: 10c0/2e95c3c87d54d45be8f2268448e5af9c7e6d023a31483ff98783da70cb751e5e986f01382ffdd4fdf21ec9b739ad45b32d8adf37e465763c01678f2e840fd7d7 + "@wix/auto_sdk_redirects_redirects": "npm:1.0.5" + checksum: 10c0/ff6a7bfa73dbc935e6f2724dd77d23e5b4e48aa94d95479f181676366540a551d2e55aedf775d2d741c01a6a8428f4bbf5892f57725a1764fb3af054723a72b6 languageName: node linkType: hard @@ -2879,7 +3424,7 @@ __metadata: languageName: node linkType: hard -"@wix/sdk-runtime@npm:0.3.46, @wix/sdk-runtime@npm:^0.3.35, @wix/sdk-runtime@npm:^0.3.41, @wix/sdk-runtime@npm:^0.3.42": +"@wix/sdk-runtime@npm:0.3.46": version: 0.3.46 resolution: "@wix/sdk-runtime@npm:0.3.46" dependencies: @@ -2889,13 +3434,24 @@ __metadata: languageName: node linkType: hard -"@wix/sdk-types@npm:^1.12.4, @wix/sdk-types@npm:^1.13.14, @wix/sdk-types@npm:^1.13.8, @wix/sdk-types@npm:^1.13.9": - version: 1.13.14 - resolution: "@wix/sdk-types@npm:1.13.14" +"@wix/sdk-runtime@npm:^0.3.41, @wix/sdk-runtime@npm:^0.3.42, @wix/sdk-runtime@npm:^0.3.49": + version: 0.3.52 + resolution: "@wix/sdk-runtime@npm:0.3.52" + dependencies: + "@wix/sdk-context": "npm:0.0.1" + "@wix/sdk-types": "npm:^1.13.25" + checksum: 10c0/1dbf3ec067142a1f16878f1355ebb6d64480b1598dfa4ef1b72970604e317d8b1e64cd5698273b11ef95a10099e1bce6f7d325d4388646b0ae424a9ecc6188b7 + languageName: node + linkType: hard + +"@wix/sdk-types@npm:^1.13.14, @wix/sdk-types@npm:^1.13.23, @wix/sdk-types@npm:^1.13.25, @wix/sdk-types@npm:^1.13.8, @wix/sdk-types@npm:^1.13.9": + version: 1.13.25 + resolution: "@wix/sdk-types@npm:1.13.25" dependencies: + "@wix/error-handler-types": "npm:^1.6.0" "@wix/monitoring-types": "npm:^0.12.0" - type-fest: "npm:^4.40.0" - checksum: 10c0/d8b5e429619cd341c42168e002f0940fe4eddfe4cfa48352b861240e164ba4ffb58d3ee664b5666f6dec4b1652512b195123f5d286fc9e6a37b3c2a6814f16f8 + type-fest: "npm:^4.41.0" + checksum: 10c0/45293a8a2ee1f2880cfd94f70da67cd285ab7fd0f58daba652860238572845f3cd8abffdb96220e610dccb8ca0c14decc6b52d9d169c88f9228b0466a0b08282 languageName: node linkType: hard @@ -8849,13 +9405,6 @@ __metadata: languageName: node linkType: hard -"regenerator-runtime@npm:^0.14.0": - version: 0.14.1 - resolution: "regenerator-runtime@npm:0.14.1" - checksum: 10c0/1b16eb2c4bceb1665c89de70dcb64126a22bc8eb958feef3cd68fe11ac6d2a4899b5cd1b80b0774c7c03591dc57d16631a7f69d2daa2ec98100e2f29f7ec4cc4 - languageName: node - linkType: hard - "regex-recursion@npm:^6.0.2": version: 6.0.2 resolution: "regex-recursion@npm:6.0.2" @@ -10039,10 +10588,10 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.21.0, type-fest@npm:^4.40.0": - version: 4.40.0 - resolution: "type-fest@npm:4.40.0" - checksum: 10c0/b39d4da6f9a154e3db7e714cd05ccf56b53f4f0bbf74dd294cb6be4921b16ecca5cb00cb81b53ab621a31c8e8509c74b5101895ada47af9de368a317d24538a3 +"type-fest@npm:^4.21.0, type-fest@npm:^4.40.0, type-fest@npm:^4.41.0": + version: 4.41.0 + resolution: "type-fest@npm:4.41.0" + checksum: 10c0/f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4 languageName: node linkType: hard From 728a4b8f435c1b87e290092030d4b6d0bd12bac0 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Mon, 26 May 2025 15:29:12 +0300 Subject: [PATCH 02/42] Try to integrate --- examples/astro-components-demo/src/components/App.jsx | 6 ------ examples/astro-components-demo/src/pages/index.astro | 6 ++++++ .../headless-components/stores/src/react/index.tsx | 10 +++++++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index d866fe71f..c79a885dc 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -1,15 +1,9 @@ import logo from "../assets/logo.svg"; import "./App.css"; -import { Bookings } from "@wix/headless-bookings/react"; -import { Ecom } from "@wix/headless-ecom/react"; -import { Stores } from "@wix/headless-stores/react"; function App() { return (
- - -
logo

diff --git a/examples/astro-components-demo/src/pages/index.astro b/examples/astro-components-demo/src/pages/index.astro index 4c5d40cc1..b3f46cf46 100644 --- a/examples/astro-components-demo/src/pages/index.astro +++ b/examples/astro-components-demo/src/pages/index.astro @@ -2,8 +2,14 @@ import React from "react"; import App from "../components/App.jsx"; import Layout from "../layouts/Layout.astro"; +import { BuyNow } from "@wix/headless-stores/react"; --- + + {({ isLoading, redirectToCheckout }) => { + return ; + }} + diff --git a/packages/headless-components/stores/src/react/index.tsx b/packages/headless-components/stores/src/react/index.tsx index 3fba9bef0..18c1e41ba 100644 --- a/packages/headless-components/stores/src/react/index.tsx +++ b/packages/headless-components/stores/src/react/index.tsx @@ -10,9 +10,11 @@ export function BuyNow(props: { redirectToCheckout: () => void; }) => React.ReactNode; }) { + console.log("BuyNow component render:start", props); const [isLoading, setIsLoading] = useState(false); const redirectToCheckout = async () => { + console.log("BuyNow redirectToCheckout"); try { setIsLoading(true); const createdCart = await cart.createCart({ @@ -31,13 +33,17 @@ export function BuyNow(props: { }); if (!createdCart || !createdCart._id) { - throw new Error("Failed to create cart"); + throw new Error("Failed to create cart :("); } + console.log(`Buy now cart created: ${createdCart._id}`); + const { checkoutId } = await cart.createCheckout(createdCart._id, { channelType: cart.ChannelType.WEB }); + console.log(`Buy now checkout created: ${checkoutId}`); + const { redirectSession } = await redirects.createRedirectSession({ ecomCheckout: { checkoutId }, callbacks: { @@ -45,6 +51,7 @@ export function BuyNow(props: { }, }); + console.log(`Buy now redirecting to: ${redirectSession?.fullUrl}`); window.location.href = redirectSession?.fullUrl!; } catch (error) { console.error("Error during checkout:", error); @@ -53,6 +60,7 @@ export function BuyNow(props: { } } + console.log("BuyNow component render:end"); return props.children({ isLoading, redirectToCheckout, From 99df155fd6f1f19fc5dc07ebb6cb59b2977f2658 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Mon, 26 May 2025 16:04:11 +0300 Subject: [PATCH 03/42] Wroking buy now --- .../src/components/ui/buy-now.tsx | 15 +++++++ .../src/pages/index.astro | 9 ++-- .../stores/src/react/index.tsx | 41 ++++++------------- 3 files changed, 31 insertions(+), 34 deletions(-) create mode 100644 examples/astro-components-demo/src/components/ui/buy-now.tsx diff --git a/examples/astro-components-demo/src/components/ui/buy-now.tsx b/examples/astro-components-demo/src/components/ui/buy-now.tsx new file mode 100644 index 000000000..cfc119334 --- /dev/null +++ b/examples/astro-components-demo/src/components/ui/buy-now.tsx @@ -0,0 +1,15 @@ +import { BuyNow as BuyNowPrimitive } from "@wix/headless-stores/react"; + +export function BuyNow(props: Omit, "children">) { + return + {({ isLoading, redirectToCheckout }) => { + if (isLoading) return <>Preparing checkout...; + + return ( + + ); + }} + +} diff --git a/examples/astro-components-demo/src/pages/index.astro b/examples/astro-components-demo/src/pages/index.astro index b3f46cf46..9e533bb37 100644 --- a/examples/astro-components-demo/src/pages/index.astro +++ b/examples/astro-components-demo/src/pages/index.astro @@ -2,14 +2,11 @@ import React from "react"; import App from "../components/App.jsx"; import Layout from "../layouts/Layout.astro"; -import { BuyNow } from "@wix/headless-stores/react"; +import { BuyNow } from "../components/ui/buy-now"; + --- - - {({ isLoading, redirectToCheckout }) => { - return ; - }} - + diff --git a/packages/headless-components/stores/src/react/index.tsx b/packages/headless-components/stores/src/react/index.tsx index 18c1e41ba..3924017fc 100644 --- a/packages/headless-components/stores/src/react/index.tsx +++ b/packages/headless-components/stores/src/react/index.tsx @@ -1,5 +1,5 @@ import { cart } from "@wix/ecom"; -import { redirects } from "@wix/redirects"; +import { redirects, checkout } from "@wix/redirects"; import { useState } from "react"; export function BuyNow(props: { @@ -17,33 +17,21 @@ export function BuyNow(props: { console.log("BuyNow redirectToCheckout"); try { setIsLoading(true); - const createdCart = await cart.createCart({ - lineItems: [ - { - catalogReference: { - catalogItemId: props.productId, - appId: "1380b703-ce81-ff05-f115-39571d94dfcd", - options: { - options: props.variant, - } - }, - quantity: 1 - }, - ], - }); - - if (!createdCart || !createdCart._id) { - throw new Error("Failed to create cart :("); - } - console.log(`Buy now cart created: ${createdCart._id}`); - - const { checkoutId } = await cart.createCheckout(createdCart._id, { - channelType: cart.ChannelType.WEB + const { checkoutId } = await checkout.createCheckout({ + lineItems: [{ + catalogReference: { + catalogItemId: props.productId, + appId: "1380b703-ce81-ff05-f115-39571d94dfcd", + options: { + options: props.variant, + } + }, + quantity: 1 + }], + channelType: checkout.ChannelType.WEB, }); - console.log(`Buy now checkout created: ${checkoutId}`); - const { redirectSession } = await redirects.createRedirectSession({ ecomCheckout: { checkoutId }, callbacks: { @@ -51,10 +39,7 @@ export function BuyNow(props: { }, }); - console.log(`Buy now redirecting to: ${redirectSession?.fullUrl}`); window.location.href = redirectSession?.fullUrl!; - } catch (error) { - console.error("Error during checkout:", error); } finally { setIsLoading(false); } From 996fe5d23f5d7d55f8ec4974d1df01d02dbd1000 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Tue, 27 May 2025 11:14:28 +0300 Subject: [PATCH 04/42] Cleanup --- .../stores/src/react/index.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/headless-components/stores/src/react/index.tsx b/packages/headless-components/stores/src/react/index.tsx index 3924017fc..861fc6dc0 100644 --- a/packages/headless-components/stores/src/react/index.tsx +++ b/packages/headless-components/stores/src/react/index.tsx @@ -1,7 +1,10 @@ -import { cart } from "@wix/ecom"; -import { redirects, checkout } from "@wix/redirects"; +import { checkout } from "@wix/ecom"; +import { redirects } from "@wix/redirects"; import { useState } from "react"; +// const CATALOG_APP_ID = "1380b703-ce81-ff05-f115-39571d94dfcd"; +const CATLOG_APP_ID_V3 = "215238eb-22a5-4c36-9e7b-e7c08025e04e"; + export function BuyNow(props: { productId: string; variant?: Record; @@ -10,19 +13,17 @@ export function BuyNow(props: { redirectToCheckout: () => void; }) => React.ReactNode; }) { - console.log("BuyNow component render:start", props); const [isLoading, setIsLoading] = useState(false); const redirectToCheckout = async () => { - console.log("BuyNow redirectToCheckout"); try { setIsLoading(true); - const { checkoutId } = await checkout.createCheckout({ + const checkoutResult = await checkout.createCheckout({ lineItems: [{ catalogReference: { catalogItemId: props.productId, - appId: "1380b703-ce81-ff05-f115-39571d94dfcd", + appId: CATLOG_APP_ID_V3, options: { options: props.variant, } @@ -32,8 +33,12 @@ export function BuyNow(props: { channelType: checkout.ChannelType.WEB, }); + if (!checkoutResult._id) { + throw new Error("Failed to create checkout"); + } + const { redirectSession } = await redirects.createRedirectSession({ - ecomCheckout: { checkoutId }, + ecomCheckout: { checkoutId: checkoutResult._id }, callbacks: { postFlowUrl: window.location.href, }, @@ -45,7 +50,6 @@ export function BuyNow(props: { } } - console.log("BuyNow component render:end"); return props.children({ isLoading, redirectToCheckout, From e7e52b09051ddeeb98737f40a40026d3734b5444 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Tue, 27 May 2025 11:35:08 +0300 Subject: [PATCH 05/42] Sprinkle tests --- .../headless-components/stores/package.json | 11 +- .../stores/src/react/BuyNow.test.tsx | 141 +++ .../stores/src/react/BuyNow.tsx | 58 ++ .../stores/src/react/index.tsx | 63 +- .../stores/src/vitest.setup.ts | 1 + .../stores/vitest.config.ts | 9 + yarn.lock | 836 +++++++++++++++++- 7 files changed, 1030 insertions(+), 89 deletions(-) create mode 100644 packages/headless-components/stores/src/react/BuyNow.test.tsx create mode 100644 packages/headless-components/stores/src/react/BuyNow.tsx create mode 100644 packages/headless-components/stores/src/vitest.setup.ts create mode 100644 packages/headless-components/stores/vitest.config.ts diff --git a/packages/headless-components/stores/package.json b/packages/headless-components/stores/package.json index 119f7c7fd..a5c607d5e 100644 --- a/packages/headless-components/stores/package.json +++ b/packages/headless-components/stores/package.json @@ -2,14 +2,21 @@ "name": "@wix/headless-stores", "private": true, "scripts": { - "build": "tsc" + "build": "tsc", + "test": "vitest" }, "exports": { "./react": "./dist/react/index.js" }, "devDependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", "@types/node": "^20.9.0", - "typescript": "^5.7.3" + "@vitest/ui": "^3.1.4", + "jsdom": "^26.1.0", + "typescript": "^5.7.3", + "vitest": "^3.1.4" }, "dependencies": { "@wix/ecom": "^1.0.1169", diff --git a/packages/headless-components/stores/src/react/BuyNow.test.tsx b/packages/headless-components/stores/src/react/BuyNow.test.tsx new file mode 100644 index 000000000..aa40153cd --- /dev/null +++ b/packages/headless-components/stores/src/react/BuyNow.test.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; +import { BuyNow } from './BuyNow'; // This is the component under test + +vi.mock('@wix/ecom', () => ({ + checkout: { + createCheckout: vi.fn(), // Define vi.fn() directly here + ChannelType: { WEB: 'WEB' }, + }, +})); + +vi.mock('@wix/redirects', () => ({ + redirects: { + createRedirectSession: vi.fn(), // Define vi.fn() directly here + }, +})); + +const originalLocation = window.location; +let ecomCheckoutMock: any; +let redirectsMock: any; + +beforeEach(async () => { + // Dynamically import the mocked modules to get the mock functions + const ecom = await import('@wix/ecom'); + ecomCheckoutMock = ecom.checkout; + const redirectsModule = await import('@wix/redirects'); + redirectsMock = redirectsModule.redirects; + + vi.clearAllMocks(); // This will clear all vi.fn() instances, including those above + + delete (window as any).location; + (window as any).location = { ...originalLocation, href: '' }; + + // Default successful responses + ecomCheckoutMock.createCheckout.mockResolvedValue({ _id: 'test-checkout-id' }); + redirectsMock.createRedirectSession.mockResolvedValue({ + redirectSession: { fullUrl: 'http://mocked-redirect-url.com' }, + }); +}); + +afterEach(() => { + window.location = originalLocation; +}); + +describe('BuyNow Component from @wix/headless-stores/react', () => { + const testProductId = 'test-product-123'; + const testVariant = { color: 'blue' }; + + // Updated to capture the redirectToCheckout function for direct invocation in rejection tests + const renderComponent = (props = {}) => { + let capturedRedirectToCheckout: () => Promise = async () => {}; + const renderOutput = render( + + {({ isLoading, redirectToCheckout }) => { + capturedRedirectToCheckout = redirectToCheckout as () => Promise; + if (isLoading) return

Loading...
; + return ; + }} + + ); + return { ...renderOutput, redirectToCheckoutDirectly: capturedRedirectToCheckout }; + }; + + test('should render the button with children render prop', () => { + renderComponent(); + expect(screen.getByRole('button', { name: /Buy Product Now/i })).toBeInTheDocument(); + }); + + test('should show loading state and call checkout and redirect services on click', async () => { + renderComponent(); + const button = screen.getByRole('button', { name: /Buy Product Now/i }); + fireEvent.click(button); + + expect(screen.getByText(/Loading.../i)).toBeInTheDocument(); + + await waitFor(() => { + expect(ecomCheckoutMock.createCheckout).toHaveBeenCalledTimes(1); + }); + expect(ecomCheckoutMock.createCheckout).toHaveBeenCalledWith({ + lineItems: [{ + catalogReference: { + catalogItemId: testProductId, + appId: '215238eb-22a5-4c36-9e7b-e7c08025e04e', + options: { + options: testVariant, + } + }, + quantity: 1 + }], + channelType: 'WEB', + }); + + await waitFor(() => { + expect(redirectsMock.createRedirectSession).toHaveBeenCalledTimes(1); + }); + expect(redirectsMock.createRedirectSession).toHaveBeenCalledWith({ + ecomCheckout: { checkoutId: 'test-checkout-id' }, + callbacks: { + postFlowUrl: expect.any(String), + }, + }); + + await waitFor(() => { + expect(window.location.href).toBe('http://mocked-redirect-url.com'); + }); + + expect(screen.getByRole('button', { name: /Buy Product Now/i })).toBeInTheDocument(); + }); + + test('should handle checkout creation failure and reject', async () => { + ecomCheckoutMock.createCheckout.mockResolvedValueOnce({ _id: null }); + + const { redirectToCheckoutDirectly } = renderComponent(); + await act(async () => { + await expect(redirectToCheckoutDirectly()).rejects.toThrow('Failed to create checkout'); + }); + + expect(screen.getByRole('button', { name: /Buy Product Now/i })).toBeInTheDocument(); + expect(ecomCheckoutMock.createCheckout).toHaveBeenCalledTimes(1); + expect(redirectsMock.createRedirectSession).not.toHaveBeenCalled(); + }); + + test('should set isLoading to false and reject if redirects.createRedirectSession throws', async () => { + redirectsMock.createRedirectSession.mockRejectedValueOnce(new Error('Redirect failed')); + + const { redirectToCheckoutDirectly } = renderComponent(); + await act(async () => { + await expect(redirectToCheckoutDirectly()).rejects.toThrow('Redirect failed'); + }); + + expect(screen.getByRole('button', { name: /Buy Product Now/i })).toBeInTheDocument(); + expect(redirectsMock.createRedirectSession).toHaveBeenCalledTimes(1); + expect(window.location.href).not.toBe('http://mocked-redirect-url.com'); + }); +}); diff --git a/packages/headless-components/stores/src/react/BuyNow.tsx b/packages/headless-components/stores/src/react/BuyNow.tsx new file mode 100644 index 000000000..9c0c0a1df --- /dev/null +++ b/packages/headless-components/stores/src/react/BuyNow.tsx @@ -0,0 +1,58 @@ +import { checkout } from "@wix/ecom"; +import { redirects } from "@wix/redirects"; +import { useState } from "react"; + +// const CATALOG_APP_ID = "1380b703-ce81-ff05-f115-39571d94dfcd"; +const CATLOG_APP_ID_V3 = "215238eb-22a5-4c36-9e7b-e7c08025e04e"; + +export function BuyNow(props: { + productId: string; + variant?: Record; + children: (props: { + isLoading: boolean; + redirectToCheckout: () => void; + }) => React.ReactNode; +}) { + const [isLoading, setIsLoading] = useState(false); + + const redirectToCheckout = async () => { + try { + setIsLoading(true); + + const checkoutResult = await checkout.createCheckout({ + lineItems: [{ + catalogReference: { + catalogItemId: props.productId, + appId: CATLOG_APP_ID_V3, + options: { + options: props.variant, + } + }, + quantity: 1 + }], + channelType: checkout.ChannelType.WEB, + }); + + if (!checkoutResult._id) { + throw new Error("Failed to create checkout"); + } + + const { redirectSession } = await redirects.createRedirectSession({ + ecomCheckout: { checkoutId: checkoutResult._id }, + callbacks: { + postFlowUrl: window.location.href, + }, + }); + + window.location.href = redirectSession?.fullUrl!; + } finally { + setIsLoading(false); + } + } + + return props.children({ + isLoading, + redirectToCheckout, + }) +} + diff --git a/packages/headless-components/stores/src/react/index.tsx b/packages/headless-components/stores/src/react/index.tsx index 861fc6dc0..e0cb7c378 100644 --- a/packages/headless-components/stores/src/react/index.tsx +++ b/packages/headless-components/stores/src/react/index.tsx @@ -1,62 +1 @@ -import { checkout } from "@wix/ecom"; -import { redirects } from "@wix/redirects"; -import { useState } from "react"; - -// const CATALOG_APP_ID = "1380b703-ce81-ff05-f115-39571d94dfcd"; -const CATLOG_APP_ID_V3 = "215238eb-22a5-4c36-9e7b-e7c08025e04e"; - -export function BuyNow(props: { - productId: string; - variant?: Record; - children: (props: { - isLoading: boolean; - redirectToCheckout: () => void; - }) => React.ReactNode; -}) { - const [isLoading, setIsLoading] = useState(false); - - const redirectToCheckout = async () => { - try { - setIsLoading(true); - - const checkoutResult = await checkout.createCheckout({ - lineItems: [{ - catalogReference: { - catalogItemId: props.productId, - appId: CATLOG_APP_ID_V3, - options: { - options: props.variant, - } - }, - quantity: 1 - }], - channelType: checkout.ChannelType.WEB, - }); - - if (!checkoutResult._id) { - throw new Error("Failed to create checkout"); - } - - const { redirectSession } = await redirects.createRedirectSession({ - ecomCheckout: { checkoutId: checkoutResult._id }, - callbacks: { - postFlowUrl: window.location.href, - }, - }); - - window.location.href = redirectSession?.fullUrl!; - } finally { - setIsLoading(false); - } - } - - return props.children({ - isLoading, - redirectToCheckout, - }) -} - -export function Stores() { - return
Stores
; -} - +export { BuyNow } from "./BuyNow"; diff --git a/packages/headless-components/stores/src/vitest.setup.ts b/packages/headless-components/stores/src/vitest.setup.ts new file mode 100644 index 000000000..bb02c60cd --- /dev/null +++ b/packages/headless-components/stores/src/vitest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest'; diff --git a/packages/headless-components/stores/vitest.config.ts b/packages/headless-components/stores/vitest.config.ts new file mode 100644 index 000000000..80a8bfbe1 --- /dev/null +++ b/packages/headless-components/stores/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/vitest.setup.ts'], // Adjusted path to be relative to this config file + }, +}); diff --git a/yarn.lock b/yarn.lock index 11ae089a0..623e00033 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,13 @@ __metadata: version: 8 cacheKey: 10c0 +"@adobe/css-tools@npm:^4.4.0": + version: 4.4.3 + resolution: "@adobe/css-tools@npm:4.4.3" + checksum: 10c0/6d16c4d4b6752d73becf6e58611f893c7ed96e04017ff7084310901ccdbe0295171b722b158f6a2b0aa77182ef3446ffd62b39488fa5a7adab1f0dfe5ffafbae + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -15,6 +22,19 @@ __metadata: languageName: node linkType: hard +"@asamuzakjp/css-color@npm:^3.1.2": + version: 3.2.0 + resolution: "@asamuzakjp/css-color@npm:3.2.0" + dependencies: + "@csstools/css-calc": "npm:^2.1.3" + "@csstools/css-color-parser": "npm:^3.0.9" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" + lru-cache: "npm:^10.4.3" + checksum: 10c0/a4bf1c831751b1fae46b437e37e8a38c0b5bd58d23230157ae210bd1e905fe509b89b7c243e63d1522d852668a6292ed730a160e21342772b4e5b7b8ea14c092 + languageName: node + linkType: hard + "@astrojs/compiler@npm:^2.11.0": version: 2.11.0 resolution: "@astrojs/compiler@npm:2.11.0" @@ -112,14 +132,14 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.26.2": - version: 7.26.2 - resolution: "@babel/code-frame@npm:7.26.2" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.26.2": + version: 7.27.1 + resolution: "@babel/code-frame@npm:7.27.1" dependencies: - "@babel/helper-validator-identifier": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.27.1" js-tokens: "npm:^4.0.0" - picocolors: "npm:^1.0.0" - checksum: 10c0/7d79621a6849183c415486af99b1a20b84737e8c11cd55b6544f688c51ce1fd710e6d869c3dd21232023da272a79b91efb3e83b5bc2dc65c1187c5fcd1b72ea8 + picocolors: "npm:^1.1.1" + checksum: 10c0/5dd9a18baa5fce4741ba729acc3a3272c49c25cb8736c4b18e113099520e7ef7b545a4096a26d600e4416157e63e87d66db46aa3fbf0a5f2286da2705c12da00 languageName: node linkType: hard @@ -216,10 +236,10 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-validator-identifier@npm:7.25.9" - checksum: 10c0/4fc6f830177b7b7e887ad3277ddb3b91d81e6c4a24151540d9d1023e8dc6b1c0505f0f0628ae653601eb4388a8db45c1c14b2c07a9173837aef7e4116456259d +"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-validator-identifier@npm:7.27.1" + checksum: 10c0/c558f11c4871d526498e49d07a84752d1800bf72ac0d3dad100309a2eaba24efbf56ea59af5137ff15e3a00280ebe588560534b0e894a4750f8b1411d8f78b84 languageName: node linkType: hard @@ -273,7 +293,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.18, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.1, @babel/runtime@npm:^7.4.5, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.18, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.1, @babel/runtime@npm:^7.4.5, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.27.1 resolution: "@babel/runtime@npm:7.27.1" checksum: 10c0/530a7332f86ac5a7442250456823a930906911d895c0b743bf1852efc88a20a016ed4cd26d442d0ca40ae6d5448111e02a08dd638a4f1064b47d080e2875dc05 @@ -456,6 +476,52 @@ __metadata: languageName: node linkType: hard +"@csstools/color-helpers@npm:^5.0.2": + version: 5.0.2 + resolution: "@csstools/color-helpers@npm:5.0.2" + checksum: 10c0/bebaddb28b9eb58b0449edd5d0c0318fa88f3cb079602ee27e88c9118070d666dcc4e09a5aa936aba2fde6ba419922ade07b7b506af97dd7051abd08dfb2959b + languageName: node + linkType: hard + +"@csstools/css-calc@npm:^2.1.3": + version: 2.1.3 + resolution: "@csstools/css-calc@npm:2.1.3" + peerDependencies: + "@csstools/css-parser-algorithms": ^3.0.4 + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/85f5b4f96d60f395d5f0108056b0ddee037b22d6deba448d74324b50f1c554de284f84715ebfac7b2888b78e09d20d02a7cd213ee7bdaa71011ea9b4eee3a251 + languageName: node + linkType: hard + +"@csstools/css-color-parser@npm:^3.0.9": + version: 3.0.9 + resolution: "@csstools/css-color-parser@npm:3.0.9" + dependencies: + "@csstools/color-helpers": "npm:^5.0.2" + "@csstools/css-calc": "npm:^2.1.3" + peerDependencies: + "@csstools/css-parser-algorithms": ^3.0.4 + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/acc026a6bd6d8c4c641fa5f9b4d77cd5dfa54c57c3278ae52329d96b5837723428dcb93c34db4062bbea2f45a98451119df06eaf39fd196aaf6368c59d799f20 + languageName: node + linkType: hard + +"@csstools/css-parser-algorithms@npm:^3.0.4": + version: 3.0.4 + resolution: "@csstools/css-parser-algorithms@npm:3.0.4" + peerDependencies: + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/d411f07765e14eede17bccc6bd4f90ff303694df09aabfede3fd104b2dfacfd4fe3697cd25ddad14684c850328f3f9420ebfa9f78380892492974db24ae47dbd + languageName: node + linkType: hard + +"@csstools/css-tokenizer@npm:^3.0.3": + version: 3.0.3 + resolution: "@csstools/css-tokenizer@npm:3.0.3" + checksum: 10c0/c31bf410e1244b942e71798e37c54639d040cb59e0121b21712b40015fced2b0fb1ffe588434c5f8923c9cd0017cfc1c1c8f3921abc94c96edf471aac2eba5e5 + languageName: node + linkType: hard + "@dnd-kit/accessibility@npm:^3.1.1": version: 3.1.1 resolution: "@dnd-kit/accessibility@npm:3.1.1" @@ -1191,6 +1257,13 @@ __metadata: languageName: node linkType: hard +"@polka/url@npm:^1.0.0-next.24": + version: 1.0.0-next.29 + resolution: "@polka/url@npm:1.0.0-next.29" + checksum: 10c0/0d58e081844095cb029d3c19a659bfefd09d5d51a2f791bc61eba7ea826f13d6ee204a8a448c2f5a855c17df07b37517373ff916dd05801063c0568ae9937684 + languageName: node + linkType: hard + "@popperjs/core@npm:2.11.2": version: 2.11.2 resolution: "@popperjs/core@npm:2.11.2" @@ -1629,6 +1702,57 @@ __metadata: languageName: node linkType: hard +"@testing-library/dom@npm:^10.4.0": + version: 10.4.0 + resolution: "@testing-library/dom@npm:10.4.0" + dependencies: + "@babel/code-frame": "npm:^7.10.4" + "@babel/runtime": "npm:^7.12.5" + "@types/aria-query": "npm:^5.0.1" + aria-query: "npm:5.3.0" + chalk: "npm:^4.1.0" + dom-accessibility-api: "npm:^0.5.9" + lz-string: "npm:^1.5.0" + pretty-format: "npm:^27.0.2" + checksum: 10c0/0352487720ecd433400671e773df0b84b8268fb3fe8e527cdfd7c11b1365b398b4e0eddba6e7e0c85e8d615f48257753283fccec41f6b986fd6c85f15eb5f84f + languageName: node + linkType: hard + +"@testing-library/jest-dom@npm:^6.6.3": + version: 6.6.3 + resolution: "@testing-library/jest-dom@npm:6.6.3" + dependencies: + "@adobe/css-tools": "npm:^4.4.0" + aria-query: "npm:^5.0.0" + chalk: "npm:^3.0.0" + css.escape: "npm:^1.5.1" + dom-accessibility-api: "npm:^0.6.3" + lodash: "npm:^4.17.21" + redent: "npm:^3.0.0" + checksum: 10c0/5566b6c0b7b0709bc244aec3aa3dc9e5f4663e8fb2b99d8cd456fc07279e59db6076cbf798f9d3099a98fca7ef4cd50e4e1f4c4dec5a60a8fad8d24a638a5bf6 + languageName: node + linkType: hard + +"@testing-library/react@npm:^16.3.0": + version: 16.3.0 + resolution: "@testing-library/react@npm:16.3.0" + dependencies: + "@babel/runtime": "npm:^7.12.5" + peerDependencies: + "@testing-library/dom": ^10.0.0 + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/3a2cb1f87c9a67e1ebbbcfd99b94b01e496fc35147be8bc5d8bf07a699c7d523a09d57ef2f7b1d91afccd1a28e21eda3b00d80187fbb51b1de01e422592d845e + languageName: node + linkType: hard + "@tiptap/core@npm:2.8.0": version: 2.8.0 resolution: "@tiptap/core@npm:2.8.0" @@ -1716,6 +1840,13 @@ __metadata: languageName: node linkType: hard +"@types/aria-query@npm:^5.0.1": + version: 5.0.4 + resolution: "@types/aria-query@npm:5.0.4" + checksum: 10c0/dc667bc6a3acc7bba2bccf8c23d56cb1f2f4defaa704cfef595437107efaa972d3b3db9ec1d66bc2711bfc35086821edd32c302bffab36f2e79b97f312069f08 + languageName: node + linkType: hard + "@types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" @@ -2106,6 +2237,104 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/expect@npm:3.1.4" + dependencies: + "@vitest/spy": "npm:3.1.4" + "@vitest/utils": "npm:3.1.4" + chai: "npm:^5.2.0" + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/9cfd7eb6d965a179b4ec0610a9c08b14dc97dbaf81925c8209a054f7a2a3d1eef59fa5e5cd4dd9bf8cb940d85aee5f5102555511a94be9933faf4cc734462a16 + languageName: node + linkType: hard + +"@vitest/mocker@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/mocker@npm:3.1.4" + dependencies: + "@vitest/spy": "npm:3.1.4" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.17" + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/d0b89e3974830d3893e7b8324a77ffeb9436db0969b57c01e2508ebd5b374c9d01f73796c8df8f555a3b1e1b502d40e725f159cd85966eebd3145b2f52e605e2 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:3.1.4, @vitest/pretty-format@npm:^3.1.4": + version: 3.1.4 + resolution: "@vitest/pretty-format@npm:3.1.4" + dependencies: + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/11e133640435822b8b8528be540b3d66c1de27ebc2dcf1de87608b7f01a44d15302c4d4bf8330fa848a435450d88a09d7e9442747a5739ae5f500ccdd1493159 + languageName: node + linkType: hard + +"@vitest/runner@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/runner@npm:3.1.4" + dependencies: + "@vitest/utils": "npm:3.1.4" + pathe: "npm:^2.0.3" + checksum: 10c0/efb7512eebd3d786baa617eab332ec9ca6ce62eb1c9dd3945019f7510d745b3cd0fc2978868d792050905aacbf158eefc132359c83e61f0398b46be566013ee6 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/snapshot@npm:3.1.4" + dependencies: + "@vitest/pretty-format": "npm:3.1.4" + magic-string: "npm:^0.30.17" + pathe: "npm:^2.0.3" + checksum: 10c0/ce9d51e1b03e4f91ffad160c570991a8a3c603cb7dc2a9020e58c012e62dccbe2c6ee45e1a1d8489e265b4485c6721eb73b5e91404d1c76da08dcd663f4e18d1 + languageName: node + linkType: hard + +"@vitest/spy@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/spy@npm:3.1.4" + dependencies: + tinyspy: "npm:^3.0.2" + checksum: 10c0/747914ac18efa82d75349b0fb0ad8a5e2af6e04f5bbb50a980c9270dd8958f9ddf84cee0849a54e1645af088fc1f709add94a35e99cb14aca2cdb322622ba501 + languageName: node + linkType: hard + +"@vitest/ui@npm:^3.1.4": + version: 3.1.4 + resolution: "@vitest/ui@npm:3.1.4" + dependencies: + "@vitest/utils": "npm:3.1.4" + fflate: "npm:^0.8.2" + flatted: "npm:^3.3.3" + pathe: "npm:^2.0.3" + sirv: "npm:^3.0.1" + tinyglobby: "npm:^0.2.13" + tinyrainbow: "npm:^2.0.0" + peerDependencies: + vitest: 3.1.4 + checksum: 10c0/02dd00e92f73aa0b71f69a374a7f991f16da13d3cec044f341b59e29209ad6197e2b9733b15f2a1b32ef77e1a9d5069eeb574035c3cea749ac2800df7ea23698 + languageName: node + linkType: hard + +"@vitest/utils@npm:3.1.4": + version: 3.1.4 + resolution: "@vitest/utils@npm:3.1.4" + dependencies: + "@vitest/pretty-format": "npm:3.1.4" + loupe: "npm:^3.1.3" + tinyrainbow: "npm:^2.0.0" + checksum: 10c0/78f1691a2dd578862b236f4962815e7475e547f006e7303a149dc5f910cc1ce6e0bdcbd7b4fd618122d62ca2dcc28bae464d31543f3898f5d88fa35017e00a95 + languageName: node + linkType: hard + "@wix/astro-components-demo@workspace:examples/astro-components-demo": version: 0.0.0-use.local resolution: "@wix/astro-components-demo@workspace:examples/astro-components-demo" @@ -3242,10 +3471,16 @@ __metadata: version: 0.0.0-use.local resolution: "@wix/headless-stores@workspace:packages/headless-components/stores" dependencies: + "@testing-library/dom": "npm:^10.4.0" + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/react": "npm:^16.3.0" "@types/node": "npm:^20.9.0" + "@vitest/ui": "npm:^3.1.4" "@wix/ecom": "npm:^1.0.1169" "@wix/redirects": "npm:^1.0.79" + jsdom: "npm:^26.1.0" typescript: "npm:^5.7.3" + vitest: "npm:^3.1.4" languageName: unknown linkType: soft @@ -3721,7 +3956,16 @@ __metadata: languageName: node linkType: hard -"aria-query@npm:^5.3.2": +"aria-query@npm:5.3.0": + version: 5.3.0 + resolution: "aria-query@npm:5.3.0" + dependencies: + dequal: "npm:^2.0.3" + checksum: 10c0/2bff0d4eba5852a9dd578ecf47eaef0e82cc52569b48469b0aac2db5145db0b17b7a58d9e01237706d1e14b7a1b0ac9b78e9c97027ad97679dd8f91b85da1469 + languageName: node + linkType: hard + +"aria-query@npm:^5.0.0, aria-query@npm:^5.3.2": version: 5.3.2 resolution: "aria-query@npm:5.3.2" checksum: 10c0/003c7e3e2cff5540bf7a7893775fc614de82b0c5dde8ae823d47b7a28a9d4da1f7ed85f340bdb93d5649caa927755f0e31ecc7ab63edfdfc00c8ef07e505e03e @@ -3749,6 +3993,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + "astro@npm:^5.5.4": version: 5.7.5 resolution: "astro@npm:5.7.5" @@ -3951,6 +4202,13 @@ __metadata: languageName: node linkType: hard +"cac@npm:^6.7.14": + version: 6.7.14 + resolution: "cac@npm:6.7.14" + checksum: 10c0/4ee06aaa7bab8981f0d54e5f5f9d4adcd64058e9697563ce336d8a3878ed018ee18ebe5359b2430eceae87e0758e62ea2019c3f52ae6e211b1bd2e133856cd10 + languageName: node + linkType: hard + "cacache@npm:^19.0.1": version: 19.0.1 resolution: "cacache@npm:19.0.1" @@ -4038,6 +4296,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.2.0": + version: 5.2.0 + resolution: "chai@npm:5.2.0" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10c0/dfd1cb719c7cebb051b727672d382a35338af1470065cb12adb01f4ee451bbf528e0e0f9ab2016af5fc1eea4df6e7f4504dc8443f8f00bd8fb87ad32dc516f7d + languageName: node + linkType: hard + "chalk-template@npm:^1.1.0": version: 1.1.0 resolution: "chalk-template@npm:1.1.0" @@ -4058,7 +4329,17 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0": +"chalk@npm:^3.0.0": + version: 3.0.0 + resolution: "chalk@npm:3.0.0" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/ee650b0a065b3d7a6fda258e75d3a86fc8e4effa55871da730a9e42ccb035bf5fd203525e5a1ef45ec2582ecc4f65b47eb11357c526b84dd29a14fb162c414d2 + languageName: node + linkType: hard + +"chalk@npm:^4.0.0, chalk@npm:^4.1.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -4125,6 +4406,13 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.1.1": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: 10c0/979f13eccab306cf1785fa10941a590b4e7ea9916ea2a4f8c87f0316fc3eab07eabefb6e587424ef0f88cbcd3805791f172ea739863ca3d7ce2afc54641c7f0e + languageName: node + linkType: hard + "chokidar@npm:^4.0.1, chokidar@npm:^4.0.3": version: 4.0.3 resolution: "chokidar@npm:4.0.3" @@ -4473,6 +4761,13 @@ __metadata: languageName: node linkType: hard +"css.escape@npm:^1.5.1": + version: 1.5.1 + resolution: "css.escape@npm:1.5.1" + checksum: 10c0/5e09035e5bf6c2c422b40c6df2eb1529657a17df37fda5d0433d722609527ab98090baf25b13970ca754079a0f3161dd3dfc0e743563ded8cfa0749d861c1525 + languageName: node + linkType: hard + "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -4482,6 +4777,16 @@ __metadata: languageName: node linkType: hard +"cssstyle@npm:^4.2.1": + version: 4.3.1 + resolution: "cssstyle@npm:4.3.1" + dependencies: + "@asamuzakjp/css-color": "npm:^3.1.2" + rrweb-cssom: "npm:^0.8.0" + checksum: 10c0/89d73252d5f9930cf67f5c576de8030a9d960aae4c8bdd42d60464b2f67c8d809601fb7e620b43d4c84e03472016da77528df9a21a21393387ed256610ca0ab4 + languageName: node + linkType: hard + "csstype@npm:^2.2.0": version: 2.6.21 resolution: "csstype@npm:2.6.21" @@ -4625,6 +4930,16 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^5.0.0": + version: 5.0.0 + resolution: "data-urls@npm:5.0.0" + dependencies: + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.0.0" + checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad + languageName: node + linkType: hard + "dataloader@npm:^2.2.2": version: 2.2.3 resolution: "dataloader@npm:2.2.3" @@ -4653,6 +4968,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.5.0": + version: 10.5.0 + resolution: "decimal.js@npm:10.5.0" + checksum: 10c0/785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3 + languageName: node + linkType: hard + "decode-named-character-reference@npm:^1.0.0": version: 1.1.0 resolution: "decode-named-character-reference@npm:1.1.0" @@ -4662,6 +4984,13 @@ __metadata: languageName: node linkType: hard +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247 + languageName: node + linkType: hard + "deep-equal@npm:^1.1.1": version: 1.1.2 resolution: "deep-equal@npm:1.1.2" @@ -4735,7 +5064,7 @@ __metadata: languageName: node linkType: hard -"dequal@npm:^2.0.0": +"dequal@npm:^2.0.0, dequal@npm:^2.0.3": version: 2.0.3 resolution: "dequal@npm:2.0.3" checksum: 10c0/f98860cdf58b64991ae10205137c0e97d384c3a4edc7f807603887b7c4b850af1224a33d88012009f150861cbee4fa2d322c4cc04b9313bee312e47f6ecaa888 @@ -4834,6 +5163,20 @@ __metadata: languageName: node linkType: hard +"dom-accessibility-api@npm:^0.5.9": + version: 0.5.16 + resolution: "dom-accessibility-api@npm:0.5.16" + checksum: 10c0/b2c2eda4fae568977cdac27a9f0c001edf4f95a6a6191dfa611e3721db2478d1badc01db5bb4fa8a848aeee13e442a6c2a4386d65ec65a1436f24715a2f8d053 + languageName: node + linkType: hard + +"dom-accessibility-api@npm:^0.6.3": + version: 0.6.3 + resolution: "dom-accessibility-api@npm:0.6.3" + checksum: 10c0/10bee5aa514b2a9a37c87cd81268db607a2e933a050074abc2f6fa3da9080ebed206a320cbc123567f2c3087d22292853bdfdceaffdd4334ffe2af9510b29360 + languageName: node + linkType: hard + "dom-align@npm:^1.7.0": version: 1.12.4 resolution: "dom-align@npm:1.12.4" @@ -5116,7 +5459,7 @@ __metadata: languageName: node linkType: hard -"es-module-lexer@npm:^1.6.0": +"es-module-lexer@npm:^1.6.0, es-module-lexer@npm:^1.7.0": version: 1.7.0 resolution: "es-module-lexer@npm:1.7.0" checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b @@ -5300,6 +5643,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.2.1": + version: 1.2.1 + resolution: "expect-type@npm:1.2.1" + checksum: 10c0/b775c9adab3c190dd0d398c722531726cdd6022849b4adba19dceab58dda7e000a7c6c872408cd73d665baa20d381eca36af4f7b393a4ba60dd10232d1fb8898 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.2 resolution: "exponential-backoff@npm:3.1.2" @@ -5405,6 +5755,13 @@ __metadata: languageName: node linkType: hard +"fflate@npm:^0.8.2": + version: 0.8.2 + resolution: "fflate@npm:0.8.2" + checksum: 10c0/03448d630c0a583abea594835a9fdb2aaf7d67787055a761515bf4ed862913cfd693b4c4ffd5c3f3b355a70cf1e19033e9ae5aedcca103188aaff91b8bd6e293 + languageName: node + linkType: hard + "fill-range@npm:^7.1.1": version: 7.1.1 resolution: "fill-range@npm:7.1.1" @@ -5414,6 +5771,13 @@ __metadata: languageName: node linkType: hard +"flatted@npm:^3.3.3": + version: 3.3.3 + resolution: "flatted@npm:3.3.3" + checksum: 10c0/e957a1c6b0254aa15b8cce8533e24165abd98fadc98575db082b786b5da1b7d72062b81bfdcd1da2f4d46b6ed93bec2434e62333e9b4261d79ef2e75a10dd538 + languageName: node + linkType: hard + "flattie@npm:^1.1.1": version: 1.1.1 resolution: "flattie@npm:1.1.1" @@ -6010,6 +6374,15 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^4.0.0": + version: 4.0.0 + resolution: "html-encoding-sniffer@npm:4.0.0" + dependencies: + whatwg-encoding: "npm:^3.1.1" + checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140 + languageName: node + linkType: hard + "html-entities@npm:^2.3.6": version: 2.6.0 resolution: "html-entities@npm:2.6.0" @@ -6065,7 +6438,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -6075,7 +6448,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1": +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -6101,7 +6474,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -6176,6 +6549,13 @@ __metadata: languageName: node linkType: hard +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + "inherits@npm:^2.0.3, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" @@ -6355,6 +6735,13 @@ __metadata: languageName: node linkType: hard +"is-potential-custom-element-name@npm:^1.0.1": + version: 1.0.1 + resolution: "is-potential-custom-element-name@npm:1.0.1" + checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9 + languageName: node + linkType: hard + "is-regex@npm:^1.1.4": version: 1.2.1 resolution: "is-regex@npm:1.2.1" @@ -6651,6 +7038,39 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:^26.1.0": + version: 26.1.0 + resolution: "jsdom@npm:26.1.0" + dependencies: + cssstyle: "npm:^4.2.1" + data-urls: "npm:^5.0.0" + decimal.js: "npm:^10.5.0" + html-encoding-sniffer: "npm:^4.0.0" + http-proxy-agent: "npm:^7.0.2" + https-proxy-agent: "npm:^7.0.6" + is-potential-custom-element-name: "npm:^1.0.1" + nwsapi: "npm:^2.2.16" + parse5: "npm:^7.2.1" + rrweb-cssom: "npm:^0.8.0" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^5.1.1" + w3c-xmlserializer: "npm:^5.0.0" + webidl-conversions: "npm:^7.0.0" + whatwg-encoding: "npm:^3.1.1" + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.1.1" + ws: "npm:^8.18.0" + xml-name-validator: "npm:^5.0.0" + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10c0/5b14a5bc32ce077a06fb42d1ab95b1191afa5cbbce8859e3b96831c5143becbbcbf0511d4d4934e922d2901443ced2cdc3b734c1cf30b5f73b3e067ce457d0f4 + languageName: node + linkType: hard + "jsesc@npm:^3.0.2": version: 3.1.0 resolution: "jsesc@npm:3.1.0" @@ -7065,6 +7485,13 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.0, loupe@npm:^3.1.3": + version: 3.1.3 + resolution: "loupe@npm:3.1.3" + checksum: 10c0/f5dab4144254677de83a35285be1b8aba58b3861439ce4ba65875d0d5f3445a4a496daef63100ccf02b2dbc25bf58c6db84c9cb0b96d6435331e9d0a33b48541 + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -7081,6 +7508,15 @@ __metadata: languageName: node linkType: hard +"lz-string@npm:^1.5.0": + version: 1.5.0 + resolution: "lz-string@npm:1.5.0" + bin: + lz-string: bin/bin.js + checksum: 10c0/36128e4de34791838abe979b19927c26e67201ca5acf00880377af7d765b38d1c60847e01c5ec61b1a260c48029084ab3893a3925fd6e48a04011364b089991b + languageName: node + linkType: hard + "magic-string@npm:^0.30.17": version: 0.30.17 resolution: "magic-string@npm:0.30.17" @@ -7736,6 +8172,13 @@ __metadata: languageName: node linkType: hard +"min-indent@npm:^1.0.0": + version: 1.0.1 + resolution: "min-indent@npm:1.0.1" + checksum: 10c0/7e207bd5c20401b292de291f02913230cb1163abca162044f7db1d951fa245b174dc00869d40dd9a9f32a885ad6a5f3e767ee104cf278f399cb4e92d3f582d5c + languageName: node + linkType: hard + "minimatch@npm:9.0.5, minimatch@npm:^9.0.4": version: 9.0.5 resolution: "minimatch@npm:9.0.5" @@ -7853,7 +8296,7 @@ __metadata: languageName: node linkType: hard -"mrmime@npm:^2.0.1": +"mrmime@npm:^2.0.0, mrmime@npm:^2.0.1": version: 2.0.1 resolution: "mrmime@npm:2.0.1" checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 @@ -8027,6 +8470,13 @@ __metadata: languageName: node linkType: hard +"nwsapi@npm:^2.2.16": + version: 2.2.20 + resolution: "nwsapi@npm:2.2.20" + checksum: 10c0/07f4dafa3186aef7c007863e90acd4342a34ba9d44b22f14f644fdb311f6086887e21c2fc15efaa826c2bc39ab2bc841364a1a630e7c87e0cb723ba59d729297 + languageName: node + linkType: hard + "oauth4webapi@npm:^3.4.0": version: 3.5.0 resolution: "oauth4webapi@npm:3.5.0" @@ -8282,7 +8732,7 @@ __metadata: languageName: node linkType: hard -"parse5@npm:^7.0.0": +"parse5@npm:^7.0.0, parse5@npm:^7.2.1": version: 7.3.0 resolution: "parse5@npm:7.3.0" dependencies: @@ -8338,7 +8788,21 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": +"pathe@npm:^2.0.3": + version: 2.0.3 + resolution: "pathe@npm:2.0.3" + checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1 + languageName: node + linkType: hard + +"pathval@npm:^2.0.0": + version: 2.0.0 + resolution: "pathval@npm:2.0.0" + checksum: 10c0/602e4ee347fba8a599115af2ccd8179836a63c925c23e04bd056d0674a64b39e3a081b643cc7bc0b84390517df2d800a46fcc5598d42c155fe4977095c2f77c5 + languageName: node + linkType: hard + +"picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 @@ -8422,6 +8886,17 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:^27.0.2": + version: 27.5.1 + resolution: "pretty-format@npm:27.5.1" + dependencies: + ansi-regex: "npm:^5.0.1" + ansi-styles: "npm:^5.0.0" + react-is: "npm:^17.0.1" + checksum: 10c0/0cbda1031aa30c659e10921fa94e0dd3f903ecbbbe7184a729ad66f2b6e7f17891e8c7d7654c458fa4ccb1a411ffb695b4f17bbcd3fe075fabe181027c4040ed + languageName: node + linkType: hard + "pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" @@ -8806,6 +9281,13 @@ __metadata: languageName: node linkType: hard +"punycode@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 + languageName: node + linkType: hard + "pure-rand@npm:^6.1.0": version: 6.1.0 resolution: "pure-rand@npm:6.1.0" @@ -9123,6 +9605,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^17.0.1": + version: 17.0.2 + resolution: "react-is@npm:17.0.2" + checksum: 10c0/2bdb6b93fbb1820b024b496042cce405c57e2f85e777c9aabd55f9b26d145408f9f74f5934676ffdc46f3dcff656d78413a6e43968e7b3f92eea35b3052e9053 + languageName: node + linkType: hard + "react-is@npm:^18.0.0, react-is@npm:^18.2.0": version: 18.3.1 resolution: "react-is@npm:18.3.1" @@ -9396,6 +9885,16 @@ __metadata: languageName: node linkType: hard +"redent@npm:^3.0.0": + version: 3.0.0 + resolution: "redent@npm:3.0.0" + dependencies: + indent-string: "npm:^4.0.0" + strip-indent: "npm:^3.0.0" + checksum: 10c0/d64a6b5c0b50eb3ddce3ab770f866658a2b9998c678f797919ceb1b586bab9259b311407280bd80b804e2a7c7539b19238ae6a2a20c843f1a7fcff21d48c2eae + languageName: node + linkType: hard + "redux@npm:^4.0.4": version: 4.2.1 resolution: "redux@npm:4.2.1" @@ -9770,6 +10269,13 @@ __metadata: languageName: node linkType: hard +"rrweb-cssom@npm:^0.8.0": + version: 0.8.0 + resolution: "rrweb-cssom@npm:0.8.0" + checksum: 10c0/56f2bfd56733adb92c0b56e274c43f864b8dd48784d6fe946ef5ff8d438234015e59ad837fc2ad54714b6421384141c1add4eb569e72054e350d1f8a50b8ac7b + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -9807,6 +10313,15 @@ __metadata: languageName: node linkType: hard +"saxes@npm:^6.0.0": + version: 6.0.0 + resolution: "saxes@npm:6.0.0" + dependencies: + xmlchars: "npm:^2.2.0" + checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74 + languageName: node + linkType: hard + "scheduler@npm:^0.23.2": version: 0.23.2 resolution: "scheduler@npm:0.23.2" @@ -9982,6 +10497,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -10013,6 +10535,17 @@ __metadata: languageName: node linkType: hard +"sirv@npm:^3.0.1": + version: 3.0.1 + resolution: "sirv@npm:3.0.1" + dependencies: + "@polka/url": "npm:^1.0.0-next.24" + mrmime: "npm:^2.0.0" + totalist: "npm:^3.0.0" + checksum: 10c0/7cf64b28daa69b15f77b38b0efdd02c007b72bb3ec5f107b208ebf59f01b174ef63a1db3aca16d2df925501831f4c209be6ece3302b98765919ef5088b45bf80 + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -10119,6 +10652,20 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + +"std-env@npm:^3.9.0": + version: 3.9.0 + resolution: "std-env@npm:3.9.0" + checksum: 10c0/4a6f9218aef3f41046c3c7ecf1f98df00b30a07f4f35c6d47b28329bc2531eef820828951c7d7b39a1c5eb19ad8a46e3ddfc7deb28f0a2f3ceebee11bab7ba50 + languageName: node + linkType: hard + "stdin-discarder@npm:^0.2.2": version: 0.2.2 resolution: "stdin-discarder@npm:0.2.2" @@ -10231,6 +10778,15 @@ __metadata: languageName: node linkType: hard +"strip-indent@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-indent@npm:3.0.0" + dependencies: + min-indent: "npm:^1.0.0" + checksum: 10c0/ae0deaf41c8d1001c5d4fbe16cb553865c1863da4fae036683b474fa926af9fc121e155cb3fc57a68262b2ae7d5b8420aa752c97a6428c315d00efe2a3875679 + languageName: node + linkType: hard + "strip-json-comments@npm:5.0.1": version: 5.0.1 resolution: "strip-json-comments@npm:5.0.1" @@ -10289,6 +10845,13 @@ __metadata: languageName: node linkType: hard +"symbol-tree@npm:^3.2.4": + version: 3.2.4 + resolution: "symbol-tree@npm:3.2.4" + checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509 + languageName: node + linkType: hard + "syncpack@npm:^13.0.0": version: 13.0.3 resolution: "syncpack@npm:13.0.3" @@ -10389,6 +10952,13 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + "tinyexec@npm:^0.3.2": version: 0.3.2 resolution: "tinyexec@npm:0.3.2" @@ -10406,6 +10976,27 @@ __metadata: languageName: node linkType: hard +"tinypool@npm:^1.0.2": + version: 1.0.2 + resolution: "tinypool@npm:1.0.2" + checksum: 10c0/31ac184c0ff1cf9a074741254fe9ea6de95026749eb2b8ec6fd2b9d8ca94abdccda731f8e102e7f32e72ed3b36d32c6975fd5f5523df3f1b6de6c3d8dfd95e63 + languageName: node + linkType: hard + +"tinyrainbow@npm:^2.0.0": + version: 2.0.0 + resolution: "tinyrainbow@npm:2.0.0" + checksum: 10c0/c83c52bef4e0ae7fb8ec6a722f70b5b6fa8d8be1c85792e829f56c0e1be94ab70b293c032dc5048d4d37cfe678f1f5babb04bdc65fd123098800148ca989184f + languageName: node + linkType: hard + +"tinyspy@npm:^3.0.2": + version: 3.0.2 + resolution: "tinyspy@npm:3.0.2" + checksum: 10c0/55ffad24e346622b59292e097c2ee30a63919d5acb7ceca87fc0d1c223090089890587b426e20054733f97a58f20af2c349fb7cc193697203868ab7ba00bcea0 + languageName: node + linkType: hard + "tippy.js@npm:^6.3.1, tippy.js@npm:^6.3.7": version: 6.3.7 resolution: "tippy.js@npm:6.3.7" @@ -10415,6 +11006,24 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^6.1.86": + version: 6.1.86 + resolution: "tldts-core@npm:6.1.86" + checksum: 10c0/8133c29375f3f99f88fce5f4d62f6ecb9532b106f31e5423b27c1eb1b6e711bd41875184a456819ceaed5c8b94f43911b1ad57e25c6eb86e1fc201228ff7e2af + languageName: node + linkType: hard + +"tldts@npm:^6.1.32": + version: 6.1.86 + resolution: "tldts@npm:6.1.86" + dependencies: + tldts-core: "npm:^6.1.86" + bin: + tldts: bin/cli.js + checksum: 10c0/27ae7526d9d78cb97b2de3f4d102e0b4321d1ccff0648a7bb0e039ed54acbce86bacdcd9cd3c14310e519b457854e7bafbef1f529f58a1e217a737ced63f0940 + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -10431,6 +11040,31 @@ __metadata: languageName: node linkType: hard +"totalist@npm:^3.0.0": + version: 3.0.1 + resolution: "totalist@npm:3.0.1" + checksum: 10c0/4bb1fadb69c3edbef91c73ebef9d25b33bbf69afe1e37ce544d5f7d13854cda15e47132f3e0dc4cafe300ddb8578c77c50a65004d8b6e97e77934a69aa924863 + languageName: node + linkType: hard + +"tough-cookie@npm:^5.1.1": + version: 5.1.2 + resolution: "tough-cookie@npm:5.1.2" + dependencies: + tldts: "npm:^6.1.32" + checksum: 10c0/5f95023a47de0f30a902bba951664b359725597d8adeabc66a0b93a931c3af801e1e697dae4b8c21a012056c0ea88bd2bf4dfe66b2adcf8e2f42cd9796fe0626 + languageName: node + linkType: hard + +"tr46@npm:^5.1.0": + version: 5.1.1 + resolution: "tr46@npm:5.1.1" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10c0/ae270e194d52ec67ebd695c1a42876e0f19b96e4aca2ab464ab1d9d17dc3acd3e18764f5034c93897db73421563be27c70c98359c4501136a497e46deda5d5ec + languageName: node + linkType: hard + "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -11002,9 +11636,24 @@ __metadata: languageName: node linkType: hard -"vite@npm:^6.0.0, vite@npm:^6.2.6": - version: 6.3.3 - resolution: "vite@npm:6.3.3" +"vite-node@npm:3.1.4": + version: 3.1.4 + resolution: "vite-node@npm:3.1.4" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.4.0" + es-module-lexer: "npm:^1.7.0" + pathe: "npm:^2.0.3" + vite: "npm:^5.0.0 || ^6.0.0" + bin: + vite-node: vite-node.mjs + checksum: 10c0/2fc71ddadd308b19b0d0dc09f5b9a108ea9bb640ec5fbd6179267994da8fd6c9d6a4c92098af7de73a0fa817055b518b28972452a2f19a1be754e79947e289d2 + languageName: node + linkType: hard + +"vite@npm:^5.0.0 || ^6.0.0, vite@npm:^6.0.0, vite@npm:^6.2.6": + version: 6.3.5 + resolution: "vite@npm:6.3.5" dependencies: esbuild: "npm:^0.25.0" fdir: "npm:^6.4.4" @@ -11053,7 +11702,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/7ea27d2c80a9e0b7ccf6cbd6c251455501286568160e8b632984e5332440f21a6d05f9236408212ba7653f7d2d4790f848956d8a620bbf4dd2ecb792a2fe1ab1 + checksum: 10c0/df70201659085133abffc6b88dcdb8a57ef35f742a01311fc56a4cfcda6a404202860729cc65a2c401a724f6e25f9ab40ce4339ed4946f550541531ced6fe41c languageName: node linkType: hard @@ -11069,6 +11718,60 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^3.1.4": + version: 3.1.4 + resolution: "vitest@npm:3.1.4" + dependencies: + "@vitest/expect": "npm:3.1.4" + "@vitest/mocker": "npm:3.1.4" + "@vitest/pretty-format": "npm:^3.1.4" + "@vitest/runner": "npm:3.1.4" + "@vitest/snapshot": "npm:3.1.4" + "@vitest/spy": "npm:3.1.4" + "@vitest/utils": "npm:3.1.4" + chai: "npm:^5.2.0" + debug: "npm:^4.4.0" + expect-type: "npm:^1.2.1" + magic-string: "npm:^0.30.17" + pathe: "npm:^2.0.3" + std-env: "npm:^3.9.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^0.3.2" + tinyglobby: "npm:^0.2.13" + tinypool: "npm:^1.0.2" + tinyrainbow: "npm:^2.0.0" + vite: "npm:^5.0.0 || ^6.0.0" + vite-node: "npm:3.1.4" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/debug": ^4.1.12 + "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + "@vitest/browser": 3.1.4 + "@vitest/ui": 3.1.4 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/debug": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10c0/aec575e3cc6cf9b3cee224ae63569479e3a41fa980e495a73d384e31e273f34b18317a0da23bbd577c60fe5e717fa41cdc390de4049ce224ffdaa266ea0cdc67 + languageName: node + linkType: hard + "void-elements@npm:3.1.0": version: 3.1.0 resolution: "void-elements@npm:3.1.0" @@ -11083,6 +11786,15 @@ __metadata: languageName: node linkType: hard +"w3c-xmlserializer@npm:^5.0.0": + version: 5.0.0 + resolution: "w3c-xmlserializer@npm:5.0.0" + dependencies: + xml-name-validator: "npm:^5.0.0" + checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b + languageName: node + linkType: hard + "walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -11140,6 +11852,39 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^7.0.0": + version: 7.0.0 + resolution: "webidl-conversions@npm:7.0.0" + checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4 + languageName: node + linkType: hard + +"whatwg-encoding@npm:^3.1.1": + version: 3.1.1 + resolution: "whatwg-encoding@npm:3.1.1" + dependencies: + iconv-lite: "npm:0.6.3" + checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e + languageName: node + linkType: hard + +"whatwg-mimetype@npm:^4.0.0": + version: 4.0.0 + resolution: "whatwg-mimetype@npm:4.0.0" + checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df + languageName: node + linkType: hard + +"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.1": + version: 14.2.0 + resolution: "whatwg-url@npm:14.2.0" + dependencies: + tr46: "npm:^5.1.0" + webidl-conversions: "npm:^7.0.0" + checksum: 10c0/f746fc2f4c906607d09537de1227b13f9494c171141e5427ed7d2c0dd0b6a48b43d8e71abaae57d368d0c06b673fd8ec63550b32ad5ed64990c7b0266c2b4272 + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" @@ -11179,6 +11924,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + "widest-line@npm:^5.0.0": version: 5.0.0 resolution: "widest-line@npm:5.0.0" @@ -11230,6 +11987,35 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.18.0": + version: 8.18.2 + resolution: "ws@npm:8.18.2" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/4b50f67931b8c6943c893f59c524f0e4905bbd183016cfb0f2b8653aa7f28dad4e456b9d99d285bbb67cca4fedd9ce90dfdfaa82b898a11414ebd66ee99141e4 + languageName: node + linkType: hard + +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5 + languageName: node + linkType: hard + +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593 + languageName: node + linkType: hard + "xxhash-wasm@npm:^1.1.0": version: 1.1.0 resolution: "xxhash-wasm@npm:1.1.0" From e35bb474294a5c34cc26a8f8cefc1600cfbd61c1 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Tue, 27 May 2025 11:38:06 +0300 Subject: [PATCH 06/42] Add jsdoc --- .../stores/src/react/BuyNow.tsx | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/headless-components/stores/src/react/BuyNow.tsx b/packages/headless-components/stores/src/react/BuyNow.tsx index 9c0c0a1df..b3499957a 100644 --- a/packages/headless-components/stores/src/react/BuyNow.tsx +++ b/packages/headless-components/stores/src/react/BuyNow.tsx @@ -5,12 +5,71 @@ import { useState } from "react"; // const CATALOG_APP_ID = "1380b703-ce81-ff05-f115-39571d94dfcd"; const CATLOG_APP_ID_V3 = "215238eb-22a5-4c36-9e7b-e7c08025e04e"; +/** + * @typedef {object} BuyNowRenderProps + * @property {boolean} isLoading - Indicates if the checkout process is currently in progress. + * @property {() => Promise} redirectToCheckout - A function to initiate the checkout process. + * When called, it creates a checkout for the specified product + * and then redirects the user to the Wix checkout page. + * It handles setting the loading state internally. + */ + +/** + * @typedef {object} BuyNowProps + * @property {string} productId - The unique identifier of the product to be purchased. + * @property {Record} [variant] - An optional record of product variants (e.g., color, size). + * The keys are variant identifiers and values are the selected options. + * @property {(props: BuyNowRenderProps) => React.ReactNode} children - A render prop function that receives the loading state + * and the checkout initiation function. + * This allows for custom rendering of the UI. + */ + +/** + * The `BuyNow` component provides a mechanism to initiate a direct purchase for a single product. + * It encapsulates the logic for creating an e-commerce checkout and redirecting the user to the + * Wix checkout page. + * + * This component uses a render prop pattern (`children`) to provide flexibility in how the UI + * (e.g., a buy button, loading indicator) is rendered. The render prop receives `isLoading` state + * and a `redirectToCheckout` function. + * + * The `redirectToCheckout` function is asynchronous. It first sets `isLoading` to true, then calls + * the Wix Ecom SDK to create a checkout with the specified `productId` and `variant`. + * If successful, it uses the Wix Redirects SDK to generate a redirect session and then navigates + * the user to the checkout URL by setting `window.location.href`. + * The `isLoading` state is set to false when the process completes (either successfully or on error). + * + * @param {BuyNowProps} props - The props for the BuyNow component. + * @returns {React.ReactNode} The output of the children render prop. + * + * @example + * ```tsx + * import { BuyNow } from '@wix/headless-stores/react'; + * + * const MyProductPage = ({ productId, productVariant }) => { + * return ( + * + * {({ isLoading, redirectToCheckout }) => { + * if (isLoading) { + * return

Preparing your order...

; + * } + * return ( + * + * ); + * }} + *
+ * ); + * }; + * ``` + */ export function BuyNow(props: { productId: string; variant?: Record; children: (props: { isLoading: boolean; - redirectToCheckout: () => void; + redirectToCheckout: () => void; // Note: internally it's async, JSDoc reflects the exposed type }) => React.ReactNode; }) { const [isLoading, setIsLoading] = useState(false); From 3aac14e5578352c18390c131dee316333bd41b70 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Tue, 27 May 2025 11:43:55 +0300 Subject: [PATCH 07/42] Adjust build --- .../stores/src/react/BuyNow.test.tsx | 16 +++++++--------- .../headless-components/stores/tsconfig.json | 11 ++++++++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/headless-components/stores/src/react/BuyNow.test.tsx b/packages/headless-components/stores/src/react/BuyNow.test.tsx index aa40153cd..d815be104 100644 --- a/packages/headless-components/stores/src/react/BuyNow.test.tsx +++ b/packages/headless-components/stores/src/react/BuyNow.test.tsx @@ -1,19 +1,18 @@ -import React from 'react'; import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import '@testing-library/jest-dom/vitest'; import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; -import { BuyNow } from './BuyNow'; // This is the component under test +import { BuyNow } from './BuyNow'; vi.mock('@wix/ecom', () => ({ checkout: { - createCheckout: vi.fn(), // Define vi.fn() directly here + createCheckout: vi.fn(), ChannelType: { WEB: 'WEB' }, }, })); vi.mock('@wix/redirects', () => ({ redirects: { - createRedirectSession: vi.fn(), // Define vi.fn() directly here + createRedirectSession: vi.fn(), }, })); @@ -22,18 +21,16 @@ let ecomCheckoutMock: any; let redirectsMock: any; beforeEach(async () => { - // Dynamically import the mocked modules to get the mock functions const ecom = await import('@wix/ecom'); ecomCheckoutMock = ecom.checkout; const redirectsModule = await import('@wix/redirects'); redirectsMock = redirectsModule.redirects; - vi.clearAllMocks(); // This will clear all vi.fn() instances, including those above + vi.clearAllMocks(); delete (window as any).location; (window as any).location = { ...originalLocation, href: '' }; - // Default successful responses ecomCheckoutMock.createCheckout.mockResolvedValue({ _id: 'test-checkout-id' }); redirectsMock.createRedirectSession.mockResolvedValue({ redirectSession: { fullUrl: 'http://mocked-redirect-url.com' }, @@ -41,14 +38,13 @@ beforeEach(async () => { }); afterEach(() => { - window.location = originalLocation; + (window as any).location = originalLocation; // Use type assertion for restoration }); describe('BuyNow Component from @wix/headless-stores/react', () => { const testProductId = 'test-product-123'; const testVariant = { color: 'blue' }; - // Updated to capture the redirectToCheckout function for direct invocation in rejection tests const renderComponent = (props = {}) => { let capturedRedirectToCheckout: () => Promise = async () => {}; const renderOutput = render( @@ -117,6 +113,7 @@ describe('BuyNow Component from @wix/headless-stores/react', () => { ecomCheckoutMock.createCheckout.mockResolvedValueOnce({ _id: null }); const { redirectToCheckoutDirectly } = renderComponent(); + await act(async () => { await expect(redirectToCheckoutDirectly()).rejects.toThrow('Failed to create checkout'); }); @@ -130,6 +127,7 @@ describe('BuyNow Component from @wix/headless-stores/react', () => { redirectsMock.createRedirectSession.mockRejectedValueOnce(new Error('Redirect failed')); const { redirectToCheckoutDirectly } = renderComponent(); + await act(async () => { await expect(redirectToCheckoutDirectly()).rejects.toThrow('Redirect failed'); }); diff --git a/packages/headless-components/stores/tsconfig.json b/packages/headless-components/stores/tsconfig.json index b739e69b9..77423930d 100644 --- a/packages/headless-components/stores/tsconfig.json +++ b/packages/headless-components/stores/tsconfig.json @@ -21,5 +21,14 @@ "noEmitOnError": false, "jsx": "react-jsx" }, - "include": ["src/**/*"] + "include": ["src/**/*"], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.spec.ts", + "**/*.spec.tsx", + "src/vitest.setup.ts" + ] } From 2465fdc96f303537234fdb8468344f72b2295736 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Wed, 28 May 2025 14:55:56 +0300 Subject: [PATCH 08/42] Headless hacks --- examples/astro-components-demo/.env.local.bak | 6 + .../astro-components-demo/astro.config.mjs | 14 +- examples/astro-components-demo/package.json | 11 +- .../src/components/ui/buy-now.tsx | 31 +- .../src/pages/index.astro | 5 +- packages/@wix-astro/package.json | 2 +- .../src/client-scripts/redirect-session.ts | 47 +++ .../headless-components/core/package.json | 7 + .../headless-components/core/src/context.tsx | 97 +++++ .../headless-components/core/src/directive.ts | 17 + .../headless-components/core/src/index.ts | 5 +- .../headless-components/stores/package.json | 11 +- .../stores/src/astro/BuyNowService.astro | 40 ++ .../stores/src/astro/BuyNowServiceContext.ts | 10 + .../stores/src/astro/withBuyButtonService.tsx | 20 + .../stores/src/react/BuyNow.tsx | 3 +- .../stores/src/react/hookim/index.ts | 24 ++ .../stores/src/services/index.ts | 34 ++ .../stores/src/utils/index.ts | 33 ++ yarn.lock | 396 +++++++++++++++++- 20 files changed, 774 insertions(+), 39 deletions(-) create mode 100644 examples/astro-components-demo/.env.local.bak create mode 100644 packages/@wix-astro/src/client-scripts/redirect-session.ts create mode 100644 packages/headless-components/core/src/context.tsx create mode 100644 packages/headless-components/core/src/directive.ts create mode 100644 packages/headless-components/stores/src/astro/BuyNowService.astro create mode 100644 packages/headless-components/stores/src/astro/BuyNowServiceContext.ts create mode 100644 packages/headless-components/stores/src/astro/withBuyButtonService.tsx create mode 100644 packages/headless-components/stores/src/react/hookim/index.ts create mode 100644 packages/headless-components/stores/src/services/index.ts create mode 100644 packages/headless-components/stores/src/utils/index.ts diff --git a/examples/astro-components-demo/.env.local.bak b/examples/astro-components-demo/.env.local.bak new file mode 100644 index 000000000..7cc3fe9a6 --- /dev/null +++ b/examples/astro-components-demo/.env.local.bak @@ -0,0 +1,6 @@ +ENV_NAME="production" +WIX_CLIENT_ID="361476f1-b0fa-40b8-8b24-69235c9d492b" +WIX_CLIENT_SECRET="3d21f749-4155-4374-b33a-665a49efc766" +WIX_CLIENT_INSTANCE_ID="f1b88319-8333-4e76-819c-7a4ba0409a1f" +WIX_CLIENT_PUBLIC_KEY="LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxSlBmMkt4WWNKQ0poeXEyMUpRYQpFZTNJWG1hcXo2MnFLVS9rK1QzN3F6VlAyaTRVT1VZMjdQc3Z5ZW9EOGc3YmhpWHNKbjlZYmo1K2o2NWVWVGhsCnAwRFI1c01ueHhOM2pVbENHQmdkY2NwVEZvcWFFbmJYY1VSaklVbjliaFJjUHRHOWNzTHlPM3g3V0hIS3V0a1kKRXpzQnNnVFZTSUIyQ0RSdXRRU3pPRlBZQzloL2k1cHNERkI2dE9UYXptLy96QjVFMjJIZ2hrSTA0TmEwSEt4UApGV082Z3BnT29helRFL2M4Qkt4OEphV25uRWRGeEcwNlJIZ2s5alhrc0ZoZm4zMEVvQUdBeVJJazU5blhzZHZkCk83aEU5dFVuaFJES3hKNzAwcGhNVStOaC9xdEkzcXBIbitobDlvU1A3R2F5VFY5VHVMTFZEQmkxZUM0ZUdFS20KaHdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t" +WIX_DEPLOYMENT_DOMAIN="hanut-shel-yury-xnw3ekus-yurym4.wix-sites.dev" diff --git a/examples/astro-components-demo/astro.config.mjs b/examples/astro-components-demo/astro.config.mjs index e2321a92f..20a1acfb1 100644 --- a/examples/astro-components-demo/astro.config.mjs +++ b/examples/astro-components-demo/astro.config.mjs @@ -4,8 +4,20 @@ import { defineConfig } from "astro/config"; import react from "@astrojs/react"; +const integration = { + name: "kaki-integration", + hooks: { + "astro:config:setup": ({ addClientDirective }) => { + addClientDirective({ + name: "context-provider", + entrypoint: "@wix/headless-components-core/directive" + }); + }, + }, +}; + // https://astro.build/config export default defineConfig({ - integrations: [react()], + integrations: [react(), integration], adapter: wix(), }); diff --git a/examples/astro-components-demo/package.json b/examples/astro-components-demo/package.json index 7925a00cf..5a1ef2e5f 100644 --- a/examples/astro-components-demo/package.json +++ b/examples/astro-components-demo/package.json @@ -13,15 +13,18 @@ "deploy:prod": "wix edge deploy --prod --no-prod-confirm" }, "dependencies": { - "@astrojs/react": "^4.2.1", + "@astrojs/react": "^4.3.0", "@playwright/test": "^1.51.1", + "@types/react": "^19.1.5", + "@types/react-dom": "^19.1.5", "@wix/astro": "workspace:*", "@wix/headless-bookings": "workspace:*", "@wix/headless-ecom": "workspace:*", "@wix/headless-stores": "workspace:*", - "astro": "^5.5.4", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "@wix/services-manager-react": "^0.1.9", + "astro": "^5.8.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" }, "devDependencies": { "@wix/cli-edge": "^1.1.74" diff --git a/examples/astro-components-demo/src/components/ui/buy-now.tsx b/examples/astro-components-demo/src/components/ui/buy-now.tsx index cfc119334..233668fe2 100644 --- a/examples/astro-components-demo/src/components/ui/buy-now.tsx +++ b/examples/astro-components-demo/src/components/ui/buy-now.tsx @@ -1,15 +1,16 @@ -import { BuyNow as BuyNowPrimitive } from "@wix/headless-stores/react"; - -export function BuyNow(props: Omit, "children">) { - return - {({ isLoading, redirectToCheckout }) => { - if (isLoading) return <>Preparing checkout...; - - return ( - - ); - }} - -} + +import type { Signal } from "@wix/services-definitions"; +import { withBuyButtonService } from "@wix/headless-stores/astro/BuyNowServiceContext"; + +export const BuyNow = withBuyButtonService(({ context }) => { + console.log("context", context); + const { loading, error, redirectToCheckout } = context; + + if ((loading as Signal).get()) return <>Preparing checkout...; + if ((error as Signal).get()) return <>Error: {(error as Signal).get()}; + + return +}); + diff --git a/examples/astro-components-demo/src/pages/index.astro b/examples/astro-components-demo/src/pages/index.astro index 9e533bb37..ee2fcfd2c 100644 --- a/examples/astro-components-demo/src/pages/index.astro +++ b/examples/astro-components-demo/src/pages/index.astro @@ -3,10 +3,13 @@ import React from "react"; import App from "../components/App.jsx"; import Layout from "../layouts/Layout.astro"; import { BuyNow } from "../components/ui/buy-now"; +import BuyNowService from "@wix/headless-stores/astro/BuyNowService.astro"; --- - + + + diff --git a/packages/@wix-astro/package.json b/packages/@wix-astro/package.json index 64579eb23..1c5fc007e 100644 --- a/packages/@wix-astro/package.json +++ b/packages/@wix-astro/package.json @@ -33,7 +33,7 @@ "@wix/blog": "^1.0.345", "@wix/data": "^1.0.194", "@wix/identity": "^1.0.125", - "@wix/sdk": "^1.15.20", + "@wix/sdk": "^1.15.22", "chalk": "^5.4.1", "esm-resolve": "^1.0.11", "globby": "^14.0.2", diff --git a/packages/@wix-astro/src/client-scripts/redirect-session.ts b/packages/@wix-astro/src/client-scripts/redirect-session.ts new file mode 100644 index 000000000..67d355901 --- /dev/null +++ b/packages/@wix-astro/src/client-scripts/redirect-session.ts @@ -0,0 +1,47 @@ +import { redirects } from '@wix/redirects'; + +// Function to check if we need to pre-warm +function checkAndExecutePreWarm(): void { + // Check if we already pre-warmed recently + const lastPreWarmTimeString = localStorage.getItem('wixRedirectSessionLastPreWarm'); + const currentTime = Date.now(); + + // If we have a stored timestamp, check if it's been less than a week + if (lastPreWarmTimeString) { + const lastPreWarmTime = parseInt(lastPreWarmTimeString, 10); + const oneWeekMs = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds + + if (currentTime - lastPreWarmTime < oneWeekMs) { + console.log('Skipping redirect session pre-warm - already done within the past week'); + return; // Skip pre-warming if done in the last week + } + } + + preWarmRedirectSession(); + + // Store the current timestamp + localStorage.setItem('wixRedirectSessionLastPreWarm', currentTime.toString()); +} + +async function preWarmRedirectSession() { + const result = await redirects.createRedirectSession({ + ecomCheckout: { + checkoutId: "b190278d-5096-4283-9030-476bfe3cca63" + } + }); + + const urlToRedirect = result.redirectSession?.fullUrl; + + if (!urlToRedirect) { + console.error('No redirect URL found'); + return; + } + + const iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.src = urlToRedirect; + document.body.appendChild(iframe); +} + +// Run the check function instead of calling preWarmRedirectSession directly +checkAndExecutePreWarm(); diff --git a/packages/headless-components/core/package.json b/packages/headless-components/core/package.json index 5c070c2b8..8fbb6956e 100644 --- a/packages/headless-components/core/package.json +++ b/packages/headless-components/core/package.json @@ -4,8 +4,15 @@ "scripts": { "build": "tsc" }, + "exports": { + ".": "./dist/index.js", + "./directive": "./dist/directive.js" + }, "devDependencies": { "@types/node": "^20.9.0", "typescript": "^5.7.3" + }, + "dependencies": { + "@preact/signals-react": "^3.1.1" } } diff --git a/packages/headless-components/core/src/context.tsx b/packages/headless-components/core/src/context.tsx new file mode 100644 index 000000000..efbdb9cd3 --- /dev/null +++ b/packages/headless-components/core/src/context.tsx @@ -0,0 +1,97 @@ +import { useSignals } from "@preact/signals-react/runtime"; +import { useMemo } from "react" + +/** + * Creates a context that can be used in Astro components. + * This function uses generics to allow any type `T` to be used as the context value. + * + * @returns A tuple containing the Provider component and a getter function for the current context. + */ +let counter = 0; +export const createContext = () => { + // initialize context casted to type `T`. + let context: T + const contextId = ++counter; + console.log('ze create context', contextId, new Error().stack); + /** + * Provider component for the context. + * This component sets the context value and provides it to its children components. + */ + function Provider( + /** Result parameter (not used in this implementation). */ + _result: any, + /** Props passed to the Provider, used as the new context value. */ + props: any, + /** Slot(s) containing the children components. */ + slots: any + ) { + // overwrite props with a deep clone itself + // avoid unintended side-effects caused by shared references + //props = structuredClone(props) + + console.log(`Provider::props ${contextId}`, props); + return { + /* Symbol indicating this is an Astro component object. */ + [Symbol.toStringTag]: 'AstroComponent', + async *[Symbol.asyncIterator]() { + // set context to the value provided by props + // ensure a deep clone of the provided value + // avoid unintended side-effects caused by shared references + context = props + console.log(`Provider::props set ${contextId}`, props); + + // yield rendered children components + yield await slots.default() + + // reset context to undefined after rendering is complete + context = undefined as T + console.log(`Provider::props reset ${contextId}`, context); + }, + } + } + + /* Flag indicating this is an Astro component factory function. */ + Provider.isAstroComponentFactory = true + + // return a tuple of Provider component and a getter function for the current context + return [ + Provider, + () => { + console.log(`Provider::context return ${contextId}`, context); + return context; + }, + ] as any as [ + /** Provider component for context. */ + (props: T) => any, + /** Returns the current context. */ + () => T + ] +} + +export function withContext( + getContext: () => T, + Component: React.ComponentType

+) { + return (props: P) => { + const { contextId, ...rest } = props as any; + const returnedContext = useMemo(() => { + // @ts-expect-error + if (import.meta.env.SSR) { + console.log("withContext::ssr"); + // @ts-expect-error + const store = getContext().service; + console.log("withContext::store", store); + return store; + } else { + console.log("withContext::client", (globalThis as any).contexts); + return (globalThis as any).contexts[contextId]; + } + }, []); + + // @ts-expect-error + if (!import.meta.env.SSR) { + useSignals(); + } + return ; + }; +} diff --git a/packages/headless-components/core/src/directive.ts b/packages/headless-components/core/src/directive.ts new file mode 100644 index 000000000..47c9a2954 --- /dev/null +++ b/packages/headless-components/core/src/directive.ts @@ -0,0 +1,17 @@ +import type { ClientDirective } from "astro"; + +export default (async (load, _, el) => { + await new Promise(resolve => setTimeout(resolve, 5000)); + + const service = (el.closest("context-provider") as any).service; + console.log("context-provider service", service); + const contextId = crypto.randomUUID(); + (globalThis as any).contexts = (globalThis as any).contexts || {}; + (globalThis as any).contexts[contextId] = service; + const props = JSON.parse(el.getAttribute("props") || "{}"); + console.log("context-provider Props from wait-for-store:", props); + props["contextId"] = [0, contextId]; + el.setAttribute("props", JSON.stringify(props)); + const [hydrate] = await Promise.all([load()]); + await hydrate(); +}); diff --git a/packages/headless-components/core/src/index.ts b/packages/headless-components/core/src/index.ts index ba7265892..82e715785 100644 --- a/packages/headless-components/core/src/index.ts +++ b/packages/headless-components/core/src/index.ts @@ -1,3 +1,2 @@ -export function Core() { - return "hello"; -} +export * from "./context"; +export * from "./directive"; diff --git a/packages/headless-components/stores/package.json b/packages/headless-components/stores/package.json index a5c607d5e..5b9fcb324 100644 --- a/packages/headless-components/stores/package.json +++ b/packages/headless-components/stores/package.json @@ -6,7 +6,12 @@ "test": "vitest" }, "exports": { - "./react": "./dist/react/index.js" + "./react": "./dist/react/index.js", + "./react/hookim": "./dist/react/hookim/index.js", + "./services": "./dist/services/index.js", + "./astro/BuyNowService.astro": "./src/astro/BuyNowService.astro", + "./astro/BuyNowServiceContext": "./dist/astro/BuyNowServiceContext.js", + "./astro/withBuyButtonService": "./dist/astro/withBuyButtonService.js" }, "devDependencies": { "@testing-library/dom": "^10.4.0", @@ -20,6 +25,8 @@ }, "dependencies": { "@wix/ecom": "^1.0.1169", - "@wix/redirects": "^1.0.79" + "@wix/redirects": "^1.0.79", + "@wix/services-definitions": "^0.1.2", + "@wix/services-manager": "^0.2.6" } } diff --git a/packages/headless-components/stores/src/astro/BuyNowService.astro b/packages/headless-components/stores/src/astro/BuyNowService.astro new file mode 100644 index 000000000..3fe2005b9 --- /dev/null +++ b/packages/headless-components/stores/src/astro/BuyNowService.astro @@ -0,0 +1,40 @@ +--- +import { BuyNowServiceProvider } from "./BuyNowServiceContext"; +import { buynowService, buynowserviceDefinition } from "../services"; +import { createServicesManager, createServicesMap } from '@wix/services-manager'; + +const map = createServicesMap().addService(buynowserviceDefinition, buynowService, { + productId: Astro.props.productId, + variantId: Astro.props.variantId, +}); +const mgr = createServicesManager(map); +--- + + + + + + + diff --git a/packages/headless-components/stores/src/astro/BuyNowServiceContext.ts b/packages/headless-components/stores/src/astro/BuyNowServiceContext.ts new file mode 100644 index 000000000..2de1b27a8 --- /dev/null +++ b/packages/headless-components/stores/src/astro/BuyNowServiceContext.ts @@ -0,0 +1,10 @@ +import { createContext, withContext } from "@wix/headless-components-core"; +import { buynowService } from "../services"; + +(globalThis as any).BuyNowServiceContext ||= createContext<{service: ReturnType}>(); + +export const [BuyNowServiceProvider, getBuyNowServiceProvider] = (globalThis as any).BuyNowServiceContext; + +export const withBuyButtonService = (Component: React.ComponentType) => { + return withContext((globalThis as any).BuyNowServiceContext[1], Component); +} diff --git a/packages/headless-components/stores/src/astro/withBuyButtonService.tsx b/packages/headless-components/stores/src/astro/withBuyButtonService.tsx new file mode 100644 index 000000000..00c44be8f --- /dev/null +++ b/packages/headless-components/stores/src/astro/withBuyButtonService.tsx @@ -0,0 +1,20 @@ +import type { Signal } from "@wix/services-definitions"; +import { withContext } from "@wix/headless-components-core"; +import { getBuyNowServiceProvider } from "@wix/headless-stores/astro/BuyNowServiceContext"; + +export const BuyNow = withContext(getBuyNowServiceProvider, ({ context }) => { + console.log("context", context); + // @ts-expect-error + const { loading, error, redirectToCheckout } = context; + + if ((loading as Signal).get()) return <>Preparing checkout...; + if ((error as Signal).get()) return <>Error: {(error as Signal).get()}; + + return +}); + +export const withBuyButtonService = (Component: React.ComponentType) => { + return withContext(getBuyNowServiceProvider, Component); +} diff --git a/packages/headless-components/stores/src/react/BuyNow.tsx b/packages/headless-components/stores/src/react/BuyNow.tsx index b3499957a..ede11cfea 100644 --- a/packages/headless-components/stores/src/react/BuyNow.tsx +++ b/packages/headless-components/stores/src/react/BuyNow.tsx @@ -85,7 +85,8 @@ export function BuyNow(props: { appId: CATLOG_APP_ID_V3, options: { options: props.variant, - } + //variantId: "08efa314-a7fe-48d5-944c-942a1a0e57a6" + }, }, quantity: 1 }], diff --git a/packages/headless-components/stores/src/react/hookim/index.ts b/packages/headless-components/stores/src/react/hookim/index.ts new file mode 100644 index 000000000..e1681d56c --- /dev/null +++ b/packages/headless-components/stores/src/react/hookim/index.ts @@ -0,0 +1,24 @@ +import { useState } from "react"; +import { getCheckoutUrlForProduct } from "../../utils"; + +export function useBuyNow(productId: string, variantId: string) { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const redirectToCheckout = async () => { + setIsLoading(true); + try { + const checkoutUrl = await getCheckoutUrlForProduct(productId, variantId); + window.location.href = checkoutUrl; + } catch (error) { + setError(error as Error); + setIsLoading(false); + } + } + + return { + isLoading, + error, + redirectToCheckout, + } +} diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts new file mode 100644 index 000000000..55a4ac87e --- /dev/null +++ b/packages/headless-components/stores/src/services/index.ts @@ -0,0 +1,34 @@ +import { defineService, implementService, Signal } from "@wix/services-definitions"; +import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; +import { getCheckoutUrlForProduct } from "../utils"; + +export const buynowserviceDefinition = defineService<{ + redirectToCheckout: () => Promise, + loading: Signal, + error: Signal, +}>("buynow") + +export const buynowService = implementService.withConfig<{ + productId: string, + variantId: string, +}>()(buynowserviceDefinition, ({ getService, config }) => { + const signalsService = getService(SignalsServiceDefinition); + const loadingSignal = signalsService.signal(false) as Signal; + const errorSignal = signalsService.signal(null) as Signal; + + return { + redirectToCheckout: async () => { + loadingSignal.set(true); + try { + const checkoutUrl = await getCheckoutUrlForProduct(config.productId, config.variantId); + window.location.href = checkoutUrl; + } catch (error) { + errorSignal.set(error as string); + } finally { + loadingSignal.set(false); + } + }, + loading: loadingSignal, + error: errorSignal, + } +}) diff --git a/packages/headless-components/stores/src/utils/index.ts b/packages/headless-components/stores/src/utils/index.ts new file mode 100644 index 000000000..e3446b774 --- /dev/null +++ b/packages/headless-components/stores/src/utils/index.ts @@ -0,0 +1,33 @@ +import { checkout } from "@wix/ecom"; +import { redirects } from "@wix/redirects"; + +const CATLOG_APP_ID_V3 = "215238eb-22a5-4c36-9e7b-e7c08025e04e"; + +export async function getCheckoutUrlForProduct(productId: string, variantId: string) { + const checkoutResult = await checkout.createCheckout({ + lineItems: [{ + catalogReference: { + catalogItemId: productId, + appId: CATLOG_APP_ID_V3, + options: { + variantId + }, + }, + quantity: 1 + }], + channelType: checkout.ChannelType.WEB, + }); + + if (!checkoutResult._id) { + throw new Error("Failed to create checkout"); + } + + const { redirectSession } = await redirects.createRedirectSession({ + ecomCheckout: { checkoutId: checkoutResult._id }, + callbacks: { + postFlowUrl: window.location.href, + }, + }); + + return redirectSession?.fullUrl!; +} diff --git a/yarn.lock b/yarn.lock index 623e00033..858f9faed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -85,6 +85,35 @@ __metadata: languageName: node linkType: hard +"@astrojs/markdown-remark@npm:6.3.2": + version: 6.3.2 + resolution: "@astrojs/markdown-remark@npm:6.3.2" + dependencies: + "@astrojs/internal-helpers": "npm:0.6.1" + "@astrojs/prism": "npm:3.3.0" + github-slugger: "npm:^2.0.0" + hast-util-from-html: "npm:^2.0.3" + hast-util-to-text: "npm:^4.0.2" + import-meta-resolve: "npm:^4.1.0" + js-yaml: "npm:^4.1.0" + mdast-util-definitions: "npm:^6.0.0" + rehype-raw: "npm:^7.0.0" + rehype-stringify: "npm:^10.0.1" + remark-gfm: "npm:^4.0.1" + remark-parse: "npm:^11.0.0" + remark-rehype: "npm:^11.1.2" + remark-smartypants: "npm:^3.0.2" + shiki: "npm:^3.2.1" + smol-toml: "npm:^1.3.1" + unified: "npm:^11.0.5" + unist-util-remove-position: "npm:^5.0.0" + unist-util-visit: "npm:^5.0.0" + unist-util-visit-parents: "npm:^6.0.1" + vfile: "npm:^6.0.3" + checksum: 10c0/e4e9926286def70548505d49333c0b80624e36dad035d0b943ef4d56211530784a8317f92277c702b6a47b68dea0f9afd154aacf2c6b8361177a51ac3c8b20bc + languageName: node + linkType: hard + "@astrojs/prism@npm:3.2.0": version: 3.2.0 resolution: "@astrojs/prism@npm:3.2.0" @@ -94,6 +123,15 @@ __metadata: languageName: node linkType: hard +"@astrojs/prism@npm:3.3.0": + version: 3.3.0 + resolution: "@astrojs/prism@npm:3.3.0" + dependencies: + prismjs: "npm:^1.30.0" + checksum: 10c0/8a87f2589f4a3e9ea982e3dd0a3e4ebf565b2e5cf16aa70d979cbddab241a7a24d7be45176fa8c5f69f000cd9ab311ab4677d7a15e2ba0cbd610c80db8b9d7dd + languageName: node + linkType: hard + "@astrojs/react@npm:^4.2.1": version: 4.2.5 resolution: "@astrojs/react@npm:4.2.5" @@ -110,6 +148,22 @@ __metadata: languageName: node linkType: hard +"@astrojs/react@npm:^4.3.0": + version: 4.3.0 + resolution: "@astrojs/react@npm:4.3.0" + dependencies: + "@vitejs/plugin-react": "npm:^4.4.1" + ultrahtml: "npm:^1.6.0" + vite: "npm:^6.3.5" + peerDependencies: + "@types/react": ^17.0.50 || ^18.0.21 || ^19.0.0 + "@types/react-dom": ^17.0.17 || ^18.0.6 || ^19.0.0 + react: ^17.0.2 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0 + checksum: 10c0/619f5d6286156b04fd5e1c48e817ce8ae18f6969f8f849fc953de0333b4f212b6a61d677ee8b2b198cf7aeaa9c125e8d8be5bdf0ca2c85e320266f0991f0b518 + languageName: node + linkType: hard + "@astrojs/telemetry@npm:3.2.1": version: 3.2.1 resolution: "@astrojs/telemetry@npm:3.2.1" @@ -125,6 +179,21 @@ __metadata: languageName: node linkType: hard +"@astrojs/telemetry@npm:3.3.0": + version: 3.3.0 + resolution: "@astrojs/telemetry@npm:3.3.0" + dependencies: + ci-info: "npm:^4.2.0" + debug: "npm:^4.4.0" + dlv: "npm:^1.1.3" + dset: "npm:^3.1.4" + is-docker: "npm:^3.0.0" + is-wsl: "npm:^3.1.0" + which-pm-runs: "npm:^1.1.0" + checksum: 10c0/7c575aad221d7335b6b1378ceac0e60a25c9540cdde8f5584b0ffe565d06b3ecfc2217738d1ce55ac13eb66e1a6251453bddf117d7f793e51b3fc7be5d001ea4 + languageName: node + linkType: hard + "@astrojs/underscore-redirects@npm:^0.4.0": version: 0.4.0 resolution: "@astrojs/underscore-redirects@npm:0.4.0" @@ -1278,6 +1347,25 @@ __metadata: languageName: node linkType: hard +"@preact/signals-core@npm:^1.7.0, @preact/signals-core@npm:^1.8.0": + version: 1.8.0 + resolution: "@preact/signals-core@npm:1.8.0" + checksum: 10c0/fa773157621d881e7aefddb8dbded805d8203600211040cc4b5907726e288a163fa58558c1d36bc7085d75e6488af0a806322bbbed005d08be76a76f40244246 + languageName: node + linkType: hard + +"@preact/signals-react@npm:^3.0.1, @preact/signals-react@npm:^3.1.1": + version: 3.1.1 + resolution: "@preact/signals-react@npm:3.1.1" + dependencies: + "@preact/signals-core": "npm:^1.7.0" + use-sync-external-store: "npm:^1.2.0" + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + checksum: 10c0/35654538988f1638fa0f78f69414bf46ac7de646ddf514d0b54e152e2da2055a0e1ee2a9753e201ee9c07be92793b1d56657951c5bb5dc05b5864bafff1b6087 + languageName: node + linkType: hard + "@react-aria/focus@npm:^3.19.0": version: 3.20.2 resolution: "@react-aria/focus@npm:3.20.2" @@ -1395,6 +1483,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-beta.9": + version: 1.0.0-beta.9 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.9" + checksum: 10c0/21aebb7ebd093282efd96f63ddd465f76746b1d70282366d6ccc7fff6eb4da5c2f8f4bfaaaeb4283c2432600e5609e39e9897864575e593efc11d376ca1a6fa1 + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^5.1.4": version: 5.1.4 resolution: "@rollup/pluginutils@npm:5.1.4" @@ -1911,6 +2006,15 @@ __metadata: languageName: node linkType: hard +"@types/fontkit@npm:^2.0.8": + version: 2.0.8 + resolution: "@types/fontkit@npm:2.0.8" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/e5a124d468f17d3b74a07d38257fc38b8d3d1e3e1e68b1c4a3314beb274223499009f4a6c1d2f15a9928ad6643fb8bfca4881d13447cfbf5de1733ad6fd5d4b1 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -2072,6 +2176,15 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^19.1.5": + version: 19.1.5 + resolution: "@types/react-dom@npm:19.1.5" + peerDependencies: + "@types/react": ^19.0.0 + checksum: 10c0/2a29e77cf6bb6e9f57bcfa54509c216cad2e16e244f0bd56369966ec88c072b9c91f6011d14f9e18fbfe2b801b18b86f616de75e5c8aef0be73c1f74abb33b49 + languageName: node + linkType: hard + "@types/react@npm:*": version: 19.1.2 resolution: "@types/react@npm:19.1.2" @@ -2101,6 +2214,15 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^19.1.5": + version: 19.1.6 + resolution: "@types/react@npm:19.1.6" + dependencies: + csstype: "npm:^3.0.2" + checksum: 10c0/8b10b198e28997b3c57559750f8bcf5ae7b33c554b16b6f4fe2ece1d4de6a2fc8cb53e7effe08ec9cb939d2f479eb97c5e08aac2cf83b10a90164fe451cc8ea2 + languageName: node + linkType: hard + "@types/retry@npm:0.12.2": version: 0.12.2 resolution: "@types/retry@npm:0.12.2" @@ -2237,6 +2359,22 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react@npm:^4.4.1": + version: 4.5.0 + resolution: "@vitejs/plugin-react@npm:4.5.0" + dependencies: + "@babel/core": "npm:^7.26.10" + "@babel/plugin-transform-react-jsx-self": "npm:^7.25.9" + "@babel/plugin-transform-react-jsx-source": "npm:^7.25.9" + "@rolldown/pluginutils": "npm:1.0.0-beta.9" + "@types/babel__core": "npm:^7.20.5" + react-refresh: "npm:^0.17.0" + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + checksum: 10c0/c9f75cde098b9aac62cb512103d7f898a0a173cb78dc9fcf79ca4b3f21a1458cd1955a4383c8c9e3841ce23c5e7f02ed1455e445c9574879b143d40734121fd8 + languageName: node + linkType: hard + "@vitest/expect@npm:3.1.4": version: 3.1.4 resolution: "@vitest/expect@npm:3.1.4" @@ -2339,16 +2477,19 @@ __metadata: version: 0.0.0-use.local resolution: "@wix/astro-components-demo@workspace:examples/astro-components-demo" dependencies: - "@astrojs/react": "npm:^4.2.1" + "@astrojs/react": "npm:^4.3.0" "@playwright/test": "npm:^1.51.1" + "@types/react": "npm:^19.1.5" + "@types/react-dom": "npm:^19.1.5" "@wix/astro": "workspace:*" "@wix/cli-edge": "npm:^1.1.74" "@wix/headless-bookings": "workspace:*" "@wix/headless-ecom": "workspace:*" "@wix/headless-stores": "workspace:*" - astro: "npm:^5.5.4" - react: "npm:^18.3.1" - react-dom: "npm:^18.3.1" + "@wix/services-manager-react": "npm:^0.1.9" + astro: "npm:^5.8.0" + react: "npm:^19.1.0" + react-dom: "npm:^19.1.0" languageName: unknown linkType: soft @@ -2389,7 +2530,7 @@ __metadata: "@wix/blog": "npm:^1.0.345" "@wix/data": "npm:^1.0.194" "@wix/identity": "npm:^1.0.125" - "@wix/sdk": "npm:^1.15.20" + "@wix/sdk": "npm:^1.15.22" chalk: "npm:^5.4.1" esm-resolve: "npm:^1.0.11" globby: "npm:^14.0.2" @@ -3415,6 +3556,15 @@ __metadata: languageName: node linkType: hard +"@wix/error-handler-types@npm:^1.7.0": + version: 1.7.0 + resolution: "@wix/error-handler-types@npm:1.7.0" + dependencies: + "@babel/runtime": "npm:^7.27.1" + checksum: 10c0/6bbc25229dbfbd9112acd5c690d0f42f7310ae3ba979c1cdd0aa0aad6e488f1d8f7210f2532b60d1887cc55e186437145fb28c35c00467cb2782a984d2784985 + languageName: node + linkType: hard + "@wix/filter-builder@npm:1.0.127": version: 1.0.127 resolution: "@wix/filter-builder@npm:1.0.127" @@ -3439,6 +3589,7 @@ __metadata: version: 0.0.0-use.local resolution: "@wix/headless-components-core@workspace:packages/headless-components/core" dependencies: + "@preact/signals-react": "npm:^3.1.1" "@types/node": "npm:^20.9.0" typescript: "npm:^5.7.3" languageName: unknown @@ -3478,6 +3629,8 @@ __metadata: "@vitest/ui": "npm:^3.1.4" "@wix/ecom": "npm:^1.0.1169" "@wix/redirects": "npm:^1.0.79" + "@wix/services-definitions": "npm:^0.1.2" + "@wix/services-manager": "npm:^0.2.6" jsdom: "npm:^26.1.0" typescript: "npm:^5.7.3" vitest: "npm:^3.1.4" @@ -3506,6 +3659,16 @@ __metadata: languageName: node linkType: hard +"@wix/image-kit@npm:^1.108.0": + version: 1.108.0 + resolution: "@wix/image-kit@npm:1.108.0" + dependencies: + "@babel/runtime": "npm:^7.26.0" + tslib: "npm:^2.8.1" + checksum: 10c0/04faf80a4ba43c72417020935299d85f84348bd2152d75fc55db7725f008e41531cd06137ec8762850cc98faf3511bd54c9824164e130f1db34fee7e5d07058d + languageName: node + linkType: hard + "@wix/image@npm:^1.325.0": version: 1.378.0 resolution: "@wix/image@npm:1.378.0" @@ -3659,6 +3822,15 @@ __metadata: languageName: node linkType: hard +"@wix/sdk-react-context@npm:0.0.2": + version: 0.0.2 + resolution: "@wix/sdk-react-context@npm:0.0.2" + peerDependencies: + react: "*" + checksum: 10c0/27b4d0c83dfbc3cd30fde6b5b34cfc99badac3f1e655dc3b9a2fa62fe1c8e15d077b5181c721a46d436d2bea4e804a331d718c7def6b3fbfd81ce221088018ab + languageName: node + linkType: hard + "@wix/sdk-runtime@npm:0.3.46": version: 0.3.46 resolution: "@wix/sdk-runtime@npm:0.3.46" @@ -3669,6 +3841,16 @@ __metadata: languageName: node linkType: hard +"@wix/sdk-runtime@npm:0.3.51": + version: 0.3.51 + resolution: "@wix/sdk-runtime@npm:0.3.51" + dependencies: + "@wix/sdk-context": "npm:0.0.1" + "@wix/sdk-types": "npm:^1.13.24" + checksum: 10c0/ab3f3d858e2de56a218b5dcbcf289a9e334255e34366a5562ece4c4e392d85dc1de94b4e70204a0466dede59fcdae7ac5bc4cfbffb03a03257108c126f39fcbc + languageName: node + linkType: hard + "@wix/sdk-runtime@npm:^0.3.41, @wix/sdk-runtime@npm:^0.3.42, @wix/sdk-runtime@npm:^0.3.49": version: 0.3.52 resolution: "@wix/sdk-runtime@npm:0.3.52" @@ -3690,6 +3872,17 @@ __metadata: languageName: node linkType: hard +"@wix/sdk-types@npm:^1.13.24": + version: 1.13.26 + resolution: "@wix/sdk-types@npm:1.13.26" + dependencies: + "@wix/error-handler-types": "npm:^1.7.0" + "@wix/monitoring-types": "npm:^0.12.0" + type-fest: "npm:^4.41.0" + checksum: 10c0/9987c3651f0af59b58970014c967fe87e114b021145b2fb9f945ff254395bd4b2753f9072140627e3b6dec4a48065eca7d56cf586724dd2a8d1e1e08fcf64a51 + languageName: node + linkType: hard + "@wix/sdk@npm:^1.14.3, @wix/sdk@npm:^1.15.20": version: 1.15.20 resolution: "@wix/sdk@npm:1.15.20" @@ -3710,6 +3903,60 @@ __metadata: languageName: node linkType: hard +"@wix/sdk@npm:^1.15.22": + version: 1.15.22 + resolution: "@wix/sdk@npm:1.15.22" + dependencies: + "@wix/identity": "npm:^1.0.104" + "@wix/image-kit": "npm:^1.108.0" + "@wix/redirects": "npm:^1.0.70" + "@wix/sdk-context": "npm:0.0.1" + "@wix/sdk-runtime": "npm:0.3.51" + "@wix/sdk-types": "npm:^1.13.24" + graphql: "npm:^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + jose: "npm:^5.10.0" + type-fest: "npm:^4.41.0" + dependenciesMeta: + graphql: + optional: true + checksum: 10c0/2b35f8302e58ef7979c77e1526ebfe8a34c4e70330240cb22f28b5e8abcadfbdec859df286c522d9d0d0b6e1f5e79bf747295f14acfdb7a6ed20d42f4b3eef13 + languageName: node + linkType: hard + +"@wix/services-definitions@npm:^0.1.2": + version: 0.1.2 + resolution: "@wix/services-definitions@npm:0.1.2" + dependencies: + type-fest: "npm:^4.40.1" + checksum: 10c0/45d51c40ae54d46b2fd5eb1eb514b4a92df2679cebb0d15ee8ad17f977b72bd8accbc378a9832bab3c8bbc42eafab55f46622eff3c0711f2255694e0df8c417b + languageName: node + linkType: hard + +"@wix/services-manager-react@npm:^0.1.9": + version: 0.1.9 + resolution: "@wix/services-manager-react@npm:0.1.9" + dependencies: + "@preact/signals-react": "npm:^3.0.1" + "@wix/sdk-react-context": "npm:0.0.2" + "@wix/services-manager": "npm:0.2.6" + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + checksum: 10c0/699c65a904e1d436efbbe8101fd4fdbaf56477e7efae5c7ba5088581efa0e3a1bd5eb64bb7bdc26d1fe5af945aa2aa80e0a787f58b56046cb5e6b7ebed812112 + languageName: node + linkType: hard + +"@wix/services-manager@npm:0.2.6, @wix/services-manager@npm:^0.2.6": + version: 0.2.6 + resolution: "@wix/services-manager@npm:0.2.6" + dependencies: + "@preact/signals-core": "npm:^1.8.0" + "@wix/sdk-context": "npm:0.0.1" + "@wix/services-definitions": "npm:^0.1.2" + comlink: "npm:^4.4.2" + checksum: 10c0/2a39787bdd5c567a0d3151e7f8496b79831841c5180f82edfed3fbd9e4dbfa198941eeeb35cdf061c5859deb731c2eece35aaf6b75c7bcd3cfbc91988d74b4a8 + languageName: node + linkType: hard + "@wix/unidriver-common@npm:^1.3.2": version: 1.3.2 resolution: "@wix/unidriver-common@npm:1.3.2" @@ -4073,6 +4320,81 @@ __metadata: languageName: node linkType: hard +"astro@npm:^5.8.0": + version: 5.8.0 + resolution: "astro@npm:5.8.0" + dependencies: + "@astrojs/compiler": "npm:^2.11.0" + "@astrojs/internal-helpers": "npm:0.6.1" + "@astrojs/markdown-remark": "npm:6.3.2" + "@astrojs/telemetry": "npm:3.3.0" + "@capsizecss/unpack": "npm:^2.4.0" + "@oslojs/encoding": "npm:^1.1.0" + "@rollup/pluginutils": "npm:^5.1.4" + acorn: "npm:^8.14.1" + aria-query: "npm:^5.3.2" + axobject-query: "npm:^4.1.0" + boxen: "npm:8.0.1" + ci-info: "npm:^4.2.0" + clsx: "npm:^2.1.1" + common-ancestor-path: "npm:^1.0.1" + cookie: "npm:^1.0.2" + cssesc: "npm:^3.0.0" + debug: "npm:^4.4.0" + deterministic-object-hash: "npm:^2.0.2" + devalue: "npm:^5.1.1" + diff: "npm:^5.2.0" + dlv: "npm:^1.1.3" + dset: "npm:^3.1.4" + es-module-lexer: "npm:^1.6.0" + esbuild: "npm:^0.25.0" + estree-walker: "npm:^3.0.3" + flattie: "npm:^1.1.1" + fontace: "npm:~0.3.0" + github-slugger: "npm:^2.0.0" + html-escaper: "npm:3.0.3" + http-cache-semantics: "npm:^4.1.1" + import-meta-resolve: "npm:^4.1.0" + js-yaml: "npm:^4.1.0" + kleur: "npm:^4.1.5" + magic-string: "npm:^0.30.17" + magicast: "npm:^0.3.5" + mrmime: "npm:^2.0.1" + neotraverse: "npm:^0.6.18" + p-limit: "npm:^6.2.0" + p-queue: "npm:^8.1.0" + package-manager-detector: "npm:^1.1.0" + picomatch: "npm:^4.0.2" + prompts: "npm:^2.4.2" + rehype: "npm:^13.0.2" + semver: "npm:^7.7.1" + sharp: "npm:^0.33.3" + shiki: "npm:^3.2.1" + tinyexec: "npm:^0.3.2" + tinyglobby: "npm:^0.2.12" + tsconfck: "npm:^3.1.5" + ultrahtml: "npm:^1.6.0" + unifont: "npm:~0.5.0" + unist-util-visit: "npm:^5.0.0" + unstorage: "npm:^1.15.0" + vfile: "npm:^6.0.3" + vite: "npm:^6.3.4" + vitefu: "npm:^1.0.6" + xxhash-wasm: "npm:^1.1.0" + yargs-parser: "npm:^21.1.1" + yocto-spinner: "npm:^0.2.1" + zod: "npm:^3.24.2" + zod-to-json-schema: "npm:^3.24.5" + zod-to-ts: "npm:^1.2.0" + dependenciesMeta: + sharp: + optional: true + bin: + astro: astro.js + checksum: 10c0/124d40e023d3126ecb307ccdc17f7a195ddac8d22a8ef826f5915ef2c41f38e63976a10738705afc1b60119dcd32f26a9e3a3799b297df611dfe75e2124273f5 + languageName: node + linkType: hard + "axobject-query@npm:^4.1.0": version: 4.1.0 resolution: "axobject-query@npm:4.1.0" @@ -4616,6 +4938,13 @@ __metadata: languageName: node linkType: hard +"comlink@npm:^4.4.2": + version: 4.4.2 + resolution: "comlink@npm:4.4.2" + checksum: 10c0/38aa1f455cf08e94aaa8fc494fd203cc0ef02ece6c21404b7931ce17567e8a72deacddab98aa5650cfd78332ff24c34610586f6fb27fd19dc77e753ed1980deb + languageName: node + linkType: hard + "comma-separated-tokens@npm:^2.0.0": version: 2.0.3 resolution: "comma-separated-tokens@npm:2.0.3" @@ -5794,7 +6123,17 @@ __metadata: languageName: node linkType: hard -"fontkit@npm:^2.0.2": +"fontace@npm:~0.3.0": + version: 0.3.0 + resolution: "fontace@npm:0.3.0" + dependencies: + "@types/fontkit": "npm:^2.0.8" + fontkit: "npm:^2.0.4" + checksum: 10c0/a81bef4f20c4bb6bb2cb7777d6fa267edb341b1b4c549b0918473d399c2314bf482f098d8ba0ae839bdfc8b63daa78815b647fd781157cb2d8bfc78c56a9745a + languageName: node + linkType: hard + +"fontkit@npm:^2.0.2, fontkit@npm:^2.0.4": version: 2.0.4 resolution: "fontkit@npm:2.0.4" dependencies: @@ -8917,7 +9256,7 @@ __metadata: languageName: node linkType: hard -"prismjs@npm:^1.29.0": +"prismjs@npm:^1.29.0, prismjs@npm:^1.30.0": version: 1.30.0 resolution: "prismjs@npm:1.30.0" checksum: 10c0/f56205bfd58ef71ccfcbcb691fd0eb84adc96c6ff21b0b69fc6fdcf02be42d6ef972ba4aed60466310de3d67733f6a746f89f2fb79c00bf217406d465b3e8f23 @@ -9540,6 +9879,17 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:^19.1.0": + version: 19.1.0 + resolution: "react-dom@npm:19.1.0" + dependencies: + scheduler: "npm:^0.26.0" + peerDependencies: + react: ^19.1.0 + checksum: 10c0/3e26e89bb6c67c9a6aa86cb888c7a7f8258f2e347a6d2a15299c17eb16e04c19194e3452bc3255bd34000a61e45e2cb51e46292392340432f133e5a5d2dfb5fc + languageName: node + linkType: hard + "react-fast-compare@npm:^3.0.1": version: 3.2.2 resolution: "react-fast-compare@npm:3.2.2" @@ -9853,6 +10203,13 @@ __metadata: languageName: node linkType: hard +"react@npm:^19.1.0": + version: 19.1.0 + resolution: "react@npm:19.1.0" + checksum: 10c0/530fb9a62237d54137a13d2cfb67a7db6a2156faed43eecc423f4713d9b20c6f2728b026b45e28fcd72e8eadb9e9ed4b089e99f5e295d2f0ad3134251bdd3698 + languageName: node + linkType: hard + "read-yaml-file@npm:^2.1.0": version: 2.1.0 resolution: "read-yaml-file@npm:2.1.0" @@ -10014,7 +10371,7 @@ __metadata: languageName: node linkType: hard -"remark-rehype@npm:^11.1.1": +"remark-rehype@npm:^11.1.1, remark-rehype@npm:^11.1.2": version: 11.1.2 resolution: "remark-rehype@npm:11.1.2" dependencies: @@ -10331,6 +10688,13 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.26.0": + version: 0.26.0 + resolution: "scheduler@npm:0.26.0" + checksum: 10c0/5b8d5bfddaae3513410eda54f2268e98a376a429931921a81b5c3a2873aab7ca4d775a8caac5498f8cbc7d0daeab947cf923dbd8e215d61671f9f4e392d34356 + languageName: node + linkType: hard + "select@npm:^1.1.2": version: 1.1.2 resolution: "select@npm:1.1.2" @@ -11222,7 +11586,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.21.0, type-fest@npm:^4.40.0, type-fest@npm:^4.41.0": +"type-fest@npm:^4.21.0, type-fest@npm:^4.40.0, type-fest@npm:^4.40.1, type-fest@npm:^4.41.0": version: 4.41.0 resolution: "type-fest@npm:4.41.0" checksum: 10c0/f5ca697797ed5e88d33ac8f1fec21921839871f808dc59345c9cf67345bfb958ce41bd821165dbf3ae591cedec2bf6fe8882098dfdd8dc54320b859711a2c1e4 @@ -11366,6 +11730,16 @@ __metadata: languageName: node linkType: hard +"unifont@npm:~0.5.0": + version: 0.5.0 + resolution: "unifont@npm:0.5.0" + dependencies: + css-tree: "npm:^3.0.0" + ohash: "npm:^2.0.0" + checksum: 10c0/4395af8a95722d88389ae100686577a80d9ff7a3e129dadf836e7beb1dbbd448d7bb12b9d8988084c186748fbb4763ce19457c7a119b8343a0fb5c81767896a2 + languageName: node + linkType: hard + "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -11567,7 +11941,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.2.2": +"use-sync-external-store@npm:^1.2.0, use-sync-external-store@npm:^1.2.2": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0" peerDependencies: @@ -11651,7 +12025,7 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.0.0 || ^6.0.0, vite@npm:^6.0.0, vite@npm:^6.2.6": +"vite@npm:^5.0.0 || ^6.0.0, vite@npm:^6.0.0, vite@npm:^6.2.6, vite@npm:^6.3.4, vite@npm:^6.3.5": version: 6.3.5 resolution: "vite@npm:6.3.5" dependencies: From 02019a6b266490df0b24747589143515a16fa558 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Wed, 28 May 2025 14:56:40 +0300 Subject: [PATCH 09/42] no no no --- examples/astro-components-demo/.env.local.bak | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 examples/astro-components-demo/.env.local.bak diff --git a/examples/astro-components-demo/.env.local.bak b/examples/astro-components-demo/.env.local.bak deleted file mode 100644 index 7cc3fe9a6..000000000 --- a/examples/astro-components-demo/.env.local.bak +++ /dev/null @@ -1,6 +0,0 @@ -ENV_NAME="production" -WIX_CLIENT_ID="361476f1-b0fa-40b8-8b24-69235c9d492b" -WIX_CLIENT_SECRET="3d21f749-4155-4374-b33a-665a49efc766" -WIX_CLIENT_INSTANCE_ID="f1b88319-8333-4e76-819c-7a4ba0409a1f" -WIX_CLIENT_PUBLIC_KEY="LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxSlBmMkt4WWNKQ0poeXEyMUpRYQpFZTNJWG1hcXo2MnFLVS9rK1QzN3F6VlAyaTRVT1VZMjdQc3Z5ZW9EOGc3YmhpWHNKbjlZYmo1K2o2NWVWVGhsCnAwRFI1c01ueHhOM2pVbENHQmdkY2NwVEZvcWFFbmJYY1VSaklVbjliaFJjUHRHOWNzTHlPM3g3V0hIS3V0a1kKRXpzQnNnVFZTSUIyQ0RSdXRRU3pPRlBZQzloL2k1cHNERkI2dE9UYXptLy96QjVFMjJIZ2hrSTA0TmEwSEt4UApGV082Z3BnT29helRFL2M4Qkt4OEphV25uRWRGeEcwNlJIZ2s5alhrc0ZoZm4zMEVvQUdBeVJJazU5blhzZHZkCk83aEU5dFVuaFJES3hKNzAwcGhNVStOaC9xdEkzcXBIbitobDlvU1A3R2F5VFY5VHVMTFZEQmkxZUM0ZUdFS20KaHdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t" -WIX_DEPLOYMENT_DOMAIN="hanut-shel-yury-xnw3ekus-yurym4.wix-sites.dev" From 0e653f79c3f1186c1301dd1f6feef4f347696784 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Thu, 29 May 2025 10:10:23 +0300 Subject: [PATCH 10/42] Sprinkle some promises --- .../headless-components/core/src/directive.ts | 82 +++++++++++++++++-- .../stores/src/astro/BuyNowService.astro | 5 +- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/packages/headless-components/core/src/directive.ts b/packages/headless-components/core/src/directive.ts index 47c9a2954..83bac18fc 100644 --- a/packages/headless-components/core/src/directive.ts +++ b/packages/headless-components/core/src/directive.ts @@ -1,17 +1,87 @@ import type { ClientDirective } from "astro"; +function createPromiseHandle() : { + promise: Promise; + resolve: (value: T) => void; + reject: (reason?: unknown) => void; +} { + let resolve: (value: T) => void; + let reject: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve: resolve!, reject: reject! }; +} + +type Task = { + load: () => Promise<(props?: any) => Promise>; + el: Element; + promiseHandle: ReturnType>; +} + +const pendingTasks: Set = new Set(); + export default (async (load, _, el) => { - await new Promise(resolve => setTimeout(resolve, 5000)); + const task: Task = { + load, + el, + promiseHandle: createPromiseHandle(), + }; + pendingTasks.add(task); + + await processPendingTasks(); + + return task.promiseHandle.promise; +}); + +async function processPendingTasks() { + const tasks = Array.from(pendingTasks); + for (const task of tasks) { + await processTask(task); + } +} + +async function processTask(task: Task) { + const service = getServiceFromElement(task.el); + if (!service) { + console.log("Service not found on closest service provider"); + return; + } + assignServiceToElement(task.el, service); + const [hydrate] = await Promise.all([task.load()]); + await hydrate(); + task.promiseHandle.resolve(); + pendingTasks.delete(task); +} - const service = (el.closest("context-provider") as any).service; - console.log("context-provider service", service); +function assignServiceToElement(el: Element, service: any) { + console.log("assign service to element", service); const contextId = crypto.randomUUID(); (globalThis as any).contexts = (globalThis as any).contexts || {}; (globalThis as any).contexts[contextId] = service; + setContextIdOnProps(el, contextId); +} + + +function getServiceFromElement(el: Element) { + const ctxProvider = el.closest("context-provider"); + if (!ctxProvider) { + return null; + } + const service = (ctxProvider as any).service; + if (!service) { + return null; + } + return service; +} + +function setContextIdOnProps(el: Element, contextId: string) { const props = JSON.parse(el.getAttribute("props") || "{}"); console.log("context-provider Props from wait-for-store:", props); + props["contextId"] = [0, contextId]; el.setAttribute("props", JSON.stringify(props)); - const [hydrate] = await Promise.all([load()]); - await hydrate(); -}); +} + +(globalThis as any).runDirectivePendingTaks = processPendingTasks; diff --git a/packages/headless-components/stores/src/astro/BuyNowService.astro b/packages/headless-components/stores/src/astro/BuyNowService.astro index 3fe2005b9..2a93080d0 100644 --- a/packages/headless-components/stores/src/astro/BuyNowService.astro +++ b/packages/headless-components/stores/src/astro/BuyNowService.astro @@ -21,17 +21,16 @@ const mgr = createServicesManager(map); class ContextProvider extends HTMLElement { service: ReturnType; - contextId: string; constructor() { super(); - this.contextId = this.getAttribute("data-context-id")!; - console.log("ContextProvider::constructor", this.contextId); + console.log("BuyNowServiceProvider ContextProvider::constructor", this.getAttribute("data-intial-data")); const map = createServicesMap().addService(buynowserviceDefinition, buynowService, JSON.parse(this.getAttribute("data-intial-data")!)); const mgr = createServicesManager(map); const service = mgr.getService(buynowserviceDefinition); this.service = service; + (globalThis as any).runDirectivePendingTaks && (globalThis as any).runDirectivePendingTaks(); } } From 4466bbeb6f9c75e590a4476789a73a63b5015881 Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Thu, 29 May 2025 10:16:20 +0300 Subject: [PATCH 11/42] Cleanup --- packages/headless-components/core/src/context.tsx | 12 ++---------- packages/headless-components/core/src/directive.ts | 7 ++++--- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/headless-components/core/src/context.tsx b/packages/headless-components/core/src/context.tsx index efbdb9cd3..c2c0fdfca 100644 --- a/packages/headless-components/core/src/context.tsx +++ b/packages/headless-components/core/src/context.tsx @@ -7,12 +7,10 @@ import { useMemo } from "react" * * @returns A tuple containing the Provider component and a getter function for the current context. */ -let counter = 0; export const createContext = () => { // initialize context casted to type `T`. let context: T - const contextId = ++counter; - console.log('ze create context', contextId, new Error().stack); + /** * Provider component for the context. * This component sets the context value and provides it to its children components. @@ -29,7 +27,6 @@ export const createContext = () => { // avoid unintended side-effects caused by shared references //props = structuredClone(props) - console.log(`Provider::props ${contextId}`, props); return { /* Symbol indicating this is an Astro component object. */ [Symbol.toStringTag]: 'AstroComponent', @@ -38,14 +35,12 @@ export const createContext = () => { // ensure a deep clone of the provided value // avoid unintended side-effects caused by shared references context = props - console.log(`Provider::props set ${contextId}`, props); // yield rendered children components yield await slots.default() // reset context to undefined after rendering is complete context = undefined as T - console.log(`Provider::props reset ${contextId}`, context); }, } } @@ -56,10 +51,7 @@ export const createContext = () => { // return a tuple of Provider component and a getter function for the current context return [ Provider, - () => { - console.log(`Provider::context return ${contextId}`, context); - return context; - }, + () => context, ] as any as [ /** Provider component for context. */ (props: T) => any, diff --git a/packages/headless-components/core/src/directive.ts b/packages/headless-components/core/src/directive.ts index 83bac18fc..ac0f6355d 100644 --- a/packages/headless-components/core/src/directive.ts +++ b/packages/headless-components/core/src/directive.ts @@ -36,6 +36,7 @@ export default (async (load, _, el) => { }); async function processPendingTasks() { + console.log("ClientDirective - processing pending tasks", pendingTasks.size); const tasks = Array.from(pendingTasks); for (const task of tasks) { await processTask(task); @@ -45,9 +46,10 @@ async function processPendingTasks() { async function processTask(task: Task) { const service = getServiceFromElement(task.el); if (!service) { - console.log("Service not found on closest service provider"); + console.log("ClientDirective - Service not found on closest service provider"); return; } + console.log("ClientDirective - assigning service to element", service); assignServiceToElement(task.el, service); const [hydrate] = await Promise.all([task.load()]); await hydrate(); @@ -56,7 +58,6 @@ async function processTask(task: Task) { } function assignServiceToElement(el: Element, service: any) { - console.log("assign service to element", service); const contextId = crypto.randomUUID(); (globalThis as any).contexts = (globalThis as any).contexts || {}; (globalThis as any).contexts[contextId] = service; @@ -78,7 +79,7 @@ function getServiceFromElement(el: Element) { function setContextIdOnProps(el: Element, contextId: string) { const props = JSON.parse(el.getAttribute("props") || "{}"); - console.log("context-provider Props from wait-for-store:", props); + props["contextId"] = [0, contextId]; el.setAttribute("props", JSON.stringify(props)); From b8fd1d58e8fc6471bab4b54347bbc378caa8f6cf Mon Sep 17 00:00:00 2001 From: Yury Michurin Date: Thu, 29 May 2025 11:44:40 +0300 Subject: [PATCH 12/42] Proper name to integration --- examples/astro-components-demo/astro.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/astro-components-demo/astro.config.mjs b/examples/astro-components-demo/astro.config.mjs index 20a1acfb1..9236b4815 100644 --- a/examples/astro-components-demo/astro.config.mjs +++ b/examples/astro-components-demo/astro.config.mjs @@ -5,7 +5,7 @@ import { defineConfig } from "astro/config"; import react from "@astrojs/react"; const integration = { - name: "kaki-integration", + name: "headless-components-integration", hooks: { "astro:config:setup": ({ addClientDirective }) => { addClientDirective({ From f312fd4dbcf6faf79608470499d2d6313c9176d8 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Thu, 29 May 2025 15:16:35 +0300 Subject: [PATCH 13/42] implement --- .../stores/src/services/index.ts | 163 ++++++++++++++++-- 1 file changed, 152 insertions(+), 11 deletions(-) diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index 55a4ac87e..311e47210 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -1,26 +1,35 @@ -import { defineService, implementService, Signal } from "@wix/services-definitions"; +import { + defineService, + implementService, + Signal, +} from "@wix/services-definitions"; import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; import { getCheckoutUrlForProduct } from "../utils"; export const buynowserviceDefinition = defineService<{ - redirectToCheckout: () => Promise, - loading: Signal, - error: Signal, -}>("buynow") + redirectToCheckout: () => Promise; + loading: Signal; + error: Signal; +}>("buynow"); export const buynowService = implementService.withConfig<{ - productId: string, - variantId: string, + productId: string; + variantId: string; }>()(buynowserviceDefinition, ({ getService, config }) => { const signalsService = getService(SignalsServiceDefinition); const loadingSignal = signalsService.signal(false) as Signal; - const errorSignal = signalsService.signal(null) as Signal; + const errorSignal = signalsService.signal(null) as Signal< + string | null + >; return { redirectToCheckout: async () => { loadingSignal.set(true); try { - const checkoutUrl = await getCheckoutUrlForProduct(config.productId, config.variantId); + const checkoutUrl = await getCheckoutUrlForProduct( + config.productId, + config.variantId + ); window.location.href = checkoutUrl; } catch (error) { errorSignal.set(error as string); @@ -30,5 +39,137 @@ export const buynowService = implementService.withConfig<{ }, loading: loadingSignal, error: errorSignal, - } -}) + }; +}); + +export const variantSelectorServiceDefinition = defineService<{ + setOption: (group: string, value: string) => void; + selectVariantById: (id: string) => void; + loadProductVariants: (variants: any[]) => void; + resetSelections: () => void; + selectedVariant: Signal; + finalPrice: Signal; + isLowStock: Signal; + loading: Signal; + error: Signal; +}>("variantSelector"); + +export const variantSelectorService = implementService.withConfig<{ + productId: string; + initialOptions?: Record; + initialVariants?: any[]; +}>()(variantSelectorServiceDefinition, ({ getService, config }) => { + const signalsService = getService(SignalsServiceDefinition); + const loadingSignal = signalsService.signal(false) as Signal; + const errorSignal = signalsService.signal(null) as Signal< + string | null + >; + const selectedVariantSignal = signalsService.signal(null) as Signal; + const finalPriceSignal = signalsService.signal(0) as Signal; + const isLowStockSignal = signalsService.signal(false) as Signal; + + return { + setOption: (group: string, value: string) => { + // TODO: Implement option selection logic + }, + selectVariantById: (id: string) => { + // TODO: Implement variant selection logic + }, + loadProductVariants: (variants: any[]) => { + // TODO: Implement variant loading logic + }, + resetSelections: () => { + // TODO: Implement reset logic + }, + selectedVariant: selectedVariantSignal, + finalPrice: finalPriceSignal, + isLowStock: isLowStockSignal, + loading: loadingSignal, + error: errorSignal, + }; +}); + +export const productGalleryServiceDefinition = defineService<{ + setImageIndex: (index: number) => void; + resetGallery: () => void; + mapVariantToImage: (variantId: string, index: number) => void; + loadImages: (images: string[]) => void; + currentImage: Signal; + loading: Signal; + error: Signal; +}>("productGallery"); + +export const productGalleryService = implementService.withConfig<{ + initialImages?: string[]; +}>()(productGalleryServiceDefinition, ({ getService, config }) => { + const signalsService = getService(SignalsServiceDefinition); + const loadingSignal = signalsService.signal(false) as Signal; + const errorSignal = signalsService.signal(null) as Signal< + string | null + >; + const currentImageSignal = signalsService.signal("") as Signal; + + return { + setImageIndex: (index: number) => { + // TODO: Implement image index selection logic + }, + resetGallery: () => { + // TODO: Implement gallery reset logic + }, + mapVariantToImage: (variantId: string, index: number) => { + // TODO: Implement variant to image mapping logic + }, + loadImages: (images: string[]) => { + // TODO: Implement image loading logic + }, + currentImage: currentImageSignal, + loading: loadingSignal, + error: errorSignal, + }; +}); + +export const cartServiceDefinition = defineService<{ + addItem: (productId: string, variantId: string, quantity: number) => void; + removeItem: (productId: string, variantId: string) => void; + clearCart: () => void; + buyNow: (productId: string, variantId: string, quantity: number) => void; + toggleWishlist: (productId: string, variantId: string) => void; + totalQuantity: Signal; + itemCount: Signal; + loading: Signal; + error: Signal; +}>("cart"); + +export const cartService = implementService.withConfig<{ + initialItems?: any[]; +}>()(cartServiceDefinition, ({ getService, config }) => { + const signalsService = getService(SignalsServiceDefinition); + const loadingSignal = signalsService.signal(false) as Signal; + const errorSignal = signalsService.signal(null) as Signal< + string | null + >; + const totalQuantitySignal = signalsService.signal(0) as Signal; + const itemCountSignal = signalsService.signal(0) as Signal; + + return { + addItem: (productId: string, variantId: string, quantity: number) => { + // TODO: Implement add item logic + }, + removeItem: (productId: string, variantId: string) => { + // TODO: Implement remove item logic + }, + clearCart: () => { + // TODO: Implement clear cart logic + }, + buyNow: (productId: string, variantId: string, quantity: number) => { + // TODO: Implement buy now logic + }, + toggleWishlist: (productId: string, variantId: string) => { + // TODO: Implement wishlist toggle logic + }, + totalQuantity: totalQuantitySignal, + itemCount: itemCountSignal, + loading: loadingSignal, + error: errorSignal, + }; +}); From d8de9e80bb094356d35925a45da50aaa5c08d709 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Thu, 29 May 2025 15:33:17 +0300 Subject: [PATCH 14/42] implement --- .../stores/src/services/index.ts | 186 ++++++++---------- 1 file changed, 77 insertions(+), 109 deletions(-) diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index 311e47210..a8e91cf65 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -42,134 +42,102 @@ export const buynowService = implementService.withConfig<{ }; }); + export const variantSelectorServiceDefinition = defineService<{ + // State + productId: string; + sku: string; + basePrice: number; + discountPrice?: number; + isOnSale?: boolean; + ribbonLabel?: string; + options: Record; + selectedOptions: Record; + variants: Array<{ + id: string; + label: string; + stock: number; + ribbon?: string; + isPreOrder?: boolean; + }>; + selectedVariantId: string; + quantityAvailable: number; + + // Getters + selectedVariant: () => { + id: string; + label: string; + stock: number; + ribbon?: string; + isPreOrder?: boolean; + }; + finalPrice: () => number; + isLowStock: (threshold?: number) => boolean; + + // Actions setOption: (group: string, value: string) => void; selectVariantById: (id: string) => void; - loadProductVariants: (variants: any[]) => void; + loadProductVariants: ( + data: Array<{ + id: string; + label: string; + stock: number; + ribbon?: string; + isPreOrder?: boolean; + }> + ) => void; resetSelections: () => void; - selectedVariant: Signal; - finalPrice: Signal; - isLowStock: Signal; - loading: Signal; - error: Signal; }>("variantSelector"); -export const variantSelectorService = implementService.withConfig<{ - productId: string; - initialOptions?: Record; - initialVariants?: any[]; -}>()(variantSelectorServiceDefinition, ({ getService, config }) => { - const signalsService = getService(SignalsServiceDefinition); - const loadingSignal = signalsService.signal(false) as Signal; - const errorSignal = signalsService.signal(null) as Signal< - string | null - >; - const selectedVariantSignal = signalsService.signal(null) as Signal; - const finalPriceSignal = signalsService.signal(0) as Signal; - const isLowStockSignal = signalsService.signal(false) as Signal; +export const productGalleryServiceDefinition = defineService<{ + // State + images: string[]; + selectedImageIndex: number; + variantImageMap: Record; - return { - setOption: (group: string, value: string) => { - // TODO: Implement option selection logic - }, - selectVariantById: (id: string) => { - // TODO: Implement variant selection logic - }, - loadProductVariants: (variants: any[]) => { - // TODO: Implement variant loading logic - }, - resetSelections: () => { - // TODO: Implement reset logic - }, - selectedVariant: selectedVariantSignal, - finalPrice: finalPriceSignal, - isLowStock: isLowStockSignal, - loading: loadingSignal, - error: errorSignal, - }; -}); + // Getters + currentImage: () => string; + variantMappedImage: (variantId: string) => string; -export const productGalleryServiceDefinition = defineService<{ + // Actions setImageIndex: (index: number) => void; resetGallery: () => void; mapVariantToImage: (variantId: string, index: number) => void; loadImages: (images: string[]) => void; - currentImage: Signal; - loading: Signal; - error: Signal; }>("productGallery"); -export const productGalleryService = implementService.withConfig<{ - initialImages?: string[]; -}>()(productGalleryServiceDefinition, ({ getService, config }) => { - const signalsService = getService(SignalsServiceDefinition); - const loadingSignal = signalsService.signal(false) as Signal; - const errorSignal = signalsService.signal(null) as Signal< - string | null - >; - const currentImageSignal = signalsService.signal("") as Signal; +export const cartServiceDefinition = defineService<{ + // State + items: Array<{ + productId: string; + variantId: string; + quantity: number; + isPreOrder?: boolean; + }>; + wishlist: Array<{ + productId: string; + variantId: string; + }>; - return { - setImageIndex: (index: number) => { - // TODO: Implement image index selection logic - }, - resetGallery: () => { - // TODO: Implement gallery reset logic - }, - mapVariantToImage: (variantId: string, index: number) => { - // TODO: Implement variant to image mapping logic - }, - loadImages: (images: string[]) => { - // TODO: Implement image loading logic - }, - currentImage: currentImageSignal, - loading: loadingSignal, - error: errorSignal, - }; -}); + // Getters + totalQuantity: () => number; + itemCount: () => number; + getItem: ( + productId: string, + variantId: string + ) => + | { + productId: string; + variantId: string; + quantity: number; + isPreOrder?: boolean; + } + | undefined; -export const cartServiceDefinition = defineService<{ + // Actions addItem: (productId: string, variantId: string, quantity: number) => void; removeItem: (productId: string, variantId: string) => void; clearCart: () => void; buyNow: (productId: string, variantId: string, quantity: number) => void; toggleWishlist: (productId: string, variantId: string) => void; - totalQuantity: Signal; - itemCount: Signal; - loading: Signal; - error: Signal; }>("cart"); - -export const cartService = implementService.withConfig<{ - initialItems?: any[]; -}>()(cartServiceDefinition, ({ getService, config }) => { - const signalsService = getService(SignalsServiceDefinition); - const loadingSignal = signalsService.signal(false) as Signal; - const errorSignal = signalsService.signal(null) as Signal< - string | null - >; - const totalQuantitySignal = signalsService.signal(0) as Signal; - const itemCountSignal = signalsService.signal(0) as Signal; - - return { - addItem: (productId: string, variantId: string, quantity: number) => { - // TODO: Implement add item logic - }, - removeItem: (productId: string, variantId: string) => { - // TODO: Implement remove item logic - }, - clearCart: () => { - // TODO: Implement clear cart logic - }, - buyNow: (productId: string, variantId: string, quantity: number) => { - // TODO: Implement buy now logic - }, - toggleWishlist: (productId: string, variantId: string) => { - // TODO: Implement wishlist toggle logic - }, - totalQuantity: totalQuantitySignal, - itemCount: itemCountSignal, - loading: loadingSignal, - error: errorSignal, - }; -}); From 846f22fa9a1db9f51be6bad59bddb1bf3ee8008e Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Thu, 29 May 2025 15:36:00 +0300 Subject: [PATCH 15/42] implement --- .../stores/src/services/index.ts | 100 ------------------ .../stores/src/services/new.ts | 99 +++++++++++++++++ 2 files changed, 99 insertions(+), 100 deletions(-) create mode 100644 packages/headless-components/stores/src/services/new.ts diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index a8e91cf65..819503d88 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -41,103 +41,3 @@ export const buynowService = implementService.withConfig<{ error: errorSignal, }; }); - - -export const variantSelectorServiceDefinition = defineService<{ - // State - productId: string; - sku: string; - basePrice: number; - discountPrice?: number; - isOnSale?: boolean; - ribbonLabel?: string; - options: Record; - selectedOptions: Record; - variants: Array<{ - id: string; - label: string; - stock: number; - ribbon?: string; - isPreOrder?: boolean; - }>; - selectedVariantId: string; - quantityAvailable: number; - - // Getters - selectedVariant: () => { - id: string; - label: string; - stock: number; - ribbon?: string; - isPreOrder?: boolean; - }; - finalPrice: () => number; - isLowStock: (threshold?: number) => boolean; - - // Actions - setOption: (group: string, value: string) => void; - selectVariantById: (id: string) => void; - loadProductVariants: ( - data: Array<{ - id: string; - label: string; - stock: number; - ribbon?: string; - isPreOrder?: boolean; - }> - ) => void; - resetSelections: () => void; -}>("variantSelector"); - -export const productGalleryServiceDefinition = defineService<{ - // State - images: string[]; - selectedImageIndex: number; - variantImageMap: Record; - - // Getters - currentImage: () => string; - variantMappedImage: (variantId: string) => string; - - // Actions - setImageIndex: (index: number) => void; - resetGallery: () => void; - mapVariantToImage: (variantId: string, index: number) => void; - loadImages: (images: string[]) => void; -}>("productGallery"); - -export const cartServiceDefinition = defineService<{ - // State - items: Array<{ - productId: string; - variantId: string; - quantity: number; - isPreOrder?: boolean; - }>; - wishlist: Array<{ - productId: string; - variantId: string; - }>; - - // Getters - totalQuantity: () => number; - itemCount: () => number; - getItem: ( - productId: string, - variantId: string - ) => - | { - productId: string; - variantId: string; - quantity: number; - isPreOrder?: boolean; - } - | undefined; - - // Actions - addItem: (productId: string, variantId: string, quantity: number) => void; - removeItem: (productId: string, variantId: string) => void; - clearCart: () => void; - buyNow: (productId: string, variantId: string, quantity: number) => void; - toggleWishlist: (productId: string, variantId: string) => void; -}>("cart"); diff --git a/packages/headless-components/stores/src/services/new.ts b/packages/headless-components/stores/src/services/new.ts new file mode 100644 index 000000000..44afb1b83 --- /dev/null +++ b/packages/headless-components/stores/src/services/new.ts @@ -0,0 +1,99 @@ +import { + defineService, + implementService, + Signal, +} from "@wix/services-definitions"; +import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; + +// VariantSelectorStore +// 🔗 Covers: +// - Product Options: via `options`, `setOption`, `selectedOptions` +// - Product Variants: via `variants`, `selectVariantById`, `selectedVariant` +// - Product discount: via `finalPrice`, `discountPrice`, `isOnSale` +// - SKU: via `sku` +// - Ribbons: via `ribbonLabel` and `variant.ribbon` +// - Low stock message: via `isLowStock()` +// - Pre-order logic: via `variant.isPreOrder` +export interface VariantSelectorStore { + productId: string; + sku: string; + basePrice: number; + discountPrice?: number; + isOnSale?: boolean; + ribbonLabel?: string; + options: Record; + selectedOptions: Record; + variants: { + id: string; + label: string; + stock: number; + ribbon?: string; + isPreOrder?: boolean; + }[]; + selectedVariantId: string; + quantityAvailable: number; + + selectedVariant: () => VariantSelectorStore["variants"][0]; + finalPrice: () => number; + isLowStock: (threshold?: number) => boolean; + + setOption: (group: string, value: string) => void; + selectVariantById: (id: string) => void; + loadProductVariants: (data: VariantSelectorStore["variants"]) => void; + resetSelections: () => void; +} + +// ProductGalleryStore +// 🔗 Covers: +// - Main Product Image: via `currentImage()` +// - Image Gallery: via `images`, `selectedImageIndex`, `loadImages()` +// - Variant display rules: handled via `variantImageMap` and `variantMappedImage()` +export interface ProductGalleryStore { + images: string[]; + selectedImageIndex: number; + variantImageMap: Record; + + currentImage: () => string; + variantMappedImage: (variantId: string) => string; + + setImageIndex: (index: number) => void; + resetGallery: () => void; + mapVariantToImage: (variantId: string, index: number) => void; + loadImages: (images: string[]) => void; +} + +// CurrentCartStore +// 🔗 Covers: +// - Action Buttons: 'Add to Cart' and 'Buy Now' → `addItem()`, `buyNow()` +// - Quantity: tracked in each cart item → `quantity` +// - Pre-order logic: via `isPreOrder` in `items[]` +// - Wishlist: via `wishlist`, `toggleWishlist()` +// - Cart icon summary: via `totalQuantity()`, `itemCount()` +export interface CartItem { + productId: string; + variantId: string; + quantity: number; + isPreOrder?: boolean; +} + +export interface CurrentCartStore { + items: CartItem[]; + wishlist: { productId: string; variantId: string }[]; + + totalQuantity: () => number; + itemCount: () => number; + getItem: (productId: string, variantId: string) => CartItem | undefined; + + addItem: (productId: string, variantId: string, quantity: number) => void; + removeItem: (productId: string, variantId: string) => void; + clearCart: () => void; + buyNow: (productId: string, variantId: string, quantity: number) => void; + toggleWishlist: (productId: string, variantId: string) => void; +} + +// ❌ External logic not covered by these services: +// - Navigation (prev/next product): should be handled via router or page-level service +// - Custom text (e.g., promotional banner): usually CMS or layout-specific state +// - Currency converter: should be implemented as a pricing/currency context service +// - Reviews & Ratings: typically requires async data and its own ReviewsStore +// - Related Products: fetched externally or provided via ProductProvider context From 4f215bf539ecf71c474077795ce69bc3a0f18931 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Thu, 29 May 2025 15:39:02 +0300 Subject: [PATCH 16/42] implement --- .../stores/src/services/new.ts | 111 +++++++++++------- 1 file changed, 66 insertions(+), 45 deletions(-) diff --git a/packages/headless-components/stores/src/services/new.ts b/packages/headless-components/stores/src/services/new.ts index 44afb1b83..1a43dab76 100644 --- a/packages/headless-components/stores/src/services/new.ts +++ b/packages/headless-components/stores/src/services/new.ts @@ -1,11 +1,6 @@ -import { - defineService, - implementService, - Signal, -} from "@wix/services-definitions"; -import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; +import { defineService, Signal } from "@wix/services-definitions"; -// VariantSelectorStore +// VariantSelectorService // 🔗 Covers: // - Product Options: via `options`, `setOption`, `selectedOptions` // - Product Variants: via `variants`, `selectVariantById`, `selectedVariant` @@ -14,44 +9,60 @@ import { SignalsServiceDefinition } from "@wix/services-definitions/core-service // - Ribbons: via `ribbonLabel` and `variant.ribbon` // - Low stock message: via `isLowStock()` // - Pre-order logic: via `variant.isPreOrder` -export interface VariantSelectorStore { - productId: string; - sku: string; - basePrice: number; - discountPrice?: number; - isOnSale?: boolean; - ribbonLabel?: string; - options: Record; - selectedOptions: Record; - variants: { +export const variantSelectorServiceDefinition = defineService<{ + productId: Signal; + sku: Signal; + basePrice: Signal; + discountPrice: Signal; + isOnSale: Signal; + ribbonLabel: Signal; + options: Signal>; + selectedOptions: Signal>; + variants: Signal< + { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }[] + >; + selectedVariantId: Signal; + quantityAvailable: Signal; + + selectedVariant: () => { id: string; label: string; stock: number; - ribbon?: string; - isPreOrder?: boolean; - }[]; - selectedVariantId: string; - quantityAvailable: number; - - selectedVariant: () => VariantSelectorStore["variants"][0]; + ribbon: string | null; + isPreOrder: boolean | null; + }; finalPrice: () => number; isLowStock: (threshold?: number) => boolean; setOption: (group: string, value: string) => void; selectVariantById: (id: string) => void; - loadProductVariants: (data: VariantSelectorStore["variants"]) => void; + loadProductVariants: ( + data: { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }[] + ) => void; resetSelections: () => void; -} +}>("variantSelector"); -// ProductGalleryStore +// ProductGalleryService // 🔗 Covers: // - Main Product Image: via `currentImage()` // - Image Gallery: via `images`, `selectedImageIndex`, `loadImages()` // - Variant display rules: handled via `variantImageMap` and `variantMappedImage()` -export interface ProductGalleryStore { - images: string[]; - selectedImageIndex: number; - variantImageMap: Record; +export const productGalleryServiceDefinition = defineService<{ + images: Signal; + selectedImageIndex: Signal; + variantImageMap: Signal>; currentImage: () => string; variantMappedImage: (variantId: string) => string; @@ -60,36 +71,46 @@ export interface ProductGalleryStore { resetGallery: () => void; mapVariantToImage: (variantId: string, index: number) => void; loadImages: (images: string[]) => void; -} +}>("productGallery"); -// CurrentCartStore +// CurrentCartService // 🔗 Covers: // - Action Buttons: 'Add to Cart' and 'Buy Now' → `addItem()`, `buyNow()` // - Quantity: tracked in each cart item → `quantity` // - Pre-order logic: via `isPreOrder` in `items[]` // - Wishlist: via `wishlist`, `toggleWishlist()` // - Cart icon summary: via `totalQuantity()`, `itemCount()` -export interface CartItem { - productId: string; - variantId: string; - quantity: number; - isPreOrder?: boolean; -} - -export interface CurrentCartStore { - items: CartItem[]; - wishlist: { productId: string; variantId: string }[]; +export const currentCartServiceDefinition = defineService<{ + items: Signal< + { + productId: string; + variantId: string; + quantity: number; + isPreOrder: boolean | null; + }[] + >; + wishlist: Signal<{ productId: string; variantId: string }[]>; totalQuantity: () => number; itemCount: () => number; - getItem: (productId: string, variantId: string) => CartItem | undefined; + getItem: ( + productId: string, + variantId: string + ) => + | { + productId: string; + variantId: string; + quantity: number; + isPreOrder: boolean | null; + } + | undefined; addItem: (productId: string, variantId: string, quantity: number) => void; removeItem: (productId: string, variantId: string) => void; clearCart: () => void; buyNow: (productId: string, variantId: string, quantity: number) => void; toggleWishlist: (productId: string, variantId: string) => void; -} +}>("currentCart"); // ❌ External logic not covered by these services: // - Navigation (prev/next product): should be handled via router or page-level service From d20c43f2e59ac27c89802cd9bb02f312256d23d8 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Thu, 29 May 2025 15:50:24 +0300 Subject: [PATCH 17/42] implement --- .../stores/src/services/new.ts | 81 +++++++++++++------ 1 file changed, 57 insertions(+), 24 deletions(-) diff --git a/packages/headless-components/stores/src/services/new.ts b/packages/headless-components/stores/src/services/new.ts index 1a43dab76..d2b76e55d 100644 --- a/packages/headless-components/stores/src/services/new.ts +++ b/packages/headless-components/stores/src/services/new.ts @@ -1,15 +1,25 @@ +// Signal-based service definitions with detailed logic and widget coverage from spec sheet import { defineService, Signal } from "@wix/services-definitions"; // VariantSelectorService -// 🔗 Covers: -// - Product Options: via `options`, `setOption`, `selectedOptions` -// - Product Variants: via `variants`, `selectVariantById`, `selectedVariant` -// - Product discount: via `finalPrice`, `discountPrice`, `isOnSale` -// - SKU: via `sku` -// - Ribbons: via `ribbonLabel` and `variant.ribbon` -// - Low stock message: via `isLowStock()` -// - Pre-order logic: via `variant.isPreOrder` +// 📄 Covers the following logic from the spec sheet: +// - Product Options: stored in `options`, selected via `setOption`, tracked in `selectedOptions` +// - Product Variants: available in `variants`, current one selected via `selectedVariantId` and `selectVariantById`, accessed using `selectedVariant()` +// - Product discount: calculated using `basePrice`, `discountPrice`, `isOnSale`, and derived `finalPrice()` +// - SKU: managed in `sku` +// - Ribbons: exposed via `ribbonLabel` and also `selectedVariant().ribbon` +// - Low stock message: derived from `selectedVariant().stock` using `isLowStock()` +// - Pre-order logic: indicated by `selectedVariant().isPreOrder` +// 🧩 Covers the following widget elements: +// - Product Options +// - Product Variants +// - Price +// - SKU +// - Ribbon +// - Discount +// - Low Stock Message export const variantSelectorServiceDefinition = defineService<{ + // --- State --- productId: Signal; sku: Signal; basePrice: Signal; @@ -30,6 +40,7 @@ export const variantSelectorServiceDefinition = defineService<{ selectedVariantId: Signal; quantityAvailable: Signal; + // --- Getters --- selectedVariant: () => { id: string; label: string; @@ -40,6 +51,7 @@ export const variantSelectorServiceDefinition = defineService<{ finalPrice: () => number; isLowStock: (threshold?: number) => boolean; + // --- Actions --- setOption: (group: string, value: string) => void; selectVariantById: (id: string) => void; loadProductVariants: ( @@ -55,18 +67,24 @@ export const variantSelectorServiceDefinition = defineService<{ }>("variantSelector"); // ProductGalleryService -// 🔗 Covers: -// - Main Product Image: via `currentImage()` -// - Image Gallery: via `images`, `selectedImageIndex`, `loadImages()` -// - Variant display rules: handled via `variantImageMap` and `variantMappedImage()` +// 📄 Covers the following logic from the spec sheet: +// - Image Gallery: stored in `images`, selected with `setImageIndex()`, reset with `resetGallery()` +// - Main Product Image: resolved using `currentImage()` +// - Variant display rules: mapped via `variantImageMap`, resolved via `variantMappedImage()` +// 🧩 Covers the following widget elements: +// - Main Product Image +// - Image Gallery export const productGalleryServiceDefinition = defineService<{ + // --- State --- images: Signal; selectedImageIndex: Signal; variantImageMap: Signal>; + // --- Getters --- currentImage: () => string; variantMappedImage: (variantId: string) => string; + // --- Actions --- setImageIndex: (index: number) => void; resetGallery: () => void; mapVariantToImage: (variantId: string, index: number) => void; @@ -74,13 +92,20 @@ export const productGalleryServiceDefinition = defineService<{ }>("productGallery"); // CurrentCartService -// 🔗 Covers: -// - Action Buttons: 'Add to Cart' and 'Buy Now' → `addItem()`, `buyNow()` -// - Quantity: tracked in each cart item → `quantity` -// - Pre-order logic: via `isPreOrder` in `items[]` -// - Wishlist: via `wishlist`, `toggleWishlist()` -// - Cart icon summary: via `totalQuantity()`, `itemCount()` +// 📄 Covers the following logic from the spec sheet: +// - Action Buttons: performed via `addItem()` and `buyNow()` +// - Quantity: stored per item in `items[].quantity` +// - Pre-order logic: flagged in `items[].isPreOrder` +// - Wishlist: handled using `wishlist`, `toggleWishlist()` +// - Cart icon summary: shown using `totalQuantity()` and `itemCount()` +// 🧩 Covers the following widget elements: +// - Action Buttons +// - Quantity +// - Pre-order +// - Wishlist +// - Cart Icon export const currentCartServiceDefinition = defineService<{ + // --- State --- items: Signal< { productId: string; @@ -91,6 +116,7 @@ export const currentCartServiceDefinition = defineService<{ >; wishlist: Signal<{ productId: string; variantId: string }[]>; + // --- Getters --- totalQuantity: () => number; itemCount: () => number; getItem: ( @@ -105,6 +131,7 @@ export const currentCartServiceDefinition = defineService<{ } | undefined; + // --- Actions --- addItem: (productId: string, variantId: string, quantity: number) => void; removeItem: (productId: string, variantId: string) => void; clearCart: () => void; @@ -112,9 +139,15 @@ export const currentCartServiceDefinition = defineService<{ toggleWishlist: (productId: string, variantId: string) => void; }>("currentCart"); -// ❌ External logic not covered by these services: -// - Navigation (prev/next product): should be handled via router or page-level service -// - Custom text (e.g., promotional banner): usually CMS or layout-specific state -// - Currency converter: should be implemented as a pricing/currency context service -// - Reviews & Ratings: typically requires async data and its own ReviewsStore -// - Related Products: fetched externally or provided via ProductProvider context +// ❌ Not Covered (out of scope for headless state logic): +// - Related Products → should be handled by ProductContext or external fetch +// - Navigation (prev/next) → should be handled by routing/navigation context +// - Custom Text (promotional) → CMS or layout-bound concern +// - Currency Converter → external pricing or currency service +// - Reviews & Ratings → requires async data and dedicated review service +// 🚫 Missing Widget Elements: +// - Related Products +// - Previous/Next Product Navigation +// - Promotional Banner +// - Currency Switcher +// - Reviews Section From 546446cc200f9d4a1816f375f59bde5547894694 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Thu, 29 May 2025 15:58:33 +0300 Subject: [PATCH 18/42] implement --- .../headless-components/stores/src/services/new.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/headless-components/stores/src/services/new.ts b/packages/headless-components/stores/src/services/new.ts index d2b76e55d..0a07a5bb1 100644 --- a/packages/headless-components/stores/src/services/new.ts +++ b/packages/headless-components/stores/src/services/new.ts @@ -2,6 +2,10 @@ import { defineService, Signal } from "@wix/services-definitions"; // VariantSelectorService +// 🧠 Purpose: Handles the entire product configuration and selection flow. +// Enables users to select from available options (e.g., size, color), resolve the appropriate variant, and retrieve associated data such as SKU, price, availability, ribbons, and stock level. +// Supports pre-order state logic and calculates dynamic pricing based on variant and discount status. +// Core to enabling all other product-related behaviors on the page — gallery, cart, stock messages, and price display rely on this selection state. // 📄 Covers the following logic from the spec sheet: // - Product Options: stored in `options`, selected via `setOption`, tracked in `selectedOptions` // - Product Variants: available in `variants`, current one selected via `selectedVariantId` and `selectVariantById`, accessed using `selectedVariant()` @@ -67,6 +71,9 @@ export const variantSelectorServiceDefinition = defineService<{ }>("variantSelector"); // ProductGalleryService +// 🧠 Purpose: Manages dynamic image gallery behavior, including syncing selected product variant with specific images and allowing user-driven image navigation. +// Enables image selection either manually or programmatically based on variant selection. +// Maintains state of currently displayed image and allows fine-grained control over how variants are visually represented. // 📄 Covers the following logic from the spec sheet: // - Image Gallery: stored in `images`, selected with `setImageIndex()`, reset with `resetGallery()` // - Main Product Image: resolved using `currentImage()` @@ -92,6 +99,9 @@ export const productGalleryServiceDefinition = defineService<{ }>("productGallery"); // CurrentCartService +// 🧠 Purpose: Handles all cart interactions — including item state, quantity management, wishlist toggling, and immediate checkout. +// Tracks pre-order flags and provides derived totals to reflect cart state globally. +// Supports both persistent wishlist behavior and rapid purchasing flows like Buy Now. // 📄 Covers the following logic from the spec sheet: // - Action Buttons: performed via `addItem()` and `buyNow()` // - Quantity: stored per item in `items[].quantity` From 0b7e8bfd409177c483bf9520004468ce8d105f35 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Thu, 29 May 2025 16:04:35 +0300 Subject: [PATCH 19/42] implement --- .../stores/src/services/new.ts | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/headless-components/stores/src/services/new.ts b/packages/headless-components/stores/src/services/new.ts index 0a07a5bb1..1d78d3c84 100644 --- a/packages/headless-components/stores/src/services/new.ts +++ b/packages/headless-components/stores/src/services/new.ts @@ -1,4 +1,3 @@ -// Signal-based service definitions with detailed logic and widget coverage from spec sheet import { defineService, Signal } from "@wix/services-definitions"; // VariantSelectorService @@ -7,13 +6,13 @@ import { defineService, Signal } from "@wix/services-definitions"; // Supports pre-order state logic and calculates dynamic pricing based on variant and discount status. // Core to enabling all other product-related behaviors on the page — gallery, cart, stock messages, and price display rely on this selection state. // 📄 Covers the following logic from the spec sheet: -// - Product Options: stored in `options`, selected via `setOption`, tracked in `selectedOptions` -// - Product Variants: available in `variants`, current one selected via `selectedVariantId` and `selectVariantById`, accessed using `selectedVariant()` -// - Product discount: calculated using `basePrice`, `discountPrice`, `isOnSale`, and derived `finalPrice()` -// - SKU: managed in `sku` -// - Ribbons: exposed via `ribbonLabel` and also `selectedVariant().ribbon` -// - Low stock message: derived from `selectedVariant().stock` using `isLowStock()` -// - Pre-order logic: indicated by `selectedVariant().isPreOrder` +// - Product Options (Must): stored in `options`, selected via `setOption`, tracked in `selectedOptions` +// - Product Variants (Must): available in `variants`, selected via `selectedVariantId` and `selectVariantById`, accessed using `selectedVariant()` +// - Product discount (High): calculated via `basePrice`, `discountPrice`, `isOnSale`, and derived `finalPrice()` +// - SKU (Mid): managed in `sku` +// - Ribbons (Low): exposed via `ribbonLabel` and also `selectedVariant().ribbon` +// - Low stock message (Low): derived from `selectedVariant().stock` using `isLowStock()` +// - Pre-order logic (Mid): indicated by `selectedVariant().isPreOrder` // 🧩 Covers the following widget elements: // - Product Options // - Product Variants @@ -24,14 +23,8 @@ import { defineService, Signal } from "@wix/services-definitions"; // - Low Stock Message export const variantSelectorServiceDefinition = defineService<{ // --- State --- - productId: Signal; - sku: Signal; - basePrice: Signal; - discountPrice: Signal; - isOnSale: Signal; - ribbonLabel: Signal; - options: Signal>; selectedOptions: Signal>; + selectedVariantId: Signal; variants: Signal< { id: string; @@ -41,8 +34,14 @@ export const variantSelectorServiceDefinition = defineService<{ isPreOrder: boolean | null; }[] >; - selectedVariantId: Signal; + options: Signal>; + basePrice: Signal; + discountPrice: Signal; + isOnSale: Signal; quantityAvailable: Signal; + productId: Signal; + sku: Signal; + ribbonLabel: Signal; // --- Getters --- selectedVariant: () => { @@ -75,9 +74,9 @@ export const variantSelectorServiceDefinition = defineService<{ // Enables image selection either manually or programmatically based on variant selection. // Maintains state of currently displayed image and allows fine-grained control over how variants are visually represented. // 📄 Covers the following logic from the spec sheet: -// - Image Gallery: stored in `images`, selected with `setImageIndex()`, reset with `resetGallery()` -// - Main Product Image: resolved using `currentImage()` -// - Variant display rules: mapped via `variantImageMap`, resolved via `variantMappedImage()` +// - Image Gallery (High): stored in `images`, selected with `setImageIndex()`, reset with `resetGallery()` +// - Main Product Image (Must): resolved using `currentImage()` +// - Variant display rules (Mid): mapped via `variantImageMap`, resolved via `variantMappedImage()` // 🧩 Covers the following widget elements: // - Main Product Image // - Image Gallery @@ -92,10 +91,10 @@ export const productGalleryServiceDefinition = defineService<{ variantMappedImage: (variantId: string) => string; // --- Actions --- + loadImages: (images: string[]) => void; setImageIndex: (index: number) => void; resetGallery: () => void; mapVariantToImage: (variantId: string, index: number) => void; - loadImages: (images: string[]) => void; }>("productGallery"); // CurrentCartService @@ -103,11 +102,11 @@ export const productGalleryServiceDefinition = defineService<{ // Tracks pre-order flags and provides derived totals to reflect cart state globally. // Supports both persistent wishlist behavior and rapid purchasing flows like Buy Now. // 📄 Covers the following logic from the spec sheet: -// - Action Buttons: performed via `addItem()` and `buyNow()` -// - Quantity: stored per item in `items[].quantity` -// - Pre-order logic: flagged in `items[].isPreOrder` -// - Wishlist: handled using `wishlist`, `toggleWishlist()` -// - Cart icon summary: shown using `totalQuantity()` and `itemCount()` +// - Action Buttons (Must): performed via `addItem()` and `buyNow()` +// - Quantity (High): stored per item in `items[].quantity` +// - Pre-order logic (Mid): flagged in `items[].isPreOrder` +// - Wishlist (Mid): handled using `wishlist`, `toggleWishlist()` +// - Cart icon summary (Low): shown using `totalQuantity()` and `itemCount()` // 🧩 Covers the following widget elements: // - Action Buttons // - Quantity @@ -143,18 +142,18 @@ export const currentCartServiceDefinition = defineService<{ // --- Actions --- addItem: (productId: string, variantId: string, quantity: number) => void; + buyNow: (productId: string, variantId: string, quantity: number) => void; removeItem: (productId: string, variantId: string) => void; clearCart: () => void; - buyNow: (productId: string, variantId: string, quantity: number) => void; toggleWishlist: (productId: string, variantId: string) => void; }>("currentCart"); // ❌ Not Covered (out of scope for headless state logic): -// - Related Products → should be handled by ProductContext or external fetch -// - Navigation (prev/next) → should be handled by routing/navigation context -// - Custom Text (promotional) → CMS or layout-bound concern -// - Currency Converter → external pricing or currency service -// - Reviews & Ratings → requires async data and dedicated review service +// - Related Products (High) → should be handled by ProductContext or external fetch +// - Navigation (prev/next) (Mid) → should be handled by routing/navigation context +// - Custom Text (promotional) (Low) → CMS or layout-bound concern +// - Currency Converter (High) → external pricing or currency service +// - Reviews & Ratings (High) → requires async data and dedicated review service // 🚫 Missing Widget Elements: // - Related Products // - Previous/Next Product Navigation From ad1dfb58ca30e4cddaf2caf013d6c2474c4bf15e Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Tue, 3 Jun 2025 10:01:46 +0300 Subject: [PATCH 20/42] implement --- .../stores/src/services/new.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/headless-components/stores/src/services/new.ts b/packages/headless-components/stores/src/services/new.ts index 1d78d3c84..226aae274 100644 --- a/packages/headless-components/stores/src/services/new.ts +++ b/packages/headless-components/stores/src/services/new.ts @@ -1,3 +1,4 @@ +// Signal-based service definitions with detailed logic and widget coverage from spec sheet import { defineService, Signal } from "@wix/services-definitions"; // VariantSelectorService @@ -150,13 +151,23 @@ export const currentCartServiceDefinition = defineService<{ // ❌ Not Covered (out of scope for headless state logic): // - Related Products (High) → should be handled by ProductContext or external fetch -// - Navigation (prev/next) (Mid) → should be handled by routing/navigation context +// - Navigation (Mid) → should be handled by routing/navigation context // - Custom Text (promotional) (Low) → CMS or layout-bound concern // - Currency Converter (High) → external pricing or currency service // - Reviews & Ratings (High) → requires async data and dedicated review service +// - Modifiers (Mid) → may require dedicated ModifiersService +// - Category (Low) → layout-bound or metadata-driven +// - Additional Info (Low) → typically CMS or static layout section +// - and stock availability. (uncategorized/likely duplicate) +// - price (uncategorized/likely duplicate) + // 🚫 Missing Widget Elements: // - Related Products // - Previous/Next Product Navigation // - Promotional Banner -// - Currency Switcher +// - Currency Converter // - Reviews Section +// - Modifier +// - Category +// - Additional Info +// - Product Variants (duplicated entry) From 0e84cc8b38c4cee43923230031683b51280b0d7c Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Tue, 3 Jun 2025 11:05:13 +0300 Subject: [PATCH 21/42] implement app --- .../src/components/App.css | 202 ++++++++++++- .../src/components/App.jsx | 272 ++++++++++++++++-- .../stores/src/services/index.ts | 2 + 3 files changed, 445 insertions(+), 31 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.css b/examples/astro-components-demo/src/components/App.css index 13214c57a..abfda5ebd 100644 --- a/examples/astro-components-demo/src/components/App.css +++ b/examples/astro-components-demo/src/components/App.css @@ -1,23 +1,199 @@ -.App { - text-align: center; +/* Reset/clean base styles */ +body, +html, +.product-page-wixstudio { + background: #fff; + color: #222; + font-family: "Inter", Arial, sans-serif; + margin: 0; + padding: 0; } -.App-logo { - height: 40vmin; - pointer-events: none; +.product-page-wixstudio { + max-width: 900px; + margin: 40px auto; + padding: 32px 16px; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.07); } -.App-header { - background-color: #282c34; - min-height: 100vh; +.main-section { + display: flex; + gap: 48px; + align-items: flex-start; +} + +.image-section { + flex: 1 1 340px; display: flex; flex-direction: column; align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; } -.App-link { - color: #61dafb; +.main-image { + width: 340px; + height: 340px; + object-fit: cover; + border-radius: 8px; + border: 1px solid #eee; + margin-bottom: 16px; +} + +.thumbnails { + display: flex; + gap: 8px; +} + +.thumb { + width: 56px; + height: 56px; + object-fit: cover; + border-radius: 4px; + border: 2px solid #eee; + cursor: pointer; + transition: border 0.2s; +} +.thumb.selected { + border: 2px solid #0070f3; +} + +.details-section { + flex: 2 1 400px; + display: flex; + flex-direction: column; + gap: 18px; +} + +.product-title { + font-size: 2.2rem; + font-weight: 700; + margin: 0 0 8px 0; +} +.sku { + color: #888; + font-size: 1rem; + margin-bottom: 8px; +} +.price { + font-size: 2rem; + color: #222; + font-weight: 600; + margin-bottom: 8px; +} +.ribbon { + display: inline-block; + background: #ffe066; + color: #b8860b; + font-weight: 600; + padding: 2px 12px; + border-radius: 12px; + font-size: 1rem; + margin-bottom: 8px; +} +.desc { + font-size: 1.1rem; + color: #444; + margin-bottom: 12px; +} +.options { + display: flex; + gap: 18px; +} +.option-group { + display: flex; + flex-direction: column; + font-size: 1rem; +} +.option-group label { + margin-bottom: 2px; + font-weight: 500; +} +.option-group select { + padding: 4px 8px; + border-radius: 4px; + border: 1px solid #ccc; + font-size: 1rem; +} +.quantity-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} +.qty-input { + width: 60px; + padding: 4px 8px; + font-size: 1rem; + border-radius: 4px; + border: 1px solid #ccc; +} +.actions-row { + display: flex; + gap: 12px; + margin-bottom: 8px; +} +.primary-btn { + background: #0070f3; + color: #fff; + border: none; + border-radius: 6px; + padding: 10px 22px; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} +.primary-btn:disabled { + background: #ccc; + cursor: not-allowed; +} +.wishlist-btn, +.wishlisted-btn { + background: none; + border: 1px solid #eee; + border-radius: 6px; + padding: 10px 18px; + font-size: 1.1rem; + cursor: pointer; + color: #e60023; + font-weight: 600; + transition: background 0.2s, border 0.2s; +} +.wishlisted-btn { + background: #ffe6ea; + border: 1px solid #e60023; +} +.preorder { + color: #0070f3; + font-weight: 600; + margin-top: 6px; +} +.low-stock { + color: #e60023; + font-weight: 600; + margin-top: 6px; +} +.info-sections { + margin-top: 40px; + display: flex; + flex-direction: column; + gap: 28px; +} +.info-block { + background: #fafafa; + border-radius: 8px; + padding: 24px 20px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03); +} +.info-block h2 { + margin: 0 0 10px 0; + font-size: 1.2rem; + color: #222; + font-weight: 700; +} +.info-block p { + margin: 0; + color: #444; + font-size: 1rem; } diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index c79a885dc..ba30abe6b 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -1,25 +1,261 @@ -import logo from "../assets/logo.svg"; +import React, { useState } from "react"; import "./App.css"; +// Import the headless service definitions +import { + variantSelectorServiceDefinition, + productGalleryServiceDefinition, + currentCartServiceDefinition, +} from "@wix/headless-components/stores/src/services"; -function App() { +// --- MOCK DATA (replace with real data/fetch in production) --- +const MOCK_PRODUCT = { + title: "I'm a product", + sku: "364215376135191", + price: 85, + description: + "I'm a product description. I'm a great place to add more details about your product such as sizing, material, care instructions and cleaning instructions.", +}; +const MOCK_VARIANTS = [ + { + id: "v1", + label: "Red / S", + stock: 5, + ribbon: "Best Seller", + isPreOrder: false, + }, + { id: "v2", label: "Red / M", stock: 2, ribbon: null, isPreOrder: false }, + { id: "v3", label: "Blue / S", stock: 0, ribbon: null, isPreOrder: true }, +]; +const MOCK_OPTIONS = { + Color: ["Red", "Blue"], + Size: ["S", "M"], +}; +const MOCK_IMAGES = [ + "https://dummyimage.com/600x600/ff4444/fff&text=Red+S", + "https://dummyimage.com/600x600/ff4444/fff&text=Red+M", + "https://dummyimage.com/600x600/4444ff/fff&text=Blue+S", +]; +const MOCK_VARIANT_IMAGE_MAP = { + v1: 0, + v2: 1, + v3: 2, +}; + +// --- Service Instances (in real app, use context/provider pattern) --- +const variantSelector = variantSelectorServiceDefinition.create(); +const productGallery = productGalleryServiceDefinition.create(); +const currentCart = currentCartServiceDefinition.create(); + +// Load initial data +variantSelector.loadProductVariants(MOCK_VARIANTS); +variantSelector.options.set(MOCK_OPTIONS); +productGallery.loadImages(MOCK_IMAGES); +productGallery.variantImageMap.set(MOCK_VARIANT_IMAGE_MAP); + +function ProductPage() { + // Local state for quantity + const [quantity, setQuantity] = useState(1); + + // --- Variant Selection --- + const options = variantSelector.options.get(); + const selectedOptions = variantSelector.selectedOptions.get(); + const variants = variantSelector.variants.get(); + const selectedVariant = variantSelector.selectedVariant(); + const isLowStock = variantSelector.isLowStock(3); // threshold=3 + const finalPrice = variantSelector.finalPrice() || MOCK_PRODUCT.price; + const basePrice = variantSelector.basePrice.get(); + const discountPrice = variantSelector.discountPrice.get(); + const isOnSale = variantSelector.isOnSale.get(); + const sku = variantSelector.sku.get(); + const ribbon = variantSelector.ribbonLabel.get() || selectedVariant.ribbon; + const isPreOrder = selectedVariant.isPreOrder; + + // --- Gallery --- + const images = productGallery.images.get(); + const selectedImageIndex = productGallery.selectedImageIndex.get(); + const currentImage = productGallery.currentImage(); + // Map variant to image + const handleVariantImage = (variantId) => { + const idx = productGallery.variantMappedImage(variantId); + productGallery.setImageIndex(idx); + }; + + // --- Cart & Wishlist --- + const cartItems = currentCart.items.get(); + const wishlist = currentCart.wishlist.get(); + const inWishlist = wishlist.some( + (w) => + w.productId === variantSelector.productId.get() && + w.variantId === selectedVariant.id + ); + + // --- Handlers --- + const handleOptionChange = (group, value) => { + variantSelector.setOption(group, value); + // Optionally sync image + handleVariantImage(variantSelector.selectedVariant().id); + }; + const handleAddToCart = () => { + currentCart.addItem( + variantSelector.productId.get(), + selectedVariant.id, + quantity + ); + }; + const handleBuyNow = () => { + currentCart.buyNow( + variantSelector.productId.get(), + selectedVariant.id, + quantity + ); + }; + const handleWishlistToggle = () => { + currentCart.toggleWishlist( + variantSelector.productId.get(), + selectedVariant.id + ); + }; + const handleQuantityChange = (e) => { + const val = Math.max( + 1, + Math.min(Number(e.target.value), selectedVariant.stock) + ); + setQuantity(val); + }; + + // --- UI --- return ( -

-
- logo -

- Edit src/components/App.jsx and save to reload. -

- - Learn React - -
+
+
+
+ Product +
+ {images.map((img, idx) => ( + {`thumb-${idx}`} productGallery.setImageIndex(idx)} + /> + ))} +
+
+
+

{MOCK_PRODUCT.title}

+
SKU: {MOCK_PRODUCT.sku}
+
+ {isOnSale && discountPrice ? ( + <> + + ${basePrice} + + + ${discountPrice} + + + ) : ( + ${finalPrice.toFixed(2)} + )} +
+ {ribbon && {ribbon}} +
{MOCK_PRODUCT.description}
+
+ {Object.entries(options).map(([group, values]) => ( +
+ + +
+ ))} +
+
+ + +
+
+ + + +
+ {isPreOrder &&
Pre-order
} + {isLowStock &&
Low stock!
} +
+
+
+
+

PRODUCT INFO

+

+ I'm a product detail. I'm a great place to add more information + about your product such as sizing, material, care and cleaning + instructions. This is also a great space to write what makes this + product special and how your customers can benefit from this item. +

+
+
+

RETURN & REFUND POLICY

+

+ I'm a Return and Refund policy. I'm a great place to let your + customers know what to do in case they are dissatisfied with their + purchase. Having a straightforward refund or exchange policy is a + great way to build trust and reassure your customers that they can + buy with confidence. +

+
+
+

SHIPPING INFO

+

+ I'm a shipping policy. I'm a great place to add more information + about your shipping methods, packaging and cost. Providing + straightforward information about your shipping policy is a great + way to build trust and reassure your customers that they can buy + from you with confidence. +

+
+
); } -export default App; +export default ProductPage; diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index 819503d88..8231c8d57 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -41,3 +41,5 @@ export const buynowService = implementService.withConfig<{ error: errorSignal, }; }); + +export * from "./new"; From 8d6eba6d6332a0316408b261fc807b90c9926c29 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Tue, 3 Jun 2025 11:24:46 +0300 Subject: [PATCH 22/42] implement app --- .../stores/src/services/index.ts | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index 8231c8d57..105bd3611 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -5,6 +5,11 @@ import { } from "@wix/services-definitions"; import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; import { getCheckoutUrlForProduct } from "../utils"; +import { + variantSelectorServiceDefinition, + productGalleryServiceDefinition, + currentCartServiceDefinition, +} from "./new"; export const buynowserviceDefinition = defineService<{ redirectToCheckout: () => Promise; @@ -43,3 +48,111 @@ export const buynowService = implementService.withConfig<{ }); export * from "./new"; + +// --- VariantSelectorService (stub) --- +export const variantSelectorService = implementService.withConfig<{ + productId: string; +}>()(variantSelectorServiceDefinition, ({ getService, config }) => { + const signalsService = getService(SignalsServiceDefinition); + // Use correct types for signals and return a valid variant object + const selectedOptions = signalsService.signal({}) as Signal< + Record + >; + const selectedVariantId = signalsService.signal("") as Signal; + const variants = signalsService.signal< + { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }[] + >([]); + const options = signalsService.signal({}) as Signal>; + const basePrice = signalsService.signal(0) as Signal; + const discountPrice = signalsService.signal(null) as Signal; + const isOnSale = signalsService.signal(false) as Signal; + const quantityAvailable = signalsService.signal(0) as Signal; + const productId = signalsService.signal(config.productId) as Signal; + const sku = signalsService.signal("") as Signal; + const ribbonLabel = signalsService.signal(null) as Signal; + // Return a valid variant object for selectedVariant + const defaultVariant = { + id: "", + label: "", + stock: 0, + ribbon: null, + isPreOrder: null, + }; + return { + selectedOptions, + selectedVariantId, + variants, + options, + basePrice, + discountPrice, + isOnSale, + quantityAvailable, + productId, + sku, + ribbonLabel, + selectedVariant: () => defaultVariant, + finalPrice: () => 0, + isLowStock: () => false, + setOption: () => {}, + selectVariantById: () => {}, + loadProductVariants: () => {}, + resetSelections: () => {}, + }; +}); + +// --- ProductGalleryService (stub) --- +export const productGalleryService = implementService.withConfig<{ + productId: string; +}>()(productGalleryServiceDefinition, ({ getService, config }) => { + const signalsService = getService(SignalsServiceDefinition); + const images = signalsService.signal([]); + const selectedImageIndex = signalsService.signal(0) as Signal; + const variantImageMap = signalsService.signal>({}); + return { + images, + selectedImageIndex, + variantImageMap, + currentImage: () => "", + variantMappedImage: () => "", + loadImages: () => {}, + setImageIndex: () => {}, + resetGallery: () => {}, + mapVariantToImage: () => {}, + }; +}); + +// --- CurrentCartService (stub) --- +export const currentCartService = implementService.withConfig<{ + userId?: string; +}>()(currentCartServiceDefinition, ({ getService, config }) => { + const signalsService = getService(SignalsServiceDefinition); + const items = signalsService.signal< + { + productId: string; + variantId: string; + quantity: number; + isPreOrder: boolean | null; + }[] + >([]); + const wishlist = signalsService.signal< + { productId: string; variantId: string }[] + >([]); + return { + items, + wishlist, + totalQuantity: () => 0, + itemCount: () => 0, + getItem: () => undefined, + addItem: () => {}, + buyNow: () => {}, + removeItem: () => {}, + clearCart: () => {}, + toggleWishlist: () => {}, + }; +}); From fd886d3bdffa4acc96d66143d52ef5a0a783a9fb Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Tue, 3 Jun 2025 11:25:38 +0300 Subject: [PATCH 23/42] implement app --- packages/headless-components/stores/src/services/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index 105bd3611..94a60799e 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -109,7 +109,7 @@ export const variantSelectorService = implementService.withConfig<{ // --- ProductGalleryService (stub) --- export const productGalleryService = implementService.withConfig<{ productId: string; -}>()(productGalleryServiceDefinition, ({ getService, config }) => { +}>()(productGalleryServiceDefinition, ({ getService }) => { const signalsService = getService(SignalsServiceDefinition); const images = signalsService.signal([]); const selectedImageIndex = signalsService.signal(0) as Signal; @@ -130,7 +130,7 @@ export const productGalleryService = implementService.withConfig<{ // --- CurrentCartService (stub) --- export const currentCartService = implementService.withConfig<{ userId?: string; -}>()(currentCartServiceDefinition, ({ getService, config }) => { +}>()(currentCartServiceDefinition, ({ getService }) => { const signalsService = getService(SignalsServiceDefinition); const items = signalsService.signal< { From 31f30e8b778a801fd252a05e25c643f83aee38df Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Tue, 3 Jun 2025 13:17:30 +0300 Subject: [PATCH 24/42] implement app --- .../src/components/App.jsx | 2 +- .../stores/src/services/index.ts | 176 +++++++++++++++++- .../stores/src/services/new.ts | 173 ----------------- 3 files changed, 171 insertions(+), 180 deletions(-) delete mode 100644 packages/headless-components/stores/src/services/new.ts diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index ba30abe6b..cc69cb297 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -5,7 +5,7 @@ import { variantSelectorServiceDefinition, productGalleryServiceDefinition, currentCartServiceDefinition, -} from "@wix/headless-components/stores/src/services"; +} from "@wix/headless-stores/services"; // --- MOCK DATA (replace with real data/fetch in production) --- const MOCK_PRODUCT = { diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index 94a60799e..d48cf0181 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -5,11 +5,6 @@ import { } from "@wix/services-definitions"; import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; import { getCheckoutUrlForProduct } from "../utils"; -import { - variantSelectorServiceDefinition, - productGalleryServiceDefinition, - currentCartServiceDefinition, -} from "./new"; export const buynowserviceDefinition = defineService<{ redirectToCheckout: () => Promise; @@ -47,7 +42,176 @@ export const buynowService = implementService.withConfig<{ }; }); -export * from "./new"; +// VariantSelectorService +// 🧠 Purpose: Handles the entire product configuration and selection flow. +// Enables users to select from available options (e.g., size, color), resolve the appropriate variant, and retrieve associated data such as SKU, price, availability, ribbons, and stock level. +// Supports pre-order state logic and calculates dynamic pricing based on variant and discount status. +// Core to enabling all other product-related behaviors on the page — gallery, cart, stock messages, and price display rely on this selection state. +// 📄 Covers the following logic from the spec sheet: +// - Product Options (Must): stored in `options`, selected via `setOption`, tracked in `selectedOptions` +// - Product Variants (Must): available in `variants`, selected via `selectedVariantId` and `selectVariantById`, accessed using `selectedVariant()` +// - Product discount (High): calculated via `basePrice`, `discountPrice`, `isOnSale`, and derived `finalPrice()` +// - SKU (Mid): managed in `sku` +// - Ribbons (Low): exposed via `ribbonLabel` and also `selectedVariant().ribbon` +// - Low stock message (Low): derived from `selectedVariant().stock` using `isLowStock()` +// - Pre-order logic (Mid): indicated by `selectedVariant().isPreOrder` +// 🧩 Covers the following widget elements: +// - Product Options +// - Product Variants +// - Price +// - SKU +// - Ribbon +// - Discount +// - Low Stock Message +export const variantSelectorServiceDefinition = defineService<{ + // --- State --- + selectedOptions: Signal>; + selectedVariantId: Signal; + variants: Signal< + { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }[] + >; + options: Signal>; + basePrice: Signal; + discountPrice: Signal; + isOnSale: Signal; + quantityAvailable: Signal; + productId: Signal; + sku: Signal; + ribbonLabel: Signal; + + // --- Getters --- + selectedVariant: () => { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }; + finalPrice: () => number; + isLowStock: (threshold?: number) => boolean; + + // --- Actions --- + setOption: (group: string, value: string) => void; + selectVariantById: (id: string) => void; + loadProductVariants: ( + data: { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }[] + ) => void; + resetSelections: () => void; +}>("variantSelector"); + +// ProductGalleryService +// 🧠 Purpose: Manages dynamic image gallery behavior, including syncing selected product variant with specific images and allowing user-driven image navigation. +// Enables image selection either manually or programmatically based on variant selection. +// Maintains state of currently displayed image and allows fine-grained control over how variants are visually represented. +// 📄 Covers the following logic from the spec sheet: +// - Image Gallery (High): stored in `images`, selected with `setImageIndex()`, reset with `resetGallery()` +// - Main Product Image (Must): resolved using `currentImage()` +// - Variant display rules (Mid): mapped via `variantImageMap`, resolved via `variantMappedImage()` +// 🧩 Covers the following widget elements: +// - Main Product Image +// - Image Gallery +export const productGalleryServiceDefinition = defineService<{ + // --- State --- + images: Signal; + selectedImageIndex: Signal; + variantImageMap: Signal>; + + // --- Getters --- + currentImage: () => string; + variantMappedImage: (variantId: string) => string; + + // --- Actions --- + loadImages: (images: string[]) => void; + setImageIndex: (index: number) => void; + resetGallery: () => void; + mapVariantToImage: (variantId: string, index: number) => void; +}>("productGallery"); + +// CurrentCartService +// 🧠 Purpose: Handles all cart interactions — including item state, quantity management, wishlist toggling, and immediate checkout. +// Tracks pre-order flags and provides derived totals to reflect cart state globally. +// Supports both persistent wishlist behavior and rapid purchasing flows like Buy Now. +// 📄 Covers the following logic from the spec sheet: +// - Action Buttons (Must): performed via `addItem()` and `buyNow()` +// - Quantity (High): stored per item in `items[].quantity` +// - Pre-order logic (Mid): flagged in `items[].isPreOrder` +// - Wishlist (Mid): handled using `wishlist`, `toggleWishlist()` +// - Cart icon summary (Low): shown using `totalQuantity()` and `itemCount()` +// 🧩 Covers the following widget elements: +// - Action Buttons +// - Quantity +// - Pre-order +// - Wishlist +// - Cart Icon +export const currentCartServiceDefinition = defineService<{ + // --- State --- + items: Signal< + { + productId: string; + variantId: string; + quantity: number; + isPreOrder: boolean | null; + }[] + >; + wishlist: Signal<{ productId: string; variantId: string }[]>; + + // --- Getters --- + totalQuantity: () => number; + itemCount: () => number; + getItem: ( + productId: string, + variantId: string + ) => + | { + productId: string; + variantId: string; + quantity: number; + isPreOrder: boolean | null; + } + | undefined; + + // --- Actions --- + addItem: (productId: string, variantId: string, quantity: number) => void; + buyNow: (productId: string, variantId: string, quantity: number) => void; + removeItem: (productId: string, variantId: string) => void; + clearCart: () => void; + toggleWishlist: (productId: string, variantId: string) => void; +}>("currentCart"); + +// ❌ Not Covered (out of scope for headless state logic): +// - Related Products (High) → should be handled by ProductContext or external fetch +// - Navigation (Mid) → should be handled by routing/navigation context +// - Custom Text (promotional) (Low) → CMS or layout-bound concern +// - Currency Converter (High) → external pricing or currency service +// - Reviews & Ratings (High) → requires async data and dedicated review service +// - Modifiers (Mid) → may require dedicated ModifiersService +// - Category (Low) → layout-bound or metadata-driven +// - Additional Info (Low) → typically CMS or static layout section +// - and stock availability. (uncategorized/likely duplicate) +// - price (uncategorized/likely duplicate) + +// 🚫 Missing Widget Elements: +// - Related Products +// - Previous/Next Product Navigation +// - Promotional Banner +// - Currency Converter +// - Reviews Section +// - Modifier +// - Category +// - Additional Info +// - Product Variants (duplicated entry) // --- VariantSelectorService (stub) --- export const variantSelectorService = implementService.withConfig<{ diff --git a/packages/headless-components/stores/src/services/new.ts b/packages/headless-components/stores/src/services/new.ts deleted file mode 100644 index 226aae274..000000000 --- a/packages/headless-components/stores/src/services/new.ts +++ /dev/null @@ -1,173 +0,0 @@ -// Signal-based service definitions with detailed logic and widget coverage from spec sheet -import { defineService, Signal } from "@wix/services-definitions"; - -// VariantSelectorService -// 🧠 Purpose: Handles the entire product configuration and selection flow. -// Enables users to select from available options (e.g., size, color), resolve the appropriate variant, and retrieve associated data such as SKU, price, availability, ribbons, and stock level. -// Supports pre-order state logic and calculates dynamic pricing based on variant and discount status. -// Core to enabling all other product-related behaviors on the page — gallery, cart, stock messages, and price display rely on this selection state. -// 📄 Covers the following logic from the spec sheet: -// - Product Options (Must): stored in `options`, selected via `setOption`, tracked in `selectedOptions` -// - Product Variants (Must): available in `variants`, selected via `selectedVariantId` and `selectVariantById`, accessed using `selectedVariant()` -// - Product discount (High): calculated via `basePrice`, `discountPrice`, `isOnSale`, and derived `finalPrice()` -// - SKU (Mid): managed in `sku` -// - Ribbons (Low): exposed via `ribbonLabel` and also `selectedVariant().ribbon` -// - Low stock message (Low): derived from `selectedVariant().stock` using `isLowStock()` -// - Pre-order logic (Mid): indicated by `selectedVariant().isPreOrder` -// 🧩 Covers the following widget elements: -// - Product Options -// - Product Variants -// - Price -// - SKU -// - Ribbon -// - Discount -// - Low Stock Message -export const variantSelectorServiceDefinition = defineService<{ - // --- State --- - selectedOptions: Signal>; - selectedVariantId: Signal; - variants: Signal< - { - id: string; - label: string; - stock: number; - ribbon: string | null; - isPreOrder: boolean | null; - }[] - >; - options: Signal>; - basePrice: Signal; - discountPrice: Signal; - isOnSale: Signal; - quantityAvailable: Signal; - productId: Signal; - sku: Signal; - ribbonLabel: Signal; - - // --- Getters --- - selectedVariant: () => { - id: string; - label: string; - stock: number; - ribbon: string | null; - isPreOrder: boolean | null; - }; - finalPrice: () => number; - isLowStock: (threshold?: number) => boolean; - - // --- Actions --- - setOption: (group: string, value: string) => void; - selectVariantById: (id: string) => void; - loadProductVariants: ( - data: { - id: string; - label: string; - stock: number; - ribbon: string | null; - isPreOrder: boolean | null; - }[] - ) => void; - resetSelections: () => void; -}>("variantSelector"); - -// ProductGalleryService -// 🧠 Purpose: Manages dynamic image gallery behavior, including syncing selected product variant with specific images and allowing user-driven image navigation. -// Enables image selection either manually or programmatically based on variant selection. -// Maintains state of currently displayed image and allows fine-grained control over how variants are visually represented. -// 📄 Covers the following logic from the spec sheet: -// - Image Gallery (High): stored in `images`, selected with `setImageIndex()`, reset with `resetGallery()` -// - Main Product Image (Must): resolved using `currentImage()` -// - Variant display rules (Mid): mapped via `variantImageMap`, resolved via `variantMappedImage()` -// 🧩 Covers the following widget elements: -// - Main Product Image -// - Image Gallery -export const productGalleryServiceDefinition = defineService<{ - // --- State --- - images: Signal; - selectedImageIndex: Signal; - variantImageMap: Signal>; - - // --- Getters --- - currentImage: () => string; - variantMappedImage: (variantId: string) => string; - - // --- Actions --- - loadImages: (images: string[]) => void; - setImageIndex: (index: number) => void; - resetGallery: () => void; - mapVariantToImage: (variantId: string, index: number) => void; -}>("productGallery"); - -// CurrentCartService -// 🧠 Purpose: Handles all cart interactions — including item state, quantity management, wishlist toggling, and immediate checkout. -// Tracks pre-order flags and provides derived totals to reflect cart state globally. -// Supports both persistent wishlist behavior and rapid purchasing flows like Buy Now. -// 📄 Covers the following logic from the spec sheet: -// - Action Buttons (Must): performed via `addItem()` and `buyNow()` -// - Quantity (High): stored per item in `items[].quantity` -// - Pre-order logic (Mid): flagged in `items[].isPreOrder` -// - Wishlist (Mid): handled using `wishlist`, `toggleWishlist()` -// - Cart icon summary (Low): shown using `totalQuantity()` and `itemCount()` -// 🧩 Covers the following widget elements: -// - Action Buttons -// - Quantity -// - Pre-order -// - Wishlist -// - Cart Icon -export const currentCartServiceDefinition = defineService<{ - // --- State --- - items: Signal< - { - productId: string; - variantId: string; - quantity: number; - isPreOrder: boolean | null; - }[] - >; - wishlist: Signal<{ productId: string; variantId: string }[]>; - - // --- Getters --- - totalQuantity: () => number; - itemCount: () => number; - getItem: ( - productId: string, - variantId: string - ) => - | { - productId: string; - variantId: string; - quantity: number; - isPreOrder: boolean | null; - } - | undefined; - - // --- Actions --- - addItem: (productId: string, variantId: string, quantity: number) => void; - buyNow: (productId: string, variantId: string, quantity: number) => void; - removeItem: (productId: string, variantId: string) => void; - clearCart: () => void; - toggleWishlist: (productId: string, variantId: string) => void; -}>("currentCart"); - -// ❌ Not Covered (out of scope for headless state logic): -// - Related Products (High) → should be handled by ProductContext or external fetch -// - Navigation (Mid) → should be handled by routing/navigation context -// - Custom Text (promotional) (Low) → CMS or layout-bound concern -// - Currency Converter (High) → external pricing or currency service -// - Reviews & Ratings (High) → requires async data and dedicated review service -// - Modifiers (Mid) → may require dedicated ModifiersService -// - Category (Low) → layout-bound or metadata-driven -// - Additional Info (Low) → typically CMS or static layout section -// - and stock availability. (uncategorized/likely duplicate) -// - price (uncategorized/likely duplicate) - -// 🚫 Missing Widget Elements: -// - Related Products -// - Previous/Next Product Navigation -// - Promotional Banner -// - Currency Converter -// - Reviews Section -// - Modifier -// - Category -// - Additional Info -// - Product Variants (duplicated entry) From e3db98d873afedfe2bd87d7c2c9ca8d04169cb79 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Tue, 3 Jun 2025 14:10:05 +0300 Subject: [PATCH 25/42] implement app --- .../src/components/App.jsx | 22 ++++++++++++++++--- .../src/components/ui/buy-now.tsx | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index cc69cb297..a0cc2a2ee 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -5,7 +5,14 @@ import { variantSelectorServiceDefinition, productGalleryServiceDefinition, currentCartServiceDefinition, + variantSelectorService, + productGalleryService, + currentCartService, } from "@wix/headless-stores/services"; +import { + createServicesMap, + createServicesManager, +} from "@wix/services-manager"; // --- MOCK DATA (replace with real data/fetch in production) --- const MOCK_PRODUCT = { @@ -42,9 +49,18 @@ const MOCK_VARIANT_IMAGE_MAP = { }; // --- Service Instances (in real app, use context/provider pattern) --- -const variantSelector = variantSelectorServiceDefinition.create(); -const productGallery = productGalleryServiceDefinition.create(); -const currentCart = currentCartServiceDefinition.create(); +const servicesMap = createServicesMap() + .addService(variantSelectorServiceDefinition, variantSelectorService) + .addService(productGalleryServiceDefinition, productGalleryService) + .addService(currentCartServiceDefinition, currentCartService); +const servicesManager = createServicesManager(servicesMap); +const variantSelector = servicesManager.getService( + variantSelectorServiceDefinition +); +const productGallery = servicesManager.getService( + productGalleryServiceDefinition +); +const currentCart = servicesManager.getService(currentCartServiceDefinition); // Load initial data variantSelector.loadProductVariants(MOCK_VARIANTS); diff --git a/examples/astro-components-demo/src/components/ui/buy-now.tsx b/examples/astro-components-demo/src/components/ui/buy-now.tsx index 233668fe2..dd61082e4 100644 --- a/examples/astro-components-demo/src/components/ui/buy-now.tsx +++ b/examples/astro-components-demo/src/components/ui/buy-now.tsx @@ -3,7 +3,7 @@ import type { Signal } from "@wix/services-definitions"; import { withBuyButtonService } from "@wix/headless-stores/astro/BuyNowServiceContext"; export const BuyNow = withBuyButtonService(({ context }) => { - console.log("context", context); + const { loading, error, redirectToCheckout } = context; if ((loading as Signal).get()) return <>Preparing checkout...; From cd489dfcffa205a72121956a250b41586c0f6921 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 08:39:42 +0300 Subject: [PATCH 26/42] implement app --- .../src/pages/index.astro | 7 +- .../stores/src/services/index.ts | 113 ++++++++++++------ 2 files changed, 79 insertions(+), 41 deletions(-) diff --git a/examples/astro-components-demo/src/pages/index.astro b/examples/astro-components-demo/src/pages/index.astro index ee2fcfd2c..7418510f4 100644 --- a/examples/astro-components-demo/src/pages/index.astro +++ b/examples/astro-components-demo/src/pages/index.astro @@ -4,12 +4,11 @@ import App from "../components/App.jsx"; import Layout from "../layouts/Layout.astro"; import { BuyNow } from "../components/ui/buy-now"; import BuyNowService from "@wix/headless-stores/astro/BuyNowService.astro"; - --- - - + + diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index d48cf0181..2bbefab69 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -4,7 +4,6 @@ import { Signal, } from "@wix/services-definitions"; import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; -import { getCheckoutUrlForProduct } from "../utils"; export const buynowserviceDefinition = defineService<{ redirectToCheckout: () => Promise; @@ -15,24 +14,30 @@ export const buynowserviceDefinition = defineService<{ export const buynowService = implementService.withConfig<{ productId: string; variantId: string; -}>()(buynowserviceDefinition, ({ getService, config }) => { +}>()(buynowserviceDefinition, ({ getService }) => { const signalsService = getService(SignalsServiceDefinition); const loadingSignal = signalsService.signal(false) as Signal; const errorSignal = signalsService.signal(null) as Signal< string | null >; + // Mock checkout and redirect logic + return { redirectToCheckout: async () => { loadingSignal.set(true); try { - const checkoutUrl = await getCheckoutUrlForProduct( - config.productId, - config.variantId - ); - window.location.href = checkoutUrl; - } catch (error) { - errorSignal.set(error as string); + // Mock checkout creation + const checkoutResult = { _id: "test-checkout-id" }; + if (!checkoutResult._id) { + throw new Error("Failed to create checkout"); + } + // Mock redirect session creation + const redirectSession = { fullUrl: "http://mocked-redirect-url.com" }; + window.location.href = redirectSession.fullUrl; + } catch (error: any) { + errorSignal.set(error?.message || String(error)); + throw error; } finally { loadingSignal.set(false); } @@ -218,12 +223,28 @@ export const variantSelectorService = implementService.withConfig<{ productId: string; }>()(variantSelectorServiceDefinition, ({ getService, config }) => { const signalsService = getService(SignalsServiceDefinition); - // Use correct types for signals and return a valid variant object - const selectedOptions = signalsService.signal({}) as Signal< - Record - >; - const selectedVariantId = signalsService.signal("") as Signal; - const variants = signalsService.signal< + // Mock product options and variants + const mockOptions = { + color: ["blue", "red"], + size: ["S", "M", "L"], + } as unknown as Record; + const mockVariants: { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }[] = [ + { id: "v1", label: "Blue S", stock: 10, ribbon: null, isPreOrder: false }, + { id: "v2", label: "Red M", stock: 5, ribbon: "Sale", isPreOrder: false }, + { id: "v3", label: "Blue L", stock: 0, ribbon: null, isPreOrder: true }, + ]; + const selectedOptions = signalsService.signal({ + color: "blue", + size: "S", + } as unknown as Record) as Signal>; + const selectedVariantId = signalsService.signal("v1") as Signal; + const variants = signalsService.signal(mockVariants) as Signal< { id: string; label: string; @@ -231,22 +252,31 @@ export const variantSelectorService = implementService.withConfig<{ ribbon: string | null; isPreOrder: boolean | null; }[] - >([]); - const options = signalsService.signal({}) as Signal>; - const basePrice = signalsService.signal(0) as Signal; - const discountPrice = signalsService.signal(null) as Signal; - const isOnSale = signalsService.signal(false) as Signal; - const quantityAvailable = signalsService.signal(0) as Signal; + >; + const options = signalsService.signal(mockOptions) as Signal< + Record + >; + const basePrice = signalsService.signal(100) as Signal; + const discountPrice = signalsService.signal(80) as Signal; + const isOnSale = signalsService.signal(true) as Signal; + const quantityAvailable = signalsService.signal(10) as Signal; const productId = signalsService.signal(config.productId) as Signal; - const sku = signalsService.signal("") as Signal; - const ribbonLabel = signalsService.signal(null) as Signal; - // Return a valid variant object for selectedVariant - const defaultVariant = { - id: "", - label: "", - stock: 0, - ribbon: null, - isPreOrder: null, + const sku = signalsService.signal("SKU123") as Signal; + const ribbonLabel = signalsService.signal("Sale") as Signal; + // Return the selected variant based on selectedVariantId + const selectedVariant = () => { + const vs = variants.get(); + if (!vs || vs.length === 0) + return { id: "", label: "", stock: 0, ribbon: null, isPreOrder: null }; + return ( + vs.find((v) => v.id === selectedVariantId.get()) || { + id: "", + label: "", + stock: 0, + ribbon: null, + isPreOrder: null, + } + ); }; return { selectedOptions, @@ -260,13 +290,22 @@ export const variantSelectorService = implementService.withConfig<{ productId, sku, ribbonLabel, - selectedVariant: () => defaultVariant, - finalPrice: () => 0, - isLowStock: () => false, - setOption: () => {}, - selectVariantById: () => {}, - loadProductVariants: () => {}, - resetSelections: () => {}, + selectedVariant, + finalPrice: () => discountPrice.get() || basePrice.get(), + isLowStock: (threshold = 5) => selectedVariant().stock <= threshold, + setOption: (group: string, value: string) => { + selectedOptions.set({ ...selectedOptions.get(), [group]: value }); + }, + selectVariantById: (id: string) => { + selectedVariantId.set(id); + }, + loadProductVariants: (data) => { + variants.set(data); + }, + resetSelections: () => { + selectedOptions.set({ color: "blue", size: "S" }); + selectedVariantId.set("v1"); + }, }; }); From df216c143cb0d307648ec441ba9e97a60e2cf0e9 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 08:53:15 +0300 Subject: [PATCH 27/42] implement app --- .../src/components/App.jsx | 2 +- .../stores/src/services/index.ts | 182 +++++++++++------- 2 files changed, 111 insertions(+), 73 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index a0cc2a2ee..cccc0d8d1 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -107,7 +107,7 @@ function ProductPage() { // --- Handlers --- const handleOptionChange = (group, value) => { - variantSelector.setOption(group, value); + variantSelector.setOption(group.toLowerCase(), value); // Optionally sync image handleVariantImage(variantSelector.selectedVariant().id); }; diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index 2bbefab69..7aaf2eccb 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -218,33 +218,16 @@ export const currentCartServiceDefinition = defineService<{ // - Additional Info // - Product Variants (duplicated entry) -// --- VariantSelectorService (stub) --- +// --- VariantSelectorService (minimal working logic, type-correct) --- export const variantSelectorService = implementService.withConfig<{ productId: string; }>()(variantSelectorServiceDefinition, ({ getService, config }) => { const signalsService = getService(SignalsServiceDefinition); - // Mock product options and variants - const mockOptions = { + const options = signalsService.signal>({ color: ["blue", "red"], size: ["S", "M", "L"], - } as unknown as Record; - const mockVariants: { - id: string; - label: string; - stock: number; - ribbon: string | null; - isPreOrder: boolean | null; - }[] = [ - { id: "v1", label: "Blue S", stock: 10, ribbon: null, isPreOrder: false }, - { id: "v2", label: "Red M", stock: 5, ribbon: "Sale", isPreOrder: false }, - { id: "v3", label: "Blue L", stock: 0, ribbon: null, isPreOrder: true }, - ]; - const selectedOptions = signalsService.signal({ - color: "blue", - size: "S", - } as unknown as Record) as Signal>; - const selectedVariantId = signalsService.signal("v1") as Signal; - const variants = signalsService.signal(mockVariants) as Signal< + }); + const variants = signalsService.signal< { id: string; label: string; @@ -252,31 +235,28 @@ export const variantSelectorService = implementService.withConfig<{ ribbon: string | null; isPreOrder: boolean | null; }[] - >; - const options = signalsService.signal(mockOptions) as Signal< - Record - >; - const basePrice = signalsService.signal(100) as Signal; - const discountPrice = signalsService.signal(80) as Signal; - const isOnSale = signalsService.signal(true) as Signal; - const quantityAvailable = signalsService.signal(10) as Signal; - const productId = signalsService.signal(config.productId) as Signal; - const sku = signalsService.signal("SKU123") as Signal; - const ribbonLabel = signalsService.signal("Sale") as Signal; - // Return the selected variant based on selectedVariantId + >([ + { id: "v1", label: "Blue S", stock: 10, ribbon: null, isPreOrder: false }, + { id: "v2", label: "Red M", stock: 5, ribbon: "Sale", isPreOrder: false }, + { id: "v3", label: "Blue L", stock: 0, ribbon: null, isPreOrder: true }, + ]); + const selectedOptions = signalsService.signal>({ + color: "blue", + size: "S", + }); + const selectedVariantId = signalsService.signal("v1"); + const basePrice = signalsService.signal(100); + const discountPrice = signalsService.signal(80); + const isOnSale = signalsService.signal(true); + const quantityAvailable = signalsService.signal(10); + const productId = signalsService.signal(config.productId); + const sku = signalsService.signal("SKU123"); + const ribbonLabel = signalsService.signal("Sale"); const selectedVariant = () => { - const vs = variants.get(); - if (!vs || vs.length === 0) - return { id: "", label: "", stock: 0, ribbon: null, isPreOrder: null }; - return ( - vs.find((v) => v.id === selectedVariantId.get()) || { - id: "", - label: "", - stock: 0, - ribbon: null, - isPreOrder: null, - } - ); + const found = variants.get().find((v) => v.id === selectedVariantId.get()); + if (found) return found; + // fallback: return a default variant object + return { id: "", label: "", stock: 0, ribbon: null, isPreOrder: null }; }; return { selectedOptions, @@ -293,15 +273,12 @@ export const variantSelectorService = implementService.withConfig<{ selectedVariant, finalPrice: () => discountPrice.get() || basePrice.get(), isLowStock: (threshold = 5) => selectedVariant().stock <= threshold, - setOption: (group: string, value: string) => { - selectedOptions.set({ ...selectedOptions.get(), [group]: value }); - }, - selectVariantById: (id: string) => { - selectedVariantId.set(id); - }, - loadProductVariants: (data) => { - variants.set(data); + setOption: (group, value) => { + const newOptions = { ...selectedOptions.get(), [group]: value }; + selectedOptions.set(newOptions); }, + selectVariantById: (id) => selectedVariantId.set(id), + loadProductVariants: (data) => variants.set(data), resetSelections: () => { selectedOptions.set({ color: "blue", size: "S" }); selectedVariantId.set("v1"); @@ -309,28 +286,41 @@ export const variantSelectorService = implementService.withConfig<{ }; }); -// --- ProductGalleryService (stub) --- +// --- ProductGalleryService (minimal working logic, type-correct) --- export const productGalleryService = implementService.withConfig<{ productId: string; }>()(productGalleryServiceDefinition, ({ getService }) => { const signalsService = getService(SignalsServiceDefinition); - const images = signalsService.signal([]); - const selectedImageIndex = signalsService.signal(0) as Signal; - const variantImageMap = signalsService.signal>({}); + const images = signalsService.signal([ + "https://dummyimage.com/600x400/000/fff&text=Blue+S", + "https://dummyimage.com/600x400/ff0000/fff&text=Red+M", + "https://dummyimage.com/600x400/0000ff/fff&text=Blue+L", + ]); + const selectedImageIndex = signalsService.signal(0); + const variantImageMap = signalsService.signal>({ + v1: 0, + v2: 1, + v3: 2, + }); return { images, selectedImageIndex, variantImageMap, - currentImage: () => "", - variantMappedImage: () => "", - loadImages: () => {}, - setImageIndex: () => {}, - resetGallery: () => {}, - mapVariantToImage: () => {}, + currentImage: () => images.get()[selectedImageIndex.get()] || "", + variantMappedImage: (variantId) => { + const map = variantImageMap.get(); + const idx = map[variantId as keyof typeof map]; + return typeof idx === "number" ? images.get()[idx] || "" : ""; + }, + loadImages: (imgs) => images.set(imgs), + setImageIndex: (index) => selectedImageIndex.set(index), + resetGallery: () => selectedImageIndex.set(0), + mapVariantToImage: (variantId, index) => + variantImageMap.set({ ...variantImageMap.get(), [variantId]: index }), }; }); -// --- CurrentCartService (stub) --- +// --- CurrentCartService (minimal working logic, type-correct) --- export const currentCartService = implementService.withConfig<{ userId?: string; }>()(currentCartServiceDefinition, ({ getService }) => { @@ -349,13 +339,61 @@ export const currentCartService = implementService.withConfig<{ return { items, wishlist, - totalQuantity: () => 0, - itemCount: () => 0, - getItem: () => undefined, - addItem: () => {}, - buyNow: () => {}, - removeItem: () => {}, - clearCart: () => {}, - toggleWishlist: () => {}, + totalQuantity: () => + items.get().reduce((sum, item) => sum + item.quantity, 0), + itemCount: () => items.get().length, + getItem: (productId, variantId) => + items + .get() + .find( + (item) => item.productId === productId && item.variantId === variantId + ), + addItem: (productId, variantId, quantity) => { + const current = items.get(); + const idx = current.findIndex( + (item) => item.productId === productId && item.variantId === variantId + ); + if (idx !== -1) { + const updated = [...current]; + updated[idx] = { + productId: updated[idx]!.productId, + variantId: updated[idx]!.variantId, + quantity: updated[idx]!.quantity + quantity, + isPreOrder: updated[idx]!.isPreOrder ?? false, + }; + items.set(updated); + } else { + items.set([ + ...current, + { productId, variantId, quantity, isPreOrder: false }, + ]); + } + }, + buyNow: (productId, variantId, quantity) => + items.set([{ productId, variantId, quantity, isPreOrder: false }]), + removeItem: (productId, variantId) => + items.set( + items + .get() + .filter( + (item) => + !(item.productId === productId && item.variantId === variantId) + ) + ), + clearCart: () => items.set([]), + toggleWishlist: (productId, variantId) => { + const current = wishlist.get(); + const idx = current.findIndex( + (item) => item.productId === productId && item.variantId === variantId + ); + if (idx !== -1) + wishlist.set( + current.filter( + (item) => + !(item.productId === productId && item.variantId === variantId) + ) + ); + else wishlist.set([...current, { productId, variantId }]); + }, }; }); From d8e50a7c825633713badf849d63a39fc7d87fa61 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 09:02:52 +0300 Subject: [PATCH 28/42] implement app --- .../src/components/App.jsx | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index cccc0d8d1..2f409d456 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import "./App.css"; +import { useSignals } from "@preact/signals-react/runtime"; // Import the headless service definitions import { variantSelectorServiceDefinition, @@ -34,8 +35,8 @@ const MOCK_VARIANTS = [ { id: "v3", label: "Blue / S", stock: 0, ribbon: null, isPreOrder: true }, ]; const MOCK_OPTIONS = { - Color: ["Red", "Blue"], - Size: ["S", "M"], + color: ["Red", "Blue"], + size: ["S", "M"], }; const MOCK_IMAGES = [ "https://dummyimage.com/600x600/ff4444/fff&text=Red+S", @@ -71,6 +72,7 @@ productGallery.variantImageMap.set(MOCK_VARIANT_IMAGE_MAP); function ProductPage() { // Local state for quantity const [quantity, setQuantity] = useState(1); + useSignals(); // --- Variant Selection --- const options = variantSelector.options.get(); @@ -107,7 +109,7 @@ function ProductPage() { // --- Handlers --- const handleOptionChange = (group, value) => { - variantSelector.setOption(group.toLowerCase(), value); + variantSelector.setOption(group, value); // Optionally sync image handleVariantImage(variantSelector.selectedVariant().id); }; @@ -185,22 +187,25 @@ function ProductPage() { {ribbon && {ribbon}}
{MOCK_PRODUCT.description}
- {Object.entries(options).map(([group, values]) => ( -
- - -
- ))} + {Object.entries(options).map(([group, values]) => { + console.log({ selectedOptions }); + return ( +
+ + +
+ ); + })}
From 8acc9b7464177bb870392deb578d4c0fb0a4182e Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 09:45:06 +0300 Subject: [PATCH 29/42] implement app --- .../astro-components-demo/src/components/App.jsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index 2f409d456..e541e5a8d 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -1,6 +1,7 @@ -import React, { useState } from "react"; +import React from "react"; import "./App.css"; import { useSignals } from "@preact/signals-react/runtime"; +import { signal } from "@preact/signals-react"; // Import the headless service definitions import { variantSelectorServiceDefinition, @@ -69,9 +70,9 @@ variantSelector.options.set(MOCK_OPTIONS); productGallery.loadImages(MOCK_IMAGES); productGallery.variantImageMap.set(MOCK_VARIANT_IMAGE_MAP); +const quantity = signal(1); + function ProductPage() { - // Local state for quantity - const [quantity, setQuantity] = useState(1); useSignals(); // --- Variant Selection --- @@ -117,14 +118,14 @@ function ProductPage() { currentCart.addItem( variantSelector.productId.get(), selectedVariant.id, - quantity + quantity.value ); }; const handleBuyNow = () => { currentCart.buyNow( variantSelector.productId.get(), selectedVariant.id, - quantity + quantity.value ); }; const handleWishlistToggle = () => { @@ -138,7 +139,7 @@ function ProductPage() { 1, Math.min(Number(e.target.value), selectedVariant.stock) ); - setQuantity(val); + quantity.value = val; }; // --- UI --- @@ -213,7 +214,7 @@ function ProductPage() { type="number" min={1} max={selectedVariant.stock} - value={quantity} + value={quantity.value} onChange={handleQuantityChange} className="qty-input" /> From 7e62a2252e869f6ccf69d5045a2c856fbad41c1d Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 11:11:24 +0300 Subject: [PATCH 30/42] implement app --- .../src/components/App.jsx | 19 +++-- .../stores/src/services/index.ts | 22 +---- .../stores/src/services/wishlistService.ts | 81 +++++++++++++++++++ 3 files changed, 95 insertions(+), 27 deletions(-) create mode 100644 packages/headless-components/stores/src/services/wishlistService.ts diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index e541e5a8d..928557beb 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -10,6 +10,8 @@ import { variantSelectorService, productGalleryService, currentCartService, + wishlistServiceDefinition, + wishlistService, } from "@wix/headless-stores/services"; import { createServicesMap, @@ -54,7 +56,8 @@ const MOCK_VARIANT_IMAGE_MAP = { const servicesMap = createServicesMap() .addService(variantSelectorServiceDefinition, variantSelectorService) .addService(productGalleryServiceDefinition, productGalleryService) - .addService(currentCartServiceDefinition, currentCartService); + .addService(currentCartServiceDefinition, currentCartService) + .addService(wishlistServiceDefinition, wishlistService); const servicesManager = createServicesManager(servicesMap); const variantSelector = servicesManager.getService( variantSelectorServiceDefinition @@ -63,6 +66,7 @@ const productGallery = servicesManager.getService( productGalleryServiceDefinition ); const currentCart = servicesManager.getService(currentCartServiceDefinition); +const wishlist = servicesManager.getService(wishlistServiceDefinition); // Load initial data variantSelector.loadProductVariants(MOCK_VARIANTS); @@ -101,11 +105,10 @@ function ProductPage() { // --- Cart & Wishlist --- const cartItems = currentCart.items.get(); - const wishlist = currentCart.wishlist.get(); - const inWishlist = wishlist.some( - (w) => - w.productId === variantSelector.productId.get() && - w.variantId === selectedVariant.id + const wishlistItems = wishlist.wishlist.get(); + const inWishlist = wishlist.isInWishlist( + variantSelector.productId.get(), + selectedVariant.id ); // --- Handlers --- @@ -120,6 +123,8 @@ function ProductPage() { selectedVariant.id, quantity.value ); + // Print current items in cart + console.log("Current cart items:", currentCart.items.get()); }; const handleBuyNow = () => { currentCart.buyNow( @@ -129,7 +134,7 @@ function ProductPage() { ); }; const handleWishlistToggle = () => { - currentCart.toggleWishlist( + wishlist.toggleWishlist( variantSelector.productId.get(), selectedVariant.id ); diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index 7aaf2eccb..83d1bea69 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -170,7 +170,6 @@ export const currentCartServiceDefinition = defineService<{ isPreOrder: boolean | null; }[] >; - wishlist: Signal<{ productId: string; variantId: string }[]>; // --- Getters --- totalQuantity: () => number; @@ -192,7 +191,6 @@ export const currentCartServiceDefinition = defineService<{ buyNow: (productId: string, variantId: string, quantity: number) => void; removeItem: (productId: string, variantId: string) => void; clearCart: () => void; - toggleWishlist: (productId: string, variantId: string) => void; }>("currentCart"); // ❌ Not Covered (out of scope for headless state logic): @@ -333,12 +331,8 @@ export const currentCartService = implementService.withConfig<{ isPreOrder: boolean | null; }[] >([]); - const wishlist = signalsService.signal< - { productId: string; variantId: string }[] - >([]); return { items, - wishlist, totalQuantity: () => items.get().reduce((sum, item) => sum + item.quantity, 0), itemCount: () => items.get().length, @@ -381,19 +375,7 @@ export const currentCartService = implementService.withConfig<{ ) ), clearCart: () => items.set([]), - toggleWishlist: (productId, variantId) => { - const current = wishlist.get(); - const idx = current.findIndex( - (item) => item.productId === productId && item.variantId === variantId - ); - if (idx !== -1) - wishlist.set( - current.filter( - (item) => - !(item.productId === productId && item.variantId === variantId) - ) - ); - else wishlist.set([...current, { productId, variantId }]); - }, }; }); + +export { wishlistService, wishlistServiceDefinition } from "./wishlistService"; diff --git a/packages/headless-components/stores/src/services/wishlistService.ts b/packages/headless-components/stores/src/services/wishlistService.ts new file mode 100644 index 000000000..9b5f7d775 --- /dev/null +++ b/packages/headless-components/stores/src/services/wishlistService.ts @@ -0,0 +1,81 @@ +import { + defineService, + implementService, + Signal, +} from "@wix/services-definitions"; +import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; + +export const wishlistServiceDefinition = defineService<{ + wishlist: Signal<{ productId: string; variantId: string }[]>; + isInWishlist: (productId: string, variantId: string) => boolean; + addToWishlist: (productId: string, variantId: string) => void; + removeFromWishlist: (productId: string, variantId: string) => void; + toggleWishlist: (productId: string, variantId: string) => void; + clearWishlist: () => void; +}>("wishlist"); + +export const wishlistService = implementService.withConfig<{ + userId?: string; +}>()(wishlistServiceDefinition, ({ getService }) => { + const signalsService = getService(SignalsServiceDefinition); + const wishlist = signalsService.signal< + { + productId: string; + variantId: string; + }[] + >([]); + + return { + wishlist, + isInWishlist: (productId, variantId) => + wishlist + .get() + .some( + (item) => item.productId === productId && item.variantId === variantId + ), + addToWishlist: (productId, variantId) => { + if ( + !wishlist + .get() + .some( + (item) => + item.productId === productId && item.variantId === variantId + ) + ) { + wishlist.set([...wishlist.get(), { productId, variantId }]); + } + }, + removeFromWishlist: (productId, variantId) => { + wishlist.set( + wishlist + .get() + .filter( + (item) => + !(item.productId === productId && item.variantId === variantId) + ) + ); + }, + toggleWishlist: (productId, variantId) => { + if ( + wishlist + .get() + .some( + (item) => + item.productId === productId && item.variantId === variantId + ) + ) { + wishlist.set( + wishlist + .get() + .filter( + (item) => + !(item.productId === productId && item.variantId === variantId) + ) + ); + } else { + wishlist.set([...wishlist.get(), { productId, variantId }]); + } + }, + clearWishlist: () => wishlist.set([]), + }; +}); From 16f9060b3b85eb30421bca3510a665ec88f42c0d Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 11:15:16 +0300 Subject: [PATCH 31/42] implement app --- .../stores/src/services/buynowService.ts | 50 +++ .../stores/src/services/currentCartService.ts | 109 +++++ .../stores/src/services/index.ts | 393 +----------------- .../src/services/productGalleryService.ts | 62 +++ .../src/services/variantSelectorService.ts | 136 ++++++ 5 files changed, 370 insertions(+), 380 deletions(-) create mode 100644 packages/headless-components/stores/src/services/buynowService.ts create mode 100644 packages/headless-components/stores/src/services/currentCartService.ts create mode 100644 packages/headless-components/stores/src/services/productGalleryService.ts create mode 100644 packages/headless-components/stores/src/services/variantSelectorService.ts diff --git a/packages/headless-components/stores/src/services/buynowService.ts b/packages/headless-components/stores/src/services/buynowService.ts new file mode 100644 index 000000000..6f83c5741 --- /dev/null +++ b/packages/headless-components/stores/src/services/buynowService.ts @@ -0,0 +1,50 @@ +// BuyNowService +// 🧠 Purpose: Handles the buy now/checkout redirect logic for a single product/variant. +import { + defineService, + implementService, + Signal, +} from "@wix/services-definitions"; +import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; + +export const buynowserviceDefinition = defineService<{ + redirectToCheckout: () => Promise; + loading: Signal; + error: Signal; +}>("buynow"); + +export const buynowService = implementService.withConfig<{ + productId: string; + variantId: string; +}>()(buynowserviceDefinition, ({ getService }) => { + const signalsService = getService(SignalsServiceDefinition); + const loadingSignal = signalsService.signal(false) as Signal; + const errorSignal = signalsService.signal(null) as Signal< + string | null + >; + + // Mock checkout and redirect logic + + return { + redirectToCheckout: async () => { + loadingSignal.set(true); + try { + // Mock checkout creation + const checkoutResult = { _id: "test-checkout-id" }; + if (!checkoutResult._id) { + throw new Error("Failed to create checkout"); + } + // Mock redirect session creation + const redirectSession = { fullUrl: "http://mocked-redirect-url.com" }; + window.location.href = redirectSession.fullUrl; + } catch (error: any) { + errorSignal.set(error?.message || String(error)); + throw error; + } finally { + loadingSignal.set(false); + } + }, + loading: loadingSignal, + error: errorSignal, + }; +}); diff --git a/packages/headless-components/stores/src/services/currentCartService.ts b/packages/headless-components/stores/src/services/currentCartService.ts new file mode 100644 index 000000000..ebc328baf --- /dev/null +++ b/packages/headless-components/stores/src/services/currentCartService.ts @@ -0,0 +1,109 @@ +// CurrentCartService +// 🧠 Purpose: Handles all cart interactions — including item state, quantity management, wishlist toggling, and immediate checkout. +// Tracks pre-order flags and provides derived totals to reflect cart state globally. +// Supports both persistent wishlist behavior and rapid purchasing flows like Buy Now. +// 📄 Covers the following logic from the spec sheet: +// - Action Buttons (Must): performed via `addItem()` and `buyNow()` +// - Quantity (High): stored per item in `items[].quantity` +// - Pre-order logic (Mid): flagged in `items[].isPreOrder` +// - Wishlist (Mid): handled using `wishlist`, `toggleWishlist()` +// - Cart icon summary (Low): shown using `totalQuantity()` and `itemCount()` +// 🧩 Covers the following widget elements: +// - Action Buttons +// - Quantity +// - Pre-order +// - Wishlist +// - Cart Icon +import { + defineService, + implementService, + Signal, +} from "@wix/services-definitions"; +import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; + +export const currentCartServiceDefinition = defineService<{ + items: Signal< + { + productId: string; + variantId: string; + quantity: number; + isPreOrder: boolean | null; + }[] + >; + totalQuantity: () => number; + itemCount: () => number; + getItem: ( + productId: string, + variantId: string + ) => + | { + productId: string; + variantId: string; + quantity: number; + isPreOrder: boolean | null; + } + | undefined; + addItem: (productId: string, variantId: string, quantity: number) => void; + buyNow: (productId: string, variantId: string, quantity: number) => void; + removeItem: (productId: string, variantId: string) => void; + clearCart: () => void; +}>("currentCart"); + +export const currentCartService = implementService.withConfig<{ + userId?: string; +}>()(currentCartServiceDefinition, ({ getService }) => { + const signalsService = getService(SignalsServiceDefinition); + const items = signalsService.signal< + { + productId: string; + variantId: string; + quantity: number; + isPreOrder: boolean | null; + }[] + >([]); + return { + items, + totalQuantity: () => + items.get().reduce((sum, item) => sum + item.quantity, 0), + itemCount: () => items.get().length, + getItem: (productId, variantId) => + items + .get() + .find( + (item) => item.productId === productId && item.variantId === variantId + ), + addItem: (productId, variantId, quantity) => { + const current = items.get(); + const idx = current.findIndex( + (item) => item.productId === productId && item.variantId === variantId + ); + if (idx !== -1) { + const updated = [...current]; + updated[idx] = { + productId: updated[idx]!.productId, + variantId: updated[idx]!.variantId, + quantity: updated[idx]!.quantity + quantity, + isPreOrder: updated[idx]!.isPreOrder ?? false, + }; + items.set(updated); + } else { + items.set([ + ...current, + { productId, variantId, quantity, isPreOrder: false }, + ]); + } + }, + buyNow: (productId, variantId, quantity) => + items.set([{ productId, variantId, quantity, isPreOrder: false }]), + removeItem: (productId, variantId) => + items.set( + items + .get() + .filter( + (item) => + !(item.productId === productId && item.variantId === variantId) + ) + ), + clearCart: () => items.set([]), + }; +}); diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index 83d1bea69..11d02e699 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -1,381 +1,14 @@ -import { - defineService, - implementService, - Signal, -} from "@wix/services-definitions"; -import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; - -export const buynowserviceDefinition = defineService<{ - redirectToCheckout: () => Promise; - loading: Signal; - error: Signal; -}>("buynow"); - -export const buynowService = implementService.withConfig<{ - productId: string; - variantId: string; -}>()(buynowserviceDefinition, ({ getService }) => { - const signalsService = getService(SignalsServiceDefinition); - const loadingSignal = signalsService.signal(false) as Signal; - const errorSignal = signalsService.signal(null) as Signal< - string | null - >; - - // Mock checkout and redirect logic - - return { - redirectToCheckout: async () => { - loadingSignal.set(true); - try { - // Mock checkout creation - const checkoutResult = { _id: "test-checkout-id" }; - if (!checkoutResult._id) { - throw new Error("Failed to create checkout"); - } - // Mock redirect session creation - const redirectSession = { fullUrl: "http://mocked-redirect-url.com" }; - window.location.href = redirectSession.fullUrl; - } catch (error: any) { - errorSignal.set(error?.message || String(error)); - throw error; - } finally { - loadingSignal.set(false); - } - }, - loading: loadingSignal, - error: errorSignal, - }; -}); - -// VariantSelectorService -// 🧠 Purpose: Handles the entire product configuration and selection flow. -// Enables users to select from available options (e.g., size, color), resolve the appropriate variant, and retrieve associated data such as SKU, price, availability, ribbons, and stock level. -// Supports pre-order state logic and calculates dynamic pricing based on variant and discount status. -// Core to enabling all other product-related behaviors on the page — gallery, cart, stock messages, and price display rely on this selection state. -// 📄 Covers the following logic from the spec sheet: -// - Product Options (Must): stored in `options`, selected via `setOption`, tracked in `selectedOptions` -// - Product Variants (Must): available in `variants`, selected via `selectedVariantId` and `selectVariantById`, accessed using `selectedVariant()` -// - Product discount (High): calculated via `basePrice`, `discountPrice`, `isOnSale`, and derived `finalPrice()` -// - SKU (Mid): managed in `sku` -// - Ribbons (Low): exposed via `ribbonLabel` and also `selectedVariant().ribbon` -// - Low stock message (Low): derived from `selectedVariant().stock` using `isLowStock()` -// - Pre-order logic (Mid): indicated by `selectedVariant().isPreOrder` -// 🧩 Covers the following widget elements: -// - Product Options -// - Product Variants -// - Price -// - SKU -// - Ribbon -// - Discount -// - Low Stock Message -export const variantSelectorServiceDefinition = defineService<{ - // --- State --- - selectedOptions: Signal>; - selectedVariantId: Signal; - variants: Signal< - { - id: string; - label: string; - stock: number; - ribbon: string | null; - isPreOrder: boolean | null; - }[] - >; - options: Signal>; - basePrice: Signal; - discountPrice: Signal; - isOnSale: Signal; - quantityAvailable: Signal; - productId: Signal; - sku: Signal; - ribbonLabel: Signal; - - // --- Getters --- - selectedVariant: () => { - id: string; - label: string; - stock: number; - ribbon: string | null; - isPreOrder: boolean | null; - }; - finalPrice: () => number; - isLowStock: (threshold?: number) => boolean; - - // --- Actions --- - setOption: (group: string, value: string) => void; - selectVariantById: (id: string) => void; - loadProductVariants: ( - data: { - id: string; - label: string; - stock: number; - ribbon: string | null; - isPreOrder: boolean | null; - }[] - ) => void; - resetSelections: () => void; -}>("variantSelector"); - -// ProductGalleryService -// 🧠 Purpose: Manages dynamic image gallery behavior, including syncing selected product variant with specific images and allowing user-driven image navigation. -// Enables image selection either manually or programmatically based on variant selection. -// Maintains state of currently displayed image and allows fine-grained control over how variants are visually represented. -// 📄 Covers the following logic from the spec sheet: -// - Image Gallery (High): stored in `images`, selected with `setImageIndex()`, reset with `resetGallery()` -// - Main Product Image (Must): resolved using `currentImage()` -// - Variant display rules (Mid): mapped via `variantImageMap`, resolved via `variantMappedImage()` -// 🧩 Covers the following widget elements: -// - Main Product Image -// - Image Gallery -export const productGalleryServiceDefinition = defineService<{ - // --- State --- - images: Signal; - selectedImageIndex: Signal; - variantImageMap: Signal>; - - // --- Getters --- - currentImage: () => string; - variantMappedImage: (variantId: string) => string; - - // --- Actions --- - loadImages: (images: string[]) => void; - setImageIndex: (index: number) => void; - resetGallery: () => void; - mapVariantToImage: (variantId: string, index: number) => void; -}>("productGallery"); - -// CurrentCartService -// 🧠 Purpose: Handles all cart interactions — including item state, quantity management, wishlist toggling, and immediate checkout. -// Tracks pre-order flags and provides derived totals to reflect cart state globally. -// Supports both persistent wishlist behavior and rapid purchasing flows like Buy Now. -// 📄 Covers the following logic from the spec sheet: -// - Action Buttons (Must): performed via `addItem()` and `buyNow()` -// - Quantity (High): stored per item in `items[].quantity` -// - Pre-order logic (Mid): flagged in `items[].isPreOrder` -// - Wishlist (Mid): handled using `wishlist`, `toggleWishlist()` -// - Cart icon summary (Low): shown using `totalQuantity()` and `itemCount()` -// 🧩 Covers the following widget elements: -// - Action Buttons -// - Quantity -// - Pre-order -// - Wishlist -// - Cart Icon -export const currentCartServiceDefinition = defineService<{ - // --- State --- - items: Signal< - { - productId: string; - variantId: string; - quantity: number; - isPreOrder: boolean | null; - }[] - >; - - // --- Getters --- - totalQuantity: () => number; - itemCount: () => number; - getItem: ( - productId: string, - variantId: string - ) => - | { - productId: string; - variantId: string; - quantity: number; - isPreOrder: boolean | null; - } - | undefined; - - // --- Actions --- - addItem: (productId: string, variantId: string, quantity: number) => void; - buyNow: (productId: string, variantId: string, quantity: number) => void; - removeItem: (productId: string, variantId: string) => void; - clearCart: () => void; -}>("currentCart"); - -// ❌ Not Covered (out of scope for headless state logic): -// - Related Products (High) → should be handled by ProductContext or external fetch -// - Navigation (Mid) → should be handled by routing/navigation context -// - Custom Text (promotional) (Low) → CMS or layout-bound concern -// - Currency Converter (High) → external pricing or currency service -// - Reviews & Ratings (High) → requires async data and dedicated review service -// - Modifiers (Mid) → may require dedicated ModifiersService -// - Category (Low) → layout-bound or metadata-driven -// - Additional Info (Low) → typically CMS or static layout section -// - and stock availability. (uncategorized/likely duplicate) -// - price (uncategorized/likely duplicate) - -// 🚫 Missing Widget Elements: -// - Related Products -// - Previous/Next Product Navigation -// - Promotional Banner -// - Currency Converter -// - Reviews Section -// - Modifier -// - Category -// - Additional Info -// - Product Variants (duplicated entry) - -// --- VariantSelectorService (minimal working logic, type-correct) --- -export const variantSelectorService = implementService.withConfig<{ - productId: string; -}>()(variantSelectorServiceDefinition, ({ getService, config }) => { - const signalsService = getService(SignalsServiceDefinition); - const options = signalsService.signal>({ - color: ["blue", "red"], - size: ["S", "M", "L"], - }); - const variants = signalsService.signal< - { - id: string; - label: string; - stock: number; - ribbon: string | null; - isPreOrder: boolean | null; - }[] - >([ - { id: "v1", label: "Blue S", stock: 10, ribbon: null, isPreOrder: false }, - { id: "v2", label: "Red M", stock: 5, ribbon: "Sale", isPreOrder: false }, - { id: "v3", label: "Blue L", stock: 0, ribbon: null, isPreOrder: true }, - ]); - const selectedOptions = signalsService.signal>({ - color: "blue", - size: "S", - }); - const selectedVariantId = signalsService.signal("v1"); - const basePrice = signalsService.signal(100); - const discountPrice = signalsService.signal(80); - const isOnSale = signalsService.signal(true); - const quantityAvailable = signalsService.signal(10); - const productId = signalsService.signal(config.productId); - const sku = signalsService.signal("SKU123"); - const ribbonLabel = signalsService.signal("Sale"); - const selectedVariant = () => { - const found = variants.get().find((v) => v.id === selectedVariantId.get()); - if (found) return found; - // fallback: return a default variant object - return { id: "", label: "", stock: 0, ribbon: null, isPreOrder: null }; - }; - return { - selectedOptions, - selectedVariantId, - variants, - options, - basePrice, - discountPrice, - isOnSale, - quantityAvailable, - productId, - sku, - ribbonLabel, - selectedVariant, - finalPrice: () => discountPrice.get() || basePrice.get(), - isLowStock: (threshold = 5) => selectedVariant().stock <= threshold, - setOption: (group, value) => { - const newOptions = { ...selectedOptions.get(), [group]: value }; - selectedOptions.set(newOptions); - }, - selectVariantById: (id) => selectedVariantId.set(id), - loadProductVariants: (data) => variants.set(data), - resetSelections: () => { - selectedOptions.set({ color: "blue", size: "S" }); - selectedVariantId.set("v1"); - }, - }; -}); - -// --- ProductGalleryService (minimal working logic, type-correct) --- -export const productGalleryService = implementService.withConfig<{ - productId: string; -}>()(productGalleryServiceDefinition, ({ getService }) => { - const signalsService = getService(SignalsServiceDefinition); - const images = signalsService.signal([ - "https://dummyimage.com/600x400/000/fff&text=Blue+S", - "https://dummyimage.com/600x400/ff0000/fff&text=Red+M", - "https://dummyimage.com/600x400/0000ff/fff&text=Blue+L", - ]); - const selectedImageIndex = signalsService.signal(0); - const variantImageMap = signalsService.signal>({ - v1: 0, - v2: 1, - v3: 2, - }); - return { - images, - selectedImageIndex, - variantImageMap, - currentImage: () => images.get()[selectedImageIndex.get()] || "", - variantMappedImage: (variantId) => { - const map = variantImageMap.get(); - const idx = map[variantId as keyof typeof map]; - return typeof idx === "number" ? images.get()[idx] || "" : ""; - }, - loadImages: (imgs) => images.set(imgs), - setImageIndex: (index) => selectedImageIndex.set(index), - resetGallery: () => selectedImageIndex.set(0), - mapVariantToImage: (variantId, index) => - variantImageMap.set({ ...variantImageMap.get(), [variantId]: index }), - }; -}); - -// --- CurrentCartService (minimal working logic, type-correct) --- -export const currentCartService = implementService.withConfig<{ - userId?: string; -}>()(currentCartServiceDefinition, ({ getService }) => { - const signalsService = getService(SignalsServiceDefinition); - const items = signalsService.signal< - { - productId: string; - variantId: string; - quantity: number; - isPreOrder: boolean | null; - }[] - >([]); - return { - items, - totalQuantity: () => - items.get().reduce((sum, item) => sum + item.quantity, 0), - itemCount: () => items.get().length, - getItem: (productId, variantId) => - items - .get() - .find( - (item) => item.productId === productId && item.variantId === variantId - ), - addItem: (productId, variantId, quantity) => { - const current = items.get(); - const idx = current.findIndex( - (item) => item.productId === productId && item.variantId === variantId - ); - if (idx !== -1) { - const updated = [...current]; - updated[idx] = { - productId: updated[idx]!.productId, - variantId: updated[idx]!.variantId, - quantity: updated[idx]!.quantity + quantity, - isPreOrder: updated[idx]!.isPreOrder ?? false, - }; - items.set(updated); - } else { - items.set([ - ...current, - { productId, variantId, quantity, isPreOrder: false }, - ]); - } - }, - buyNow: (productId, variantId, quantity) => - items.set([{ productId, variantId, quantity, isPreOrder: false }]), - removeItem: (productId, variantId) => - items.set( - items - .get() - .filter( - (item) => - !(item.productId === productId && item.variantId === variantId) - ) - ), - clearCart: () => items.set([]), - }; -}); - +export { buynowserviceDefinition, buynowService } from "./buynowService"; +export { + variantSelectorServiceDefinition, + variantSelectorService, +} from "./variantSelectorService"; +export { + productGalleryServiceDefinition, + productGalleryService, +} from "./productGalleryService"; +export { + currentCartServiceDefinition, + currentCartService, +} from "./currentCartService"; export { wishlistService, wishlistServiceDefinition } from "./wishlistService"; diff --git a/packages/headless-components/stores/src/services/productGalleryService.ts b/packages/headless-components/stores/src/services/productGalleryService.ts new file mode 100644 index 000000000..933b41228 --- /dev/null +++ b/packages/headless-components/stores/src/services/productGalleryService.ts @@ -0,0 +1,62 @@ +// ProductGalleryService +// 🧠 Purpose: Manages dynamic image gallery behavior, including syncing selected product variant with specific images and allowing user-driven image navigation. +// Enables image selection either manually or programmatically based on variant selection. +// Maintains state of currently displayed image and allows fine-grained control over how variants are visually represented. +// 📄 Covers the following logic from the spec sheet: +// - Image Gallery (High): stored in `images`, selected with `setImageIndex()`, reset with `resetGallery()` +// - Main Product Image (Must): resolved using `currentImage()` +// - Variant display rules (Mid): mapped via `variantImageMap`, resolved via `variantMappedImage()` +// 🧩 Covers the following widget elements: +// - Main Product Image +// - Image Gallery +import { + defineService, + implementService, + Signal, +} from "@wix/services-definitions"; +import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; + +export const productGalleryServiceDefinition = defineService<{ + images: Signal; + selectedImageIndex: Signal; + variantImageMap: Signal>; + currentImage: () => string; + variantMappedImage: (variantId: string) => string; + loadImages: (images: string[]) => void; + setImageIndex: (index: number) => void; + resetGallery: () => void; + mapVariantToImage: (variantId: string, index: number) => void; +}>("productGallery"); + +export const productGalleryService = implementService.withConfig<{ + productId: string; +}>()(productGalleryServiceDefinition, ({ getService }) => { + const signalsService = getService(SignalsServiceDefinition); + const images = signalsService.signal([ + "https://dummyimage.com/600x400/000/fff&text=Blue+S", + "https://dummyimage.com/600x400/ff0000/fff&text=Red+M", + "https://dummyimage.com/600x400/0000ff/fff&text=Blue+L", + ]); + const selectedImageIndex = signalsService.signal(0); + const variantImageMap = signalsService.signal>({ + v1: 0, + v2: 1, + v3: 2, + }); + return { + images, + selectedImageIndex, + variantImageMap, + currentImage: () => images.get()[selectedImageIndex.get()] || "", + variantMappedImage: (variantId) => { + const map = variantImageMap.get(); + const idx = map[variantId as keyof typeof map]; + return typeof idx === "number" ? images.get()[idx] || "" : ""; + }, + loadImages: (imgs) => images.set(imgs), + setImageIndex: (index) => selectedImageIndex.set(index), + resetGallery: () => selectedImageIndex.set(0), + mapVariantToImage: (variantId, index) => + variantImageMap.set({ ...variantImageMap.get(), [variantId]: index }), + }; +}); diff --git a/packages/headless-components/stores/src/services/variantSelectorService.ts b/packages/headless-components/stores/src/services/variantSelectorService.ts new file mode 100644 index 000000000..091594f97 --- /dev/null +++ b/packages/headless-components/stores/src/services/variantSelectorService.ts @@ -0,0 +1,136 @@ +// VariantSelectorService +// 🧠 Purpose: Handles the entire product configuration and selection flow. +// Enables users to select from available options (e.g., size, color), resolve the appropriate variant, and retrieve associated data such as SKU, price, availability, ribbons, and stock level. +// Supports pre-order state logic and calculates dynamic pricing based on variant and discount status. +// Core to enabling all other product-related behaviors on the page — gallery, cart, stock messages, and price display rely on this selection state. +// 📄 Covers the following logic from the spec sheet: +// - Product Options (Must): stored in `options`, selected via `setOption`, tracked in `selectedOptions` +// - Product Variants (Must): available in `variants`, selected via `selectedVariantId` and `selectVariantById`, accessed using `selectedVariant()` +// - Product discount (High): calculated via `basePrice`, `discountPrice`, `isOnSale`, and derived `finalPrice()` +// - SKU (Mid): managed in `sku` +// - Ribbons (Low): exposed via `ribbonLabel` and also `selectedVariant().ribbon` +// - Low stock message (Low): derived from `selectedVariant().stock` using `isLowStock()` +// - Pre-order logic (Mid): indicated by `selectedVariant().isPreOrder` +// 🧩 Covers the following widget elements: +// - Product Options +// - Product Variants +// - Price +// - SKU +// - Ribbon +// - Discount +// - Low Stock Message +import { + defineService, + implementService, + Signal, +} from "@wix/services-definitions"; +import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; + +export const variantSelectorServiceDefinition = defineService<{ + selectedOptions: Signal>; + selectedVariantId: Signal; + variants: Signal< + { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }[] + >; + options: Signal>; + basePrice: Signal; + discountPrice: Signal; + isOnSale: Signal; + quantityAvailable: Signal; + productId: Signal; + sku: Signal; + ribbonLabel: Signal; + selectedVariant: () => { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }; + finalPrice: () => number; + isLowStock: (threshold?: number) => boolean; + setOption: (group: string, value: string) => void; + selectVariantById: (id: string) => void; + loadProductVariants: ( + data: { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }[] + ) => void; + resetSelections: () => void; +}>("variantSelector"); + +export const variantSelectorService = implementService.withConfig<{ + productId: string; +}>()(variantSelectorServiceDefinition, ({ getService, config }) => { + const signalsService = getService(SignalsServiceDefinition); + const options = signalsService.signal>({ + color: ["blue", "red"], + size: ["S", "M", "L"], + }); + const variants = signalsService.signal< + { + id: string; + label: string; + stock: number; + ribbon: string | null; + isPreOrder: boolean | null; + }[] + >([ + { id: "v1", label: "Blue S", stock: 10, ribbon: null, isPreOrder: false }, + { id: "v2", label: "Red M", stock: 5, ribbon: "Sale", isPreOrder: false }, + { id: "v3", label: "Blue L", stock: 0, ribbon: null, isPreOrder: true }, + ]); + const selectedOptions = signalsService.signal>({ + color: "blue", + size: "S", + }); + const selectedVariantId = signalsService.signal("v1"); + const basePrice = signalsService.signal(100); + const discountPrice = signalsService.signal(80); + const isOnSale = signalsService.signal(true); + const quantityAvailable = signalsService.signal(10); + const productId = signalsService.signal(config.productId); + const sku = signalsService.signal("SKU123"); + const ribbonLabel = signalsService.signal("Sale"); + const selectedVariant = () => { + const found = variants.get().find((v) => v.id === selectedVariantId.get()); + if (found) return found; + return { id: "", label: "", stock: 0, ribbon: null, isPreOrder: null }; + }; + return { + selectedOptions, + selectedVariantId, + variants, + options, + basePrice, + discountPrice, + isOnSale, + quantityAvailable, + productId, + sku, + ribbonLabel, + selectedVariant, + finalPrice: () => discountPrice.get() || basePrice.get(), + isLowStock: (threshold = 5) => selectedVariant().stock <= threshold, + setOption: (group, value) => { + const newOptions = { ...selectedOptions.get(), [group]: value }; + selectedOptions.set(newOptions); + }, + selectVariantById: (id) => selectedVariantId.set(id), + loadProductVariants: (data) => variants.set(data), + resetSelections: () => { + selectedOptions.set({ color: "blue", size: "S" }); + selectedVariantId.set("v1"); + }, + }; +}); From 1efc38962508eccc89a44b1051ae4e454013d587 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 11:18:26 +0300 Subject: [PATCH 32/42] implement app --- .../stores/src/services/currentCartService.ts | 5 +++++ .../stores/src/services/productGalleryService.ts | 5 +++++ .../stores/src/services/wishlistService.ts | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/packages/headless-components/stores/src/services/currentCartService.ts b/packages/headless-components/stores/src/services/currentCartService.ts index ebc328baf..0a8d8baca 100644 --- a/packages/headless-components/stores/src/services/currentCartService.ts +++ b/packages/headless-components/stores/src/services/currentCartService.ts @@ -22,6 +22,7 @@ import { import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; export const currentCartServiceDefinition = defineService<{ + // --- State --- items: Signal< { productId: string; @@ -30,6 +31,8 @@ export const currentCartServiceDefinition = defineService<{ isPreOrder: boolean | null; }[] >; + + // --- Getters --- totalQuantity: () => number; itemCount: () => number; getItem: ( @@ -43,6 +46,8 @@ export const currentCartServiceDefinition = defineService<{ isPreOrder: boolean | null; } | undefined; + + // --- Actions --- addItem: (productId: string, variantId: string, quantity: number) => void; buyNow: (productId: string, variantId: string, quantity: number) => void; removeItem: (productId: string, variantId: string) => void; diff --git a/packages/headless-components/stores/src/services/productGalleryService.ts b/packages/headless-components/stores/src/services/productGalleryService.ts index 933b41228..3eba38ad0 100644 --- a/packages/headless-components/stores/src/services/productGalleryService.ts +++ b/packages/headless-components/stores/src/services/productGalleryService.ts @@ -17,11 +17,16 @@ import { import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; export const productGalleryServiceDefinition = defineService<{ + // --- State --- images: Signal; selectedImageIndex: Signal; variantImageMap: Signal>; + + // --- Getters --- currentImage: () => string; variantMappedImage: (variantId: string) => string; + + // --- Actions --- loadImages: (images: string[]) => void; setImageIndex: (index: number) => void; resetGallery: () => void; diff --git a/packages/headless-components/stores/src/services/wishlistService.ts b/packages/headless-components/stores/src/services/wishlistService.ts index 9b5f7d775..6bffa6b36 100644 --- a/packages/headless-components/stores/src/services/wishlistService.ts +++ b/packages/headless-components/stores/src/services/wishlistService.ts @@ -6,8 +6,13 @@ import { import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; export const wishlistServiceDefinition = defineService<{ + // --- State --- wishlist: Signal<{ productId: string; variantId: string }[]>; + + // --- Getters --- isInWishlist: (productId: string, variantId: string) => boolean; + + // --- Actions --- addToWishlist: (productId: string, variantId: string) => void; removeFromWishlist: (productId: string, variantId: string) => void; toggleWishlist: (productId: string, variantId: string) => void; From a803ae913236439b4d25c11659af397a8d663205 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 11:29:30 +0300 Subject: [PATCH 33/42] implement app --- .../src/components/App.jsx | 25 +++++++-- .../src/services/variantSelectorService.ts | 56 ++++++++++++++++--- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index 928557beb..ebf16be28 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -29,17 +29,32 @@ const MOCK_PRODUCT = { const MOCK_VARIANTS = [ { id: "v1", - label: "Red / S", + attributes: { color: "blue", size: "S" }, stock: 5, + price: 100, ribbon: "Best Seller", isPreOrder: false, }, - { id: "v2", label: "Red / M", stock: 2, ribbon: null, isPreOrder: false }, - { id: "v3", label: "Blue / S", stock: 0, ribbon: null, isPreOrder: true }, + { + id: "v2", + attributes: { color: "red", size: "M" }, + stock: 2, + price: 110, + ribbon: null, + isPreOrder: false, + }, + { + id: "v3", + attributes: { color: "blue", size: "L" }, + stock: 0, + price: 120, + ribbon: null, + isPreOrder: true, + }, ]; const MOCK_OPTIONS = { - color: ["Red", "Blue"], - size: ["S", "M"], + color: ["blue", "red"], + size: ["S", "M", "L"], }; const MOCK_IMAGES = [ "https://dummyimage.com/600x600/ff4444/fff&text=Red+S", diff --git a/packages/headless-components/stores/src/services/variantSelectorService.ts b/packages/headless-components/stores/src/services/variantSelectorService.ts index 091594f97..9b04c1609 100644 --- a/packages/headless-components/stores/src/services/variantSelectorService.ts +++ b/packages/headless-components/stores/src/services/variantSelectorService.ts @@ -32,8 +32,9 @@ export const variantSelectorServiceDefinition = defineService<{ variants: Signal< { id: string; - label: string; + attributes: Record; stock: number; + price: number; ribbon: string | null; isPreOrder: boolean | null; }[] @@ -48,8 +49,9 @@ export const variantSelectorServiceDefinition = defineService<{ ribbonLabel: Signal; selectedVariant: () => { id: string; - label: string; + attributes: Record; stock: number; + price: number; ribbon: string | null; isPreOrder: boolean | null; }; @@ -60,8 +62,9 @@ export const variantSelectorServiceDefinition = defineService<{ loadProductVariants: ( data: { id: string; - label: string; + attributes: Record; stock: number; + price: number; ribbon: string | null; isPreOrder: boolean | null; }[] @@ -80,15 +83,37 @@ export const variantSelectorService = implementService.withConfig<{ const variants = signalsService.signal< { id: string; - label: string; + attributes: Record; stock: number; + price: number; ribbon: string | null; isPreOrder: boolean | null; }[] >([ - { id: "v1", label: "Blue S", stock: 10, ribbon: null, isPreOrder: false }, - { id: "v2", label: "Red M", stock: 5, ribbon: "Sale", isPreOrder: false }, - { id: "v3", label: "Blue L", stock: 0, ribbon: null, isPreOrder: true }, + { + id: "v1", + attributes: { color: "blue", size: "S" }, + stock: 10, + price: 100, + ribbon: null, + isPreOrder: false, + }, + { + id: "v2", + attributes: { color: "red", size: "M" }, + stock: 5, + price: 110, + ribbon: "Sale", + isPreOrder: false, + }, + { + id: "v3", + attributes: { color: "blue", size: "L" }, + stock: 0, + price: 120, + ribbon: null, + isPreOrder: true, + }, ]); const selectedOptions = signalsService.signal>({ color: "blue", @@ -105,7 +130,14 @@ export const variantSelectorService = implementService.withConfig<{ const selectedVariant = () => { const found = variants.get().find((v) => v.id === selectedVariantId.get()); if (found) return found; - return { id: "", label: "", stock: 0, ribbon: null, isPreOrder: null }; + return { + id: "", + attributes: {}, + stock: 0, + price: 0, + ribbon: null, + isPreOrder: null, + }; }; return { selectedOptions, @@ -125,6 +157,14 @@ export const variantSelectorService = implementService.withConfig<{ setOption: (group, value) => { const newOptions = { ...selectedOptions.get(), [group]: value }; selectedOptions.set(newOptions); + const match = variants + .get() + .find((v) => + Object.entries(newOptions).every( + ([key, val]) => v.attributes[key] === val + ) + ); + if (match) selectedVariantId.set(match.id); }, selectVariantById: (id) => selectedVariantId.set(id), loadProductVariants: (data) => variants.set(data), From 3a7b870d5823e45f20bc43b3d8c49a17108e8cbe Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 11:53:38 +0300 Subject: [PATCH 34/42] implement app --- .../src/components/App.jsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index ebf16be28..9ef709f71 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import "./App.css"; import { useSignals } from "@preact/signals-react/runtime"; import { signal } from "@preact/signals-react"; @@ -110,13 +110,10 @@ function ProductPage() { // --- Gallery --- const images = productGallery.images.get(); - const selectedImageIndex = productGallery.selectedImageIndex.get(); - const currentImage = productGallery.currentImage(); - // Map variant to image - const handleVariantImage = (variantId) => { - const idx = productGallery.variantMappedImage(variantId); - productGallery.setImageIndex(idx); - }; + const variantImageMap = productGallery.variantImageMap.get(); + const mappedIdx = variantImageMap[selectedVariant.id]; + const currentImage = + typeof mappedIdx === "number" ? images[mappedIdx] : images[0]; // --- Cart & Wishlist --- const cartItems = currentCart.items.get(); @@ -129,8 +126,13 @@ function ProductPage() { // --- Handlers --- const handleOptionChange = (group, value) => { variantSelector.setOption(group, value); - // Optionally sync image - handleVariantImage(variantSelector.selectedVariant().id); + // After the variant is updated, update the gallery image to match the new selected variant + const newSelectedVariant = variantSelector.selectedVariant(); + const newMappedIdx = + productGallery.variantImageMap.get()[newSelectedVariant.id]; + if (typeof newMappedIdx === "number") { + productGallery.setImageIndex(newMappedIdx); + } }; const handleAddToCart = () => { currentCart.addItem( @@ -174,10 +176,8 @@ function ProductPage() { key={img} src={img} alt={`thumb-${idx}`} - className={`thumb ${ - idx === selectedImageIndex ? "selected" : "" - }`} - onClick={() => productGallery.setImageIndex(idx)} + className={`thumb ${idx === mappedIdx ? "selected" : ""}`} + style={{ cursor: "default" }} /> ))}
From 462b2248b2a12a227e700402987aa7081cf2a9ee Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 11:55:58 +0300 Subject: [PATCH 35/42] implement app --- .../src/components/App.jsx | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index 9ef709f71..88bea4430 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -37,9 +37,9 @@ const MOCK_VARIANTS = [ }, { id: "v2", - attributes: { color: "red", size: "M" }, - stock: 2, - price: 110, + attributes: { color: "blue", size: "M" }, + stock: 3, + price: 105, ribbon: null, isPreOrder: false, }, @@ -47,24 +47,54 @@ const MOCK_VARIANTS = [ id: "v3", attributes: { color: "blue", size: "L" }, stock: 0, - price: 120, + price: 110, ribbon: null, isPreOrder: true, }, + { + id: "v4", + attributes: { color: "red", size: "S" }, + stock: 4, + price: 100, + ribbon: null, + isPreOrder: false, + }, + { + id: "v5", + attributes: { color: "red", size: "M" }, + stock: 2, + price: 105, + ribbon: null, + isPreOrder: false, + }, + { + id: "v6", + attributes: { color: "red", size: "L" }, + stock: 1, + price: 110, + ribbon: null, + isPreOrder: false, + }, ]; const MOCK_OPTIONS = { color: ["blue", "red"], size: ["S", "M", "L"], }; const MOCK_IMAGES = [ - "https://dummyimage.com/600x600/ff4444/fff&text=Red+S", - "https://dummyimage.com/600x600/ff4444/fff&text=Red+M", - "https://dummyimage.com/600x600/4444ff/fff&text=Blue+S", + "https://dummyimage.com/600x600/0000ff/fff&text=Blue+S", // v1 + "https://dummyimage.com/600x600/0000ff/fff&text=Blue+M", // v2 + "https://dummyimage.com/600x600/0000ff/fff&text=Blue+L", // v3 + "https://dummyimage.com/600x600/ff4444/fff&text=Red+S", // v4 + "https://dummyimage.com/600x600/ff4444/fff&text=Red+M", // v5 + "https://dummyimage.com/600x600/ff4444/fff&text=Red+L", // v6 ]; const MOCK_VARIANT_IMAGE_MAP = { v1: 0, v2: 1, v3: 2, + v4: 3, + v5: 4, + v6: 5, }; // --- Service Instances (in real app, use context/provider pattern) --- From b0de87eec47d6c29861031f2705b3162ffb7484b Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 11:57:02 +0300 Subject: [PATCH 36/42] implement app --- .../src/components/App.jsx | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index 88bea4430..9efc7ebc2 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -201,15 +201,26 @@ function ProductPage() {
Product
- {images.map((img, idx) => ( - {`thumb-${idx}`} - ))} + {images.map((img, idx) => { + // Find the variant id mapped to this image index + const variantId = Object.keys(variantImageMap).find( + (vid) => variantImageMap[vid] === idx + ); + return ( + {`thumb-${idx}`} { + if (variantId) { + variantSelector.selectVariantById(variantId); + } + }} + /> + ); + })}
From d2e7055daa5bd005602c9c0eb23809d25e55bea6 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 14:19:14 +0300 Subject: [PATCH 37/42] implement app --- examples/astro-components-demo/src/components/App.jsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index 9efc7ebc2..5069de7de 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -216,6 +216,15 @@ function ProductPage() { onClick={() => { if (variantId) { variantSelector.selectVariantById(variantId); + // Update selected options (color, size) to match the variant + const variant = variants.find((v) => v.id === variantId); + if (variant && variant.attributes) { + Object.entries(variant.attributes).forEach( + ([group, value]) => { + variantSelector.setOption(group, value); + } + ); + } } }} /> From a5c38e7798875ab308de5f567305e04231023b24 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 15:03:22 +0300 Subject: [PATCH 38/42] implement app --- .../src/components/App.jsx | 122 +++++++++------- .../src/services/variantSelectorService.ts | 137 +++++++----------- 2 files changed, 126 insertions(+), 133 deletions(-) diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index 5069de7de..aa5a0fb1d 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -28,52 +28,70 @@ const MOCK_PRODUCT = { }; const MOCK_VARIANTS = [ { - id: "v1", - attributes: { color: "blue", size: "S" }, - stock: 5, - price: 100, - ribbon: "Best Seller", - isPreOrder: false, + _id: "v1", + visible: true, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "blue" } }, + { optionChoiceNames: { optionName: "size", choiceName: "S" } }, + ], + price: { actualPrice: { amount: "100", formattedAmount: "$100" } }, + inventoryStatus: { inStock: true, preorderEnabled: false }, }, { - id: "v2", - attributes: { color: "blue", size: "M" }, - stock: 3, - price: 105, - ribbon: null, - isPreOrder: false, + _id: "v2", + visible: true, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "blue" } }, + { optionChoiceNames: { optionName: "size", choiceName: "M" } }, + ], + price: { actualPrice: { amount: "105", formattedAmount: "$105" } }, + inventoryStatus: { inStock: true, preorderEnabled: false }, }, { - id: "v3", - attributes: { color: "blue", size: "L" }, - stock: 0, - price: 110, - ribbon: null, - isPreOrder: true, + _id: "v3", + visible: false, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "blue" } }, + { optionChoiceNames: { optionName: "size", choiceName: "L" } }, + ], + price: { actualPrice: { amount: "110", formattedAmount: "$110" } }, + inventoryStatus: { inStock: false, preorderEnabled: true }, }, { - id: "v4", - attributes: { color: "red", size: "S" }, - stock: 4, - price: 100, - ribbon: null, - isPreOrder: false, + _id: "v4", + visible: true, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "red" } }, + { optionChoiceNames: { optionName: "size", choiceName: "S" } }, + ], + price: { actualPrice: { amount: "100", formattedAmount: "$100" } }, + inventoryStatus: { inStock: true, preorderEnabled: false }, }, { - id: "v5", - attributes: { color: "red", size: "M" }, - stock: 2, - price: 105, - ribbon: null, - isPreOrder: false, + _id: "v5", + visible: true, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "red" } }, + { optionChoiceNames: { optionName: "size", choiceName: "M" } }, + ], + price: { actualPrice: { amount: "105", formattedAmount: "$105" } }, + inventoryStatus: { inStock: true, preorderEnabled: false }, }, { - id: "v6", - attributes: { color: "red", size: "L" }, - stock: 1, - price: 110, - ribbon: null, - isPreOrder: false, + _id: "v6", + visible: true, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "red" } }, + { optionChoiceNames: { optionName: "size", choiceName: "L" } }, + ], + price: { actualPrice: { amount: "110", formattedAmount: "$110" } }, + inventoryStatus: { inStock: true, preorderEnabled: false }, }, ]; const MOCK_OPTIONS = { @@ -134,14 +152,14 @@ function ProductPage() { const basePrice = variantSelector.basePrice.get(); const discountPrice = variantSelector.discountPrice.get(); const isOnSale = variantSelector.isOnSale.get(); - const sku = variantSelector.sku.get(); - const ribbon = variantSelector.ribbonLabel.get() || selectedVariant.ribbon; - const isPreOrder = selectedVariant.isPreOrder; + const sku = selectedVariant.sku; + const ribbon = variantSelector.ribbonLabel.get(); + const isPreOrder = selectedVariant.inventoryStatus?.preorderEnabled; // --- Gallery --- const images = productGallery.images.get(); const variantImageMap = productGallery.variantImageMap.get(); - const mappedIdx = variantImageMap[selectedVariant.id]; + const mappedIdx = variantImageMap[selectedVariant._id]; const currentImage = typeof mappedIdx === "number" ? images[mappedIdx] : images[0]; @@ -150,7 +168,7 @@ function ProductPage() { const wishlistItems = wishlist.wishlist.get(); const inWishlist = wishlist.isInWishlist( variantSelector.productId.get(), - selectedVariant.id + selectedVariant._id ); // --- Handlers --- @@ -159,7 +177,7 @@ function ProductPage() { // After the variant is updated, update the gallery image to match the new selected variant const newSelectedVariant = variantSelector.selectedVariant(); const newMappedIdx = - productGallery.variantImageMap.get()[newSelectedVariant.id]; + productGallery.variantImageMap.get()[newSelectedVariant._id]; if (typeof newMappedIdx === "number") { productGallery.setImageIndex(newMappedIdx); } @@ -167,7 +185,7 @@ function ProductPage() { const handleAddToCart = () => { currentCart.addItem( variantSelector.productId.get(), - selectedVariant.id, + selectedVariant._id, quantity.value ); // Print current items in cart @@ -176,21 +194,21 @@ function ProductPage() { const handleBuyNow = () => { currentCart.buyNow( variantSelector.productId.get(), - selectedVariant.id, + selectedVariant._id, quantity.value ); }; const handleWishlistToggle = () => { wishlist.toggleWishlist( variantSelector.productId.get(), - selectedVariant.id + selectedVariant._id ); }; const handleQuantityChange = (e) => { - const val = Math.max( - 1, - Math.min(Number(e.target.value), selectedVariant.stock) - ); + // Find the selected option's stock (if available) + // For demo, fallback to 99 if not found + const maxQty = selectedVariant.inventoryStatus?.inStock ? 99 : 0; + const val = Math.max(1, Math.min(Number(e.target.value), maxQty)); quantity.value = val; }; @@ -283,7 +301,7 @@ function ProductPage() {
-

{MOCK_PRODUCT.title}

+

{MOCK_PRODUCT.name}

SKU: {MOCK_PRODUCT.sku}
{isOnSale && discountPrice ? ( @@ -274,7 +347,7 @@ function ProductPage() { )}
{ribbon && {ribbon}} -
{MOCK_PRODUCT.description}
+
{MOCK_PRODUCT.plainDescription}
{Object.entries(options).map(([group, values]) => { console.log({ selectedOptions }); diff --git a/packages/headless-components/stores/src/services/variantSelectorService.ts b/packages/headless-components/stores/src/services/variantSelectorService.ts index e40c88a58..52ef379d1 100644 --- a/packages/headless-components/stores/src/services/variantSelectorService.ts +++ b/packages/headless-components/stores/src/services/variantSelectorService.ts @@ -26,21 +26,28 @@ import { } from "@wix/services-definitions"; import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals"; +// Add types for Variant and Option +interface OptionChoiceNames { + optionName: string; + choiceName: string; +} +interface Variant { + _id: string; + visible: boolean; + sku: string; + choices: { optionChoiceNames: OptionChoiceNames }[]; + price: { actualPrice: { amount: string; formattedAmount: string } }; + inventoryStatus: { inStock: boolean; preorderEnabled: boolean }; +} +interface Option { + name: string; + choicesSettings: { choices: { name: string; choiceId: string }[] }; +} + export const variantSelectorServiceDefinition = defineService<{ selectedOptions: Signal>; selectedVariantId: Signal; - variants: Signal< - { - _id: string; - visible: boolean; - sku: string; - choices: { - optionChoiceNames: { optionName: string; choiceName: string }; - }[]; - price: { actualPrice: { amount: string; formattedAmount: string } }; - inventoryStatus: { inStock: boolean; preorderEnabled: boolean }; - }[] - >; + variants: Signal; options: Signal>; basePrice: Signal; discountPrice: Signal; @@ -49,12 +56,12 @@ export const variantSelectorServiceDefinition = defineService<{ productId: Signal; sku: Signal; ribbonLabel: Signal; - selectedVariant: () => any; + selectedVariant: () => Variant | undefined; finalPrice: () => number; isLowStock: (threshold?: number) => boolean; setOption: (group: string, value: string) => void; selectVariantById: (id: string) => void; - loadProductVariants: (data: any[]) => void; + loadProductVariants: (data: Variant[]) => void; resetSelections: () => void; }>("variantSelector"); @@ -62,15 +69,9 @@ export const variantSelectorService = implementService.withConfig<{ productId: string; }>()(variantSelectorServiceDefinition, ({ getService, config }) => { const signalsService = getService(SignalsServiceDefinition); - const options = signalsService.signal>({ - color: ["blue", "red"], - size: ["S", "M", "L"], - }); - const variants = signalsService.signal([]); - const selectedOptions = signalsService.signal>({ - color: "blue", - size: "S", - }); + const options = signalsService.signal>({}); + const variants = signalsService.signal([]); + const selectedOptions = signalsService.signal>({}); const selectedVariantId = signalsService.signal(""); const basePrice = signalsService.signal(100); const discountPrice = signalsService.signal(80); @@ -80,14 +81,13 @@ export const variantSelectorService = implementService.withConfig<{ const sku = signalsService.signal("SKU123"); const ribbonLabel = signalsService.signal("Sale"); - // Helper to match variant by selected options function findVariantByOptions(opts: Record) { return variants .get() .find((variant) => Object.entries(opts).every(([group, value]) => variant.choices.some( - (c) => + (c: { optionChoiceNames: OptionChoiceNames }) => c.optionChoiceNames.optionName === group && c.optionChoiceNames.choiceName === value ) @@ -98,10 +98,9 @@ export const variantSelectorService = implementService.withConfig<{ const selectedVariant = () => { const found = variants.get().find((v) => v._id === selectedVariantId.get()); if (found) return found; - // fallback: try to match by selectedOptions const fallback = findVariantByOptions(selectedOptions.get()); if (fallback) return fallback; - return variants.get()[0] || {}; + return variants.get()[0]; }; return { @@ -139,12 +138,17 @@ export const variantSelectorService = implementService.withConfig<{ selectVariantById: (id) => selectedVariantId.set(id), loadProductVariants: (data) => { variants.set(data); - // Set default selected variant if (data.length > 0) selectedVariantId.set(data[0]._id); }, resetSelections: () => { - selectedOptions.set({ color: "blue", size: "S" }); - const match = findVariantByOptions({ color: "blue", size: "S" }); + // Try to set defaults from options + const opts = options.get(); + const defaultOptions: Record = {}; + Object.keys(opts).forEach((key) => { + defaultOptions[key] = opts[key][0]; + }); + selectedOptions.set(defaultOptions); + const match = findVariantByOptions(defaultOptions); if (match) selectedVariantId.set(match._id); }, }; From 37cd7ccd3cf65844ae2fa8cd31de6d593d99638d Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 16:02:20 +0300 Subject: [PATCH 40/42] implement app --- .../src/components/App.jsx | 193 +++--------------- .../stores/src/services/index.ts | 1 + .../stores/src/services/productService.ts | 182 +++++++++++++++++ 3 files changed, 207 insertions(+), 169 deletions(-) create mode 100644 packages/headless-components/stores/src/services/productService.ts diff --git a/examples/astro-components-demo/src/components/App.jsx b/examples/astro-components-demo/src/components/App.jsx index 1c82ba7a9..9165ac1bd 100644 --- a/examples/astro-components-demo/src/components/App.jsx +++ b/examples/astro-components-demo/src/components/App.jsx @@ -12,171 +12,42 @@ import { currentCartService, wishlistServiceDefinition, wishlistService, + productService, + productServiceDefinition, } from "@wix/headless-stores/services"; import { createServicesMap, createServicesManager, } from "@wix/services-manager"; -// --- MOCK DATA (replace with real data/fetch in production) --- -const MOCK_PRODUCT = { - _id: "p1", - name: "I'm a product", - sku: "364215376135191", - handle: "im-a-product", - slug: "im-a-product", - visible: true, - productType: "PHYSICAL", - plainDescription: - "I'm a product description. I'm a great place to add more details about your product such as sizing, material, care instructions and cleaning instructions.", - description: { nodes: [], metadata: {}, documentStyle: {} }, - options: [ - { - name: "color", - choicesSettings: { - choices: [ - { name: "blue", choiceId: "c1" }, - { name: "red", choiceId: "c2" }, - ], - }, - }, - { - name: "size", - choicesSettings: { - choices: [ - { name: "S", choiceId: "s1" }, - { name: "M", choiceId: "s2" }, - { name: "L", choiceId: "s3" }, - ], - }, - }, - ], - variantsInfo: { - variants: [ - { - _id: "v1", - visible: true, - sku: "364215376135191", - choices: [ - { optionChoiceNames: { optionName: "color", choiceName: "blue" } }, - { optionChoiceNames: { optionName: "size", choiceName: "S" } }, - ], - price: { actualPrice: { amount: "100", formattedAmount: "$100" } }, - inventoryStatus: { inStock: true, preorderEnabled: false }, - }, - { - _id: "v2", - visible: true, - sku: "364215376135191", - choices: [ - { optionChoiceNames: { optionName: "color", choiceName: "blue" } }, - { optionChoiceNames: { optionName: "size", choiceName: "M" } }, - ], - price: { actualPrice: { amount: "105", formattedAmount: "$105" } }, - inventoryStatus: { inStock: true, preorderEnabled: false }, - }, - { - _id: "v3", - visible: false, - sku: "364215376135191", - choices: [ - { optionChoiceNames: { optionName: "color", choiceName: "blue" } }, - { optionChoiceNames: { optionName: "size", choiceName: "L" } }, - ], - price: { actualPrice: { amount: "110", formattedAmount: "$110" } }, - inventoryStatus: { inStock: false, preorderEnabled: true }, - }, - { - _id: "v4", - visible: true, - sku: "364215376135191", - choices: [ - { optionChoiceNames: { optionName: "color", choiceName: "red" } }, - { optionChoiceNames: { optionName: "size", choiceName: "S" } }, - ], - price: { actualPrice: { amount: "100", formattedAmount: "$100" } }, - inventoryStatus: { inStock: true, preorderEnabled: false }, - }, - { - _id: "v5", - visible: true, - sku: "364215376135191", - choices: [ - { optionChoiceNames: { optionName: "color", choiceName: "red" } }, - { optionChoiceNames: { optionName: "size", choiceName: "M" } }, - ], - price: { actualPrice: { amount: "105", formattedAmount: "$105" } }, - inventoryStatus: { inStock: true, preorderEnabled: false }, - }, - { - _id: "v6", - visible: true, - sku: "364215376135191", - choices: [ - { optionChoiceNames: { optionName: "color", choiceName: "red" } }, - { optionChoiceNames: { optionName: "size", choiceName: "L" } }, - ], - price: { actualPrice: { amount: "110", formattedAmount: "$110" } }, - inventoryStatus: { inStock: true, preorderEnabled: false }, - }, - ], - }, - media: { - itemsInfo: { - items: [ - { - url: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+S", - image: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+S", - }, - { - url: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+M", - image: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+M", - }, - { - url: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+L", - image: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+L", - }, - { - url: "https://dummyimage.com/600x600/ff4444/fff&text=Red+S", - image: "https://dummyimage.com/600x600/ff4444/fff&text=Red+S", - }, - { - url: "https://dummyimage.com/600x600/ff4444/fff&text=Red+M", - image: "https://dummyimage.com/600x600/ff4444/fff&text=Red+M", - }, - { - url: "https://dummyimage.com/600x600/ff4444/fff&text=Red+L", - image: "https://dummyimage.com/600x600/ff4444/fff&text=Red+L", - }, - ], - }, - }, -}; - -// Use types for Option and Variant -/** - * @typedef {Object} Option - * @property {string} name - * @property {{choices: {name: string, choiceId: string}[]}} choicesSettings - */ -/** - * @typedef {Object} Variant - * @property {string} _id - * @property {boolean} visible - * @property {string} sku - * @property {{optionChoiceNames: {optionName: string, choiceName: string}}[]} choices - * @property {{actualPrice: {amount: string, formattedAmount: string}}} price - * @property {{inStock: boolean, preorderEnabled: boolean}} inventoryStatus - */ +// --- Service Instances (in real app, use context/provider pattern) --- +const servicesMap = createServicesMap() + .addService(variantSelectorServiceDefinition, variantSelectorService) + .addService(productGalleryServiceDefinition, productGalleryService) + .addService(currentCartServiceDefinition, currentCartService) + .addService(wishlistServiceDefinition, wishlistService) + .addService(productServiceDefinition, productService); +const servicesManager = createServicesManager(servicesMap); +const variantSelector = servicesManager.getService( + variantSelectorServiceDefinition +); +const productGallery = servicesManager.getService( + productGalleryServiceDefinition +); +const currentCart = servicesManager.getService(currentCartServiceDefinition); +const wishlist = servicesManager.getService(wishlistServiceDefinition); +const product = servicesManager.getService(productServiceDefinition); -const MOCK_VARIANTS = MOCK_PRODUCT.variantsInfo.variants; +const PRODUCT_DATA = product.getProduct(); +const MOCK_PRODUCT = PRODUCT_DATA; +const MOCK_VARIANTS = PRODUCT_DATA.variantsInfo.variants; const MOCK_OPTIONS = Object.fromEntries( - (MOCK_PRODUCT.options || []).map((opt) => [ + (PRODUCT_DATA.options || []).map((opt) => [ opt.name, (opt.choicesSettings?.choices || []).map((c) => c.name), ]) ); -const MOCK_IMAGES = MOCK_PRODUCT.media.itemsInfo.items.map( +const MOCK_IMAGES = PRODUCT_DATA.media.itemsInfo.items.map( (item) => item.image ); const MOCK_VARIANT_IMAGE_MAP = { @@ -188,22 +59,6 @@ const MOCK_VARIANT_IMAGE_MAP = { v6: 5, }; -// --- Service Instances (in real app, use context/provider pattern) --- -const servicesMap = createServicesMap() - .addService(variantSelectorServiceDefinition, variantSelectorService) - .addService(productGalleryServiceDefinition, productGalleryService) - .addService(currentCartServiceDefinition, currentCartService) - .addService(wishlistServiceDefinition, wishlistService); -const servicesManager = createServicesManager(servicesMap); -const variantSelector = servicesManager.getService( - variantSelectorServiceDefinition -); -const productGallery = servicesManager.getService( - productGalleryServiceDefinition -); -const currentCart = servicesManager.getService(currentCartServiceDefinition); -const wishlist = servicesManager.getService(wishlistServiceDefinition); - // Load initial data variantSelector.loadProductVariants(MOCK_VARIANTS); variantSelector.options.set(MOCK_OPTIONS); diff --git a/packages/headless-components/stores/src/services/index.ts b/packages/headless-components/stores/src/services/index.ts index 11d02e699..060619be1 100644 --- a/packages/headless-components/stores/src/services/index.ts +++ b/packages/headless-components/stores/src/services/index.ts @@ -12,3 +12,4 @@ export { currentCartService, } from "./currentCartService"; export { wishlistService, wishlistServiceDefinition } from "./wishlistService"; +export { productService, productServiceDefinition } from "./productService"; diff --git a/packages/headless-components/stores/src/services/productService.ts b/packages/headless-components/stores/src/services/productService.ts new file mode 100644 index 000000000..6d4c55d44 --- /dev/null +++ b/packages/headless-components/stores/src/services/productService.ts @@ -0,0 +1,182 @@ +import { + defineService, + implementService, + Signal, +} from "@wix/services-definitions"; + +// Types (reuse from variantSelectorService or App.jsx) +interface OptionChoiceNames { + optionName: string; + choiceName: string; +} +interface Variant { + _id: string; + visible: boolean; + sku: string; + choices: { optionChoiceNames: OptionChoiceNames }[]; + price: { actualPrice: { amount: string; formattedAmount: string } }; + inventoryStatus: { inStock: boolean; preorderEnabled: boolean }; +} +interface Option { + name: string; + choicesSettings: { choices: { name: string; choiceId: string }[] }; +} +interface Product { + _id: string; + name: string; + sku: string; + handle: string; + slug: string; + visible: boolean; + productType: string; + plainDescription: string; + description: any; + options: Option[]; + variantsInfo: { variants: Variant[] }; + media: { itemsInfo: { items: { url: string; image: string }[] } }; +} + +// Mock product data (copy from App.jsx) +const MOCK_PRODUCT: Product = { + _id: "p1", + name: "I'm a product", + sku: "364215376135191", + handle: "im-a-product", + slug: "im-a-product", + visible: true, + productType: "PHYSICAL", + plainDescription: + "I'm a product description. I'm a great place to add more details about your product such as sizing, material, care instructions and cleaning instructions.", + description: { nodes: [], metadata: {}, documentStyle: {} }, + options: [ + { + name: "color", + choicesSettings: { + choices: [ + { name: "blue", choiceId: "c1" }, + { name: "red", choiceId: "c2" }, + ], + }, + }, + { + name: "size", + choicesSettings: { + choices: [ + { name: "S", choiceId: "s1" }, + { name: "M", choiceId: "s2" }, + { name: "L", choiceId: "s3" }, + ], + }, + }, + ], + variantsInfo: { + variants: [ + { + _id: "v1", + visible: true, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "blue" } }, + { optionChoiceNames: { optionName: "size", choiceName: "S" } }, + ], + price: { actualPrice: { amount: "100", formattedAmount: "$100" } }, + inventoryStatus: { inStock: true, preorderEnabled: false }, + }, + { + _id: "v2", + visible: true, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "blue" } }, + { optionChoiceNames: { optionName: "size", choiceName: "M" } }, + ], + price: { actualPrice: { amount: "105", formattedAmount: "$105" } }, + inventoryStatus: { inStock: true, preorderEnabled: false }, + }, + { + _id: "v3", + visible: false, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "blue" } }, + { optionChoiceNames: { optionName: "size", choiceName: "L" } }, + ], + price: { actualPrice: { amount: "110", formattedAmount: "$110" } }, + inventoryStatus: { inStock: false, preorderEnabled: true }, + }, + { + _id: "v4", + visible: true, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "red" } }, + { optionChoiceNames: { optionName: "size", choiceName: "S" } }, + ], + price: { actualPrice: { amount: "100", formattedAmount: "$100" } }, + inventoryStatus: { inStock: true, preorderEnabled: false }, + }, + { + _id: "v5", + visible: true, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "red" } }, + { optionChoiceNames: { optionName: "size", choiceName: "M" } }, + ], + price: { actualPrice: { amount: "105", formattedAmount: "$105" } }, + inventoryStatus: { inStock: true, preorderEnabled: false }, + }, + { + _id: "v6", + visible: true, + sku: "364215376135191", + choices: [ + { optionChoiceNames: { optionName: "color", choiceName: "red" } }, + { optionChoiceNames: { optionName: "size", choiceName: "L" } }, + ], + price: { actualPrice: { amount: "110", formattedAmount: "$110" } }, + inventoryStatus: { inStock: true, preorderEnabled: false }, + }, + ], + }, + media: { + itemsInfo: { + items: [ + { + url: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+S", + image: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+S", + }, + { + url: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+M", + image: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+M", + }, + { + url: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+L", + image: "https://dummyimage.com/600x600/0000ff/fff&text=Blue+L", + }, + { + url: "https://dummyimage.com/600x600/ff4444/fff&text=Red+S", + image: "https://dummyimage.com/600x600/ff4444/fff&text=Red+S", + }, + { + url: "https://dummyimage.com/600x600/ff4444/fff&text=Red+M", + image: "https://dummyimage.com/600x600/ff4444/fff&text=Red+M", + }, + { + url: "https://dummyimage.com/600x600/ff4444/fff&text=Red+L", + image: "https://dummyimage.com/600x600/ff4444/fff&text=Red+L", + }, + ], + }, + }, +}; + +export const productServiceDefinition = defineService<{ + getProduct: () => Product; +}>("productService"); + +export const productService = implementService(productServiceDefinition, () => { + return { + getProduct: () => MOCK_PRODUCT, + }; +}); From 07927f730a04525059e7c433058339341a34b572 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 16:13:08 +0300 Subject: [PATCH 41/42] implement app --- .../stores/src/services/productGalleryService.ts | 2 +- .../stores/src/services/variantSelectorService.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/headless-components/stores/src/services/productGalleryService.ts b/packages/headless-components/stores/src/services/productGalleryService.ts index 3eba38ad0..e2f12619d 100644 --- a/packages/headless-components/stores/src/services/productGalleryService.ts +++ b/packages/headless-components/stores/src/services/productGalleryService.ts @@ -25,7 +25,7 @@ export const productGalleryServiceDefinition = defineService<{ // --- Getters --- currentImage: () => string; variantMappedImage: (variantId: string) => string; - + // --- Actions --- loadImages: (images: string[]) => void; setImageIndex: (index: number) => void; diff --git a/packages/headless-components/stores/src/services/variantSelectorService.ts b/packages/headless-components/stores/src/services/variantSelectorService.ts index 52ef379d1..046526716 100644 --- a/packages/headless-components/stores/src/services/variantSelectorService.ts +++ b/packages/headless-components/stores/src/services/variantSelectorService.ts @@ -45,6 +45,7 @@ interface Option { } export const variantSelectorServiceDefinition = defineService<{ + // --- State --- selectedOptions: Signal>; selectedVariantId: Signal; variants: Signal; @@ -56,9 +57,11 @@ export const variantSelectorServiceDefinition = defineService<{ productId: Signal; sku: Signal; ribbonLabel: Signal; + // --- Getters --- selectedVariant: () => Variant | undefined; finalPrice: () => number; isLowStock: (threshold?: number) => boolean; + // --- Actions --- setOption: (group: string, value: string) => void; selectVariantById: (id: string) => void; loadProductVariants: (data: Variant[]) => void; From 0644b03111db475133fed2be02b4fe1bc0da7e54 Mon Sep 17 00:00:00 2001 From: Nitay Neeman Date: Wed, 4 Jun 2025 16:13:16 +0300 Subject: [PATCH 42/42] implement app --- .../stores/src/services/productGalleryService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/headless-components/stores/src/services/productGalleryService.ts b/packages/headless-components/stores/src/services/productGalleryService.ts index e2f12619d..3eba38ad0 100644 --- a/packages/headless-components/stores/src/services/productGalleryService.ts +++ b/packages/headless-components/stores/src/services/productGalleryService.ts @@ -25,7 +25,7 @@ export const productGalleryServiceDefinition = defineService<{ // --- Getters --- currentImage: () => string; variantMappedImage: (variantId: string) => string; - + // --- Actions --- loadImages: (images: string[]) => void; setImageIndex: (index: number) => void;