@wix/headless-stores 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/astro/BuyNowServiceContext.d.ts +2 -0
- package/dist/astro/BuyNowServiceContext.js +6 -0
- package/dist/astro/ManagerProviderContext.d.ts +2 -0
- package/dist/astro/ManagerProviderContext.js +7 -0
- package/dist/astro/withBuyButtonService.d.ts +2 -0
- package/dist/astro/withBuyButtonService.js +16 -0
- package/dist/react/BuyNow.d.ts +51 -0
- package/dist/react/BuyNow.js +39 -0
- package/dist/react/CurrentCartServiceProvider.d.ts +5 -0
- package/dist/react/CurrentCartServiceProvider.js +12 -0
- package/dist/react/VariantSelectorServiceProvider.d.ts +7 -0
- package/dist/react/VariantSelectorServiceProvider.js +22 -0
- package/dist/react/hookim/index.d.ts +5 -0
- package/dist/react/hookim/index.js +22 -0
- package/dist/react/index.js +1 -0
- package/dist/services/CurrentCartService.d.ts +18 -0
- package/dist/services/CurrentCartService.js +9 -0
- package/dist/services/VariantSelectorServices.d.ts +8 -0
- package/dist/services/VariantSelectorServices.js +20 -0
- package/dist/services/buy-now-service.d.ts +81 -0
- package/dist/services/buy-now-service.js +58 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +30 -0
- package/package.json +4 -1
- package/src/react/BuyNow.test.tsx +0 -139
- package/src/react/BuyNow.tsx +0 -76
- package/src/services/buy-now-service.ts +0 -100
- package/src/services/index.ts +0 -4
- package/src/utils/index.ts +0 -38
- package/src/vitest.setup.ts +0 -1
- package/tsconfig.json +0 -34
- package/vitest.config.ts +0 -9
- /package/{src/react/index.tsx → dist/react/index.d.ts} +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContext, withContext } from "@wix/headless-components-core";
|
|
2
|
+
globalThis.BuyNowServiceContext ||= createContext();
|
|
3
|
+
export const [BuyNowServiceProvider, getBuyNowServiceProvider] = globalThis.BuyNowServiceContext;
|
|
4
|
+
export const withBuyButtonService = (Component) => {
|
|
5
|
+
return withContext(globalThis.BuyNowServiceContext[1], Component);
|
|
6
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createContext, withContext } from "@wix/headless-components-core";
|
|
2
|
+
globalThis.ManagerProviderContext ||= createContext();
|
|
3
|
+
export const [ManagerProvider, getManagerProvider] = globalThis.ManagerProviderContext;
|
|
4
|
+
export const withManagerProvider = (Component) => {
|
|
5
|
+
console.log("withManagerProvider manager is", globalThis.ManagerProviderContext[1]);
|
|
6
|
+
return withContext(globalThis.ManagerProviderContext[1], Component);
|
|
7
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { withContext } from "@wix/headless-components-core";
|
|
3
|
+
import { getBuyNowServiceProvider } from "@wix/headless-stores/astro/BuyNowServiceContext";
|
|
4
|
+
export const BuyNow = withContext(getBuyNowServiceProvider, ({ context }) => {
|
|
5
|
+
console.log("context", context);
|
|
6
|
+
// @ts-expect-error
|
|
7
|
+
const { loading, error, redirectToCheckout } = context;
|
|
8
|
+
if (loading.get())
|
|
9
|
+
return _jsx(_Fragment, { children: "Preparing checkout..." });
|
|
10
|
+
if (error.get())
|
|
11
|
+
return _jsxs(_Fragment, { children: ["Error: ", error.get()] });
|
|
12
|
+
return _jsx("button", { onClick: redirectToCheckout, className: "bg-blue-500 text-white p-2 rounded-md", children: "Yalla, Buy!" });
|
|
13
|
+
});
|
|
14
|
+
export const withBuyButtonService = (Component) => {
|
|
15
|
+
return withContext(getBuyNowServiceProvider, Component);
|
|
16
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type RedirectToCheckout = () => void;
|
|
2
|
+
/**
|
|
3
|
+
* Props passed to the render function of the BuyNow component
|
|
4
|
+
*/
|
|
5
|
+
export interface BuyNowRenderProps {
|
|
6
|
+
/** Whether the buy now operation is currently loading */
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
/** The name of the product being purchased */
|
|
9
|
+
productName: string;
|
|
10
|
+
/** Function to redirect the user to the checkout page */
|
|
11
|
+
redirectToCheckout: RedirectToCheckout;
|
|
12
|
+
/** The error message if the buy now operation fails */
|
|
13
|
+
error: string | null;
|
|
14
|
+
/** The price of the product being purchased */
|
|
15
|
+
price: string;
|
|
16
|
+
/** The currency of the product being purchased */
|
|
17
|
+
currency: string;
|
|
18
|
+
}
|
|
19
|
+
export type BuyNowChildren = (props: BuyNowRenderProps) => React.ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Props for the BuyNow component
|
|
22
|
+
*/
|
|
23
|
+
export interface BuyNowProps {
|
|
24
|
+
/** Render function that receives buy now state and actions */
|
|
25
|
+
children: BuyNowChildren;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A headless component that provides buy now functionality using the render props pattern.
|
|
29
|
+
*
|
|
30
|
+
* This component manages the state and actions for a "buy now" flow, allowing consumers
|
|
31
|
+
* to render their own UI while accessing the underlying buy now functionality.
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <BuyNow>
|
|
35
|
+
* {({ isLoading, productName, redirectToCheckout, error, price, currency }) => (
|
|
36
|
+
* <div>
|
|
37
|
+
* <h2>{productName}</h2>
|
|
38
|
+
* <p>{price} {currency}</p>
|
|
39
|
+
* {error && <div className="error">{error}</div>}
|
|
40
|
+
* <button
|
|
41
|
+
* onClick={redirectToCheckout}
|
|
42
|
+
* disabled={isLoading}
|
|
43
|
+
* >
|
|
44
|
+
* {isLoading ? 'Processing...' : 'Buy Now'}
|
|
45
|
+
* </button>
|
|
46
|
+
* </div>
|
|
47
|
+
* )}
|
|
48
|
+
* </BuyNow>
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function BuyNow(props: BuyNowProps): import("react").ReactNode;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useService } from "@wix/services-manager-react";
|
|
2
|
+
import { BuyNowServiceDefinition } from "../services/buy-now-service";
|
|
3
|
+
;
|
|
4
|
+
;
|
|
5
|
+
/**
|
|
6
|
+
* A headless component that provides buy now functionality using the render props pattern.
|
|
7
|
+
*
|
|
8
|
+
* This component manages the state and actions for a "buy now" flow, allowing consumers
|
|
9
|
+
* to render their own UI while accessing the underlying buy now functionality.
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <BuyNow>
|
|
13
|
+
* {({ isLoading, productName, redirectToCheckout, error, price, currency }) => (
|
|
14
|
+
* <div>
|
|
15
|
+
* <h2>{productName}</h2>
|
|
16
|
+
* <p>{price} {currency}</p>
|
|
17
|
+
* {error && <div className="error">{error}</div>}
|
|
18
|
+
* <button
|
|
19
|
+
* onClick={redirectToCheckout}
|
|
20
|
+
* disabled={isLoading}
|
|
21
|
+
* >
|
|
22
|
+
* {isLoading ? 'Processing...' : 'Buy Now'}
|
|
23
|
+
* </button>
|
|
24
|
+
* </div>
|
|
25
|
+
* )}
|
|
26
|
+
* </BuyNow>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function BuyNow(props) {
|
|
30
|
+
const { redirectToCheckout, loadingSignal, productName, errorSignal, price, currency, } = useService(BuyNowServiceDefinition);
|
|
31
|
+
return props.children({
|
|
32
|
+
isLoading: loadingSignal.get(),
|
|
33
|
+
error: errorSignal.get(),
|
|
34
|
+
productName: productName,
|
|
35
|
+
redirectToCheckout,
|
|
36
|
+
price,
|
|
37
|
+
currency,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { CurrentCartService } from "../services/CurrentCartService";
|
|
4
|
+
export const CurrentCartServiceContext = React.createContext(null);
|
|
5
|
+
export function CurrentCartServiceProvider(props) {
|
|
6
|
+
const theService = CurrentCartService({
|
|
7
|
+
// @ts-expect-error
|
|
8
|
+
getService: () => { },
|
|
9
|
+
config: {},
|
|
10
|
+
});
|
|
11
|
+
return (_jsx(CurrentCartServiceContext.Provider, { value: theService, children: props.children }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { productsV3 } from "@wix/stores";
|
|
3
|
+
export declare const VariantSelectorContext: React.Context<unknown>;
|
|
4
|
+
export declare function VariantSelectorServiceProvider(props: {
|
|
5
|
+
product: productsV3.V3Product;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useContext } from "react";
|
|
3
|
+
import { VariantSelectorService } from "../services/VariantSelectorServices";
|
|
4
|
+
import { CurrentCartServiceContext } from "./CurrentCartServiceProvider";
|
|
5
|
+
import { CurrentCartServiceDefinition } from "../services/CurrentCartService";
|
|
6
|
+
export const VariantSelectorContext = React.createContext(null);
|
|
7
|
+
export function VariantSelectorServiceProvider(props) {
|
|
8
|
+
const currentCartService = useContext(CurrentCartServiceContext);
|
|
9
|
+
const theService = VariantSelectorService({
|
|
10
|
+
// @ts-expect-error
|
|
11
|
+
getService: (id) => {
|
|
12
|
+
if (id === CurrentCartServiceDefinition) {
|
|
13
|
+
return currentCartService;
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`Unknown service: ${id}`);
|
|
16
|
+
},
|
|
17
|
+
config: {
|
|
18
|
+
product: props.product,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
return (_jsx(VariantSelectorContext.Provider, { value: theService, children: props.children }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { getCheckoutUrlForProduct } from "../../utils";
|
|
3
|
+
export function useBuyNow(productId, variantId) {
|
|
4
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
5
|
+
const [error, setError] = useState(null);
|
|
6
|
+
const redirectToCheckout = async () => {
|
|
7
|
+
setIsLoading(true);
|
|
8
|
+
try {
|
|
9
|
+
const checkoutUrl = await getCheckoutUrlForProduct(productId, variantId);
|
|
10
|
+
window.location.href = checkoutUrl;
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
setError(error);
|
|
14
|
+
setIsLoading(false);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
isLoading,
|
|
19
|
+
error,
|
|
20
|
+
redirectToCheckout,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BuyNow } from "./BuyNow";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const CurrentCartServiceDefinition: string & {
|
|
2
|
+
__api: {
|
|
3
|
+
addItem: () => void;
|
|
4
|
+
};
|
|
5
|
+
__config: {};
|
|
6
|
+
isServiceDefinition?: boolean;
|
|
7
|
+
} & {
|
|
8
|
+
addItem: () => void;
|
|
9
|
+
};
|
|
10
|
+
export declare const CurrentCartService: import("@wix/services-definitions").ServiceFactory<string & {
|
|
11
|
+
__api: {
|
|
12
|
+
addItem: () => void;
|
|
13
|
+
};
|
|
14
|
+
__config: {};
|
|
15
|
+
isServiceDefinition?: boolean;
|
|
16
|
+
} & {
|
|
17
|
+
addItem: () => void;
|
|
18
|
+
}, {}, import("@wix/services-definitions").ThreadMode.MAIN>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineService, implementService } from "@wix/services-definitions";
|
|
2
|
+
export const CurrentCartServiceDefinition = defineService("kaki2");
|
|
3
|
+
export const CurrentCartService = implementService.withConfig()(CurrentCartServiceDefinition, () => {
|
|
4
|
+
return {
|
|
5
|
+
addItem: () => {
|
|
6
|
+
alert("addItem");
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { productsV3 } from "@wix/stores";
|
|
2
|
+
export declare const VariantSelectorService: import("@wix/services-definitions").ServiceFactory<string & {
|
|
3
|
+
__api: {};
|
|
4
|
+
__config: {};
|
|
5
|
+
isServiceDefinition?: boolean;
|
|
6
|
+
}, {
|
|
7
|
+
product: productsV3.V3Product;
|
|
8
|
+
}, import("@wix/services-definitions").ThreadMode.MAIN>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineService, implementService, } from "@wix/services-definitions";
|
|
2
|
+
import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals";
|
|
3
|
+
import { CurrentCartServiceDefinition } from "./CurrentCartService";
|
|
4
|
+
const VariantSelectorServiceDefinition = defineService("kaki");
|
|
5
|
+
export const VariantSelectorService = implementService.withConfig()(VariantSelectorServiceDefinition, ({ getService, config }) => {
|
|
6
|
+
const cartService = getService(CurrentCartServiceDefinition);
|
|
7
|
+
const signalsService = getService(SignalsServiceDefinition);
|
|
8
|
+
const selectedVariant = signalsService.signal((config.product.variantsInfo?.variants ?? []).length > 0
|
|
9
|
+
? config.product.variantsInfo.variants[0]._id
|
|
10
|
+
: null);
|
|
11
|
+
return {
|
|
12
|
+
selectedVariant,
|
|
13
|
+
setSelectedVariant: (variant) => {
|
|
14
|
+
selectedVariant.set(variant);
|
|
15
|
+
},
|
|
16
|
+
addToCart: async () => {
|
|
17
|
+
await cartService.addItem();
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ServiceFactoryConfig, Signal } from "@wix/services-definitions";
|
|
2
|
+
export declare const BuyNowServiceDefinition: string & {
|
|
3
|
+
__api: {
|
|
4
|
+
redirectToCheckout: () => Promise<void>;
|
|
5
|
+
loadingSignal: Signal<boolean>;
|
|
6
|
+
errorSignal: Signal<string | null>;
|
|
7
|
+
};
|
|
8
|
+
__config: {};
|
|
9
|
+
isServiceDefinition?: boolean;
|
|
10
|
+
} & {
|
|
11
|
+
redirectToCheckout: () => Promise<void>;
|
|
12
|
+
loadingSignal: Signal<boolean>;
|
|
13
|
+
errorSignal: Signal<string | null>;
|
|
14
|
+
};
|
|
15
|
+
export declare const BuyNowServiceImplementation: import("@wix/services-definitions").ServiceFactory<string & {
|
|
16
|
+
__api: {
|
|
17
|
+
redirectToCheckout: () => Promise<void>;
|
|
18
|
+
loadingSignal: Signal<boolean>;
|
|
19
|
+
errorSignal: Signal<string | null>;
|
|
20
|
+
};
|
|
21
|
+
__config: {};
|
|
22
|
+
isServiceDefinition?: boolean;
|
|
23
|
+
} & {
|
|
24
|
+
redirectToCheckout: () => Promise<void>;
|
|
25
|
+
loadingSignal: Signal<boolean>;
|
|
26
|
+
errorSignal: Signal<string | null>;
|
|
27
|
+
}, {
|
|
28
|
+
productId: string;
|
|
29
|
+
variantId?: string;
|
|
30
|
+
productName: string;
|
|
31
|
+
price: string;
|
|
32
|
+
currency: string;
|
|
33
|
+
}, import("@wix/services-definitions").ThreadMode.MAIN>;
|
|
34
|
+
export declare const loadBuyNowServiceInitialData: (productSlug: string, variantId?: string) => Promise<{
|
|
35
|
+
[BuyNowServiceDefinition]: {
|
|
36
|
+
variantId?: string | undefined;
|
|
37
|
+
productId: string;
|
|
38
|
+
productName: string;
|
|
39
|
+
price: string;
|
|
40
|
+
currency: string;
|
|
41
|
+
};
|
|
42
|
+
}>;
|
|
43
|
+
export declare const buyNowServiceBinding: <T extends {
|
|
44
|
+
[key: string]: Awaited<ReturnType<typeof loadBuyNowServiceInitialData>>[typeof BuyNowServiceDefinition];
|
|
45
|
+
}>(servicesConfigs: T) => readonly [string & {
|
|
46
|
+
__api: {
|
|
47
|
+
redirectToCheckout: () => Promise<void>;
|
|
48
|
+
loadingSignal: Signal<boolean>;
|
|
49
|
+
errorSignal: Signal<string | null>;
|
|
50
|
+
};
|
|
51
|
+
__config: {};
|
|
52
|
+
isServiceDefinition?: boolean;
|
|
53
|
+
} & {
|
|
54
|
+
redirectToCheckout: () => Promise<void>;
|
|
55
|
+
loadingSignal: Signal<boolean>;
|
|
56
|
+
errorSignal: Signal<string | null>;
|
|
57
|
+
}, import("@wix/services-definitions").ServiceFactory<string & {
|
|
58
|
+
__api: {
|
|
59
|
+
redirectToCheckout: () => Promise<void>;
|
|
60
|
+
loadingSignal: Signal<boolean>;
|
|
61
|
+
errorSignal: Signal<string | null>;
|
|
62
|
+
};
|
|
63
|
+
__config: {};
|
|
64
|
+
isServiceDefinition?: boolean;
|
|
65
|
+
} & {
|
|
66
|
+
redirectToCheckout: () => Promise<void>;
|
|
67
|
+
loadingSignal: Signal<boolean>;
|
|
68
|
+
errorSignal: Signal<string | null>;
|
|
69
|
+
}, {
|
|
70
|
+
productId: string;
|
|
71
|
+
variantId?: string;
|
|
72
|
+
productName: string;
|
|
73
|
+
price: string;
|
|
74
|
+
currency: string;
|
|
75
|
+
}, import("@wix/services-definitions").ThreadMode.MAIN>, {
|
|
76
|
+
productId: string;
|
|
77
|
+
variantId?: string;
|
|
78
|
+
productName: string;
|
|
79
|
+
price: string;
|
|
80
|
+
currency: string;
|
|
81
|
+
}];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineService, implementService, } from "@wix/services-definitions";
|
|
2
|
+
import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals";
|
|
3
|
+
import { productsV3 } from "@wix/stores";
|
|
4
|
+
import { getCheckoutUrlForProduct } from "../utils";
|
|
5
|
+
export const BuyNowServiceDefinition = defineService("BuyNow");
|
|
6
|
+
export const BuyNowServiceImplementation = implementService.withConfig()(BuyNowServiceDefinition, ({ getService, config }) => {
|
|
7
|
+
const signalsService = getService(SignalsServiceDefinition);
|
|
8
|
+
const loadingSignal = signalsService.signal(false);
|
|
9
|
+
const errorSignal = signalsService.signal(null);
|
|
10
|
+
return {
|
|
11
|
+
redirectToCheckout: async () => {
|
|
12
|
+
loadingSignal.set(true);
|
|
13
|
+
try {
|
|
14
|
+
const checkoutUrl = await getCheckoutUrlForProduct(config.productId, config.variantId);
|
|
15
|
+
window.location.href = checkoutUrl;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
errorSignal.set(error);
|
|
19
|
+
loadingSignal.set(false);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
loadingSignal,
|
|
23
|
+
errorSignal,
|
|
24
|
+
productName: config.productName,
|
|
25
|
+
price: config.price,
|
|
26
|
+
currency: config.currency,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
export const loadBuyNowServiceInitialData = async (productSlug, variantId) => {
|
|
30
|
+
const res = await productsV3.getProductBySlug(productSlug, {
|
|
31
|
+
fields: ["CURRENCY"],
|
|
32
|
+
});
|
|
33
|
+
const product = res.product;
|
|
34
|
+
const selectedVariantId = variantId ?? product.variantsInfo?.variants?.[0]?._id;
|
|
35
|
+
const price = product.variantsInfo?.variants?.find((v) => v._id === selectedVariantId)
|
|
36
|
+
?.price?.actualPrice?.amount ??
|
|
37
|
+
product.actualPriceRange?.minValue?.amount;
|
|
38
|
+
return {
|
|
39
|
+
[BuyNowServiceDefinition]: {
|
|
40
|
+
productId: product._id,
|
|
41
|
+
productName: product.name,
|
|
42
|
+
price: price,
|
|
43
|
+
currency: product.currency,
|
|
44
|
+
...(typeof selectedVariantId !== "undefined"
|
|
45
|
+
? {
|
|
46
|
+
variantId: selectedVariantId,
|
|
47
|
+
}
|
|
48
|
+
: {}),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export const buyNowServiceBinding = (servicesConfigs) => {
|
|
53
|
+
return [
|
|
54
|
+
BuyNowServiceDefinition,
|
|
55
|
+
BuyNowServiceImplementation,
|
|
56
|
+
servicesConfigs[BuyNowServiceDefinition],
|
|
57
|
+
];
|
|
58
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buyNowServiceBinding, loadBuyNowServiceInitialData, } from "./buy-now-service";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buyNowServiceBinding, loadBuyNowServiceInitialData, } from "./buy-now-service";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getCheckoutUrlForProduct(productId: string, variantId?: string): Promise<string>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { checkout } from "@wix/ecom";
|
|
2
|
+
import { redirects } from "@wix/redirects";
|
|
3
|
+
const CATLOG_APP_ID_V3 = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
|
|
4
|
+
export async function getCheckoutUrlForProduct(productId, variantId) {
|
|
5
|
+
const checkoutResult = await checkout.createCheckout({
|
|
6
|
+
lineItems: [
|
|
7
|
+
{
|
|
8
|
+
catalogReference: {
|
|
9
|
+
catalogItemId: productId,
|
|
10
|
+
appId: CATLOG_APP_ID_V3,
|
|
11
|
+
options: {
|
|
12
|
+
variantId,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
quantity: 1,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
channelType: checkout.ChannelType.WEB,
|
|
19
|
+
});
|
|
20
|
+
if (!checkoutResult._id) {
|
|
21
|
+
throw new Error("Failed to create checkout");
|
|
22
|
+
}
|
|
23
|
+
const { redirectSession } = await redirects.createRedirectSession({
|
|
24
|
+
ecomCheckout: { checkoutId: checkoutResult._id },
|
|
25
|
+
callbacks: {
|
|
26
|
+
postFlowUrl: window.location.href,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
return redirectSession?.fullUrl;
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/headless-stores",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"build": "tsc",
|
|
6
6
|
"test": "vitest"
|
|
7
7
|
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
8
11
|
"exports": {
|
|
9
12
|
"./react": "./dist/react/index.js",
|
|
10
13
|
"./services": "./dist/services/index.js"
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
|
2
|
-
import '@testing-library/jest-dom/vitest';
|
|
3
|
-
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
4
|
-
import { BuyNow } from './BuyNow';
|
|
5
|
-
|
|
6
|
-
vi.mock('@wix/ecom', () => ({
|
|
7
|
-
checkout: {
|
|
8
|
-
createCheckout: vi.fn(),
|
|
9
|
-
ChannelType: { WEB: 'WEB' },
|
|
10
|
-
},
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
vi.mock('@wix/redirects', () => ({
|
|
14
|
-
redirects: {
|
|
15
|
-
createRedirectSession: vi.fn(),
|
|
16
|
-
},
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
const originalLocation = window.location;
|
|
20
|
-
let ecomCheckoutMock: any;
|
|
21
|
-
let redirectsMock: any;
|
|
22
|
-
|
|
23
|
-
beforeEach(async () => {
|
|
24
|
-
const ecom = await import('@wix/ecom');
|
|
25
|
-
ecomCheckoutMock = ecom.checkout;
|
|
26
|
-
const redirectsModule = await import('@wix/redirects');
|
|
27
|
-
redirectsMock = redirectsModule.redirects;
|
|
28
|
-
|
|
29
|
-
vi.clearAllMocks();
|
|
30
|
-
|
|
31
|
-
delete (window as any).location;
|
|
32
|
-
(window as any).location = { ...originalLocation, href: '' };
|
|
33
|
-
|
|
34
|
-
ecomCheckoutMock.createCheckout.mockResolvedValue({ _id: 'test-checkout-id' });
|
|
35
|
-
redirectsMock.createRedirectSession.mockResolvedValue({
|
|
36
|
-
redirectSession: { fullUrl: 'http://mocked-redirect-url.com' },
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
afterEach(() => {
|
|
41
|
-
(window as any).location = originalLocation; // Use type assertion for restoration
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('BuyNow Component from @wix/headless-stores/react', () => {
|
|
45
|
-
const testProductId = 'test-product-123';
|
|
46
|
-
const testVariant = { color: 'blue' };
|
|
47
|
-
|
|
48
|
-
const renderComponent = (props = {}) => {
|
|
49
|
-
let capturedRedirectToCheckout: () => Promise<void> = async () => {};
|
|
50
|
-
const renderOutput = render(
|
|
51
|
-
<BuyNow
|
|
52
|
-
productId={testProductId}
|
|
53
|
-
variant={testVariant}
|
|
54
|
-
{...props}
|
|
55
|
-
>
|
|
56
|
-
{({ isLoading, redirectToCheckout }) => {
|
|
57
|
-
capturedRedirectToCheckout = redirectToCheckout as () => Promise<void>;
|
|
58
|
-
if (isLoading) return <div>Loading...</div>;
|
|
59
|
-
return <button onClick={redirectToCheckout}>Buy Product Now</button>;
|
|
60
|
-
}}
|
|
61
|
-
</BuyNow>
|
|
62
|
-
);
|
|
63
|
-
return { ...renderOutput, redirectToCheckoutDirectly: capturedRedirectToCheckout };
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
test('should render the button with children render prop', () => {
|
|
67
|
-
renderComponent();
|
|
68
|
-
expect(screen.getByRole('button', { name: /Buy Product Now/i })).toBeInTheDocument();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('should show loading state and call checkout and redirect services on click', async () => {
|
|
72
|
-
renderComponent();
|
|
73
|
-
const button = screen.getByRole('button', { name: /Buy Product Now/i });
|
|
74
|
-
fireEvent.click(button);
|
|
75
|
-
|
|
76
|
-
expect(screen.getByText(/Loading.../i)).toBeInTheDocument();
|
|
77
|
-
|
|
78
|
-
await waitFor(() => {
|
|
79
|
-
expect(ecomCheckoutMock.createCheckout).toHaveBeenCalledTimes(1);
|
|
80
|
-
});
|
|
81
|
-
expect(ecomCheckoutMock.createCheckout).toHaveBeenCalledWith({
|
|
82
|
-
lineItems: [{
|
|
83
|
-
catalogReference: {
|
|
84
|
-
catalogItemId: testProductId,
|
|
85
|
-
appId: '215238eb-22a5-4c36-9e7b-e7c08025e04e',
|
|
86
|
-
options: {
|
|
87
|
-
options: testVariant,
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
quantity: 1
|
|
91
|
-
}],
|
|
92
|
-
channelType: 'WEB',
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
await waitFor(() => {
|
|
96
|
-
expect(redirectsMock.createRedirectSession).toHaveBeenCalledTimes(1);
|
|
97
|
-
});
|
|
98
|
-
expect(redirectsMock.createRedirectSession).toHaveBeenCalledWith({
|
|
99
|
-
ecomCheckout: { checkoutId: 'test-checkout-id' },
|
|
100
|
-
callbacks: {
|
|
101
|
-
postFlowUrl: expect.any(String),
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
await waitFor(() => {
|
|
106
|
-
expect(window.location.href).toBe('http://mocked-redirect-url.com');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
expect(screen.getByRole('button', { name: /Buy Product Now/i })).toBeInTheDocument();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('should handle checkout creation failure and reject', async () => {
|
|
113
|
-
ecomCheckoutMock.createCheckout.mockResolvedValueOnce({ _id: null });
|
|
114
|
-
|
|
115
|
-
const { redirectToCheckoutDirectly } = renderComponent();
|
|
116
|
-
|
|
117
|
-
await act(async () => {
|
|
118
|
-
await expect(redirectToCheckoutDirectly()).rejects.toThrow('Failed to create checkout');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
expect(screen.getByRole('button', { name: /Buy Product Now/i })).toBeInTheDocument();
|
|
122
|
-
expect(ecomCheckoutMock.createCheckout).toHaveBeenCalledTimes(1);
|
|
123
|
-
expect(redirectsMock.createRedirectSession).not.toHaveBeenCalled();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('should set isLoading to false and reject if redirects.createRedirectSession throws', async () => {
|
|
127
|
-
redirectsMock.createRedirectSession.mockRejectedValueOnce(new Error('Redirect failed'));
|
|
128
|
-
|
|
129
|
-
const { redirectToCheckoutDirectly } = renderComponent();
|
|
130
|
-
|
|
131
|
-
await act(async () => {
|
|
132
|
-
await expect(redirectToCheckoutDirectly()).rejects.toThrow('Redirect failed');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
expect(screen.getByRole('button', { name: /Buy Product Now/i })).toBeInTheDocument();
|
|
136
|
-
expect(redirectsMock.createRedirectSession).toHaveBeenCalledTimes(1);
|
|
137
|
-
expect(window.location.href).not.toBe('http://mocked-redirect-url.com');
|
|
138
|
-
});
|
|
139
|
-
});
|
package/src/react/BuyNow.tsx
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { useService } from "@wix/services-manager-react";
|
|
2
|
-
import { BuyNowServiceDefinition } from "../services/buy-now-service";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Props passed to the render function of the BuyNow component
|
|
6
|
-
*/
|
|
7
|
-
export type BuyNowRenderProps = {
|
|
8
|
-
/** Whether the buy now operation is currently loading */
|
|
9
|
-
isLoading: boolean;
|
|
10
|
-
/** The name of the product being purchased */
|
|
11
|
-
productName: string;
|
|
12
|
-
/** Function to redirect the user to the checkout page */
|
|
13
|
-
redirectToCheckout: () => void;
|
|
14
|
-
/** The error message if the buy now operation fails */
|
|
15
|
-
error: string | null;
|
|
16
|
-
/** The price of the product being purchased */
|
|
17
|
-
price: string;
|
|
18
|
-
/** The currency of the product being purchased */
|
|
19
|
-
currency: string;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Props for the BuyNow component
|
|
24
|
-
*/
|
|
25
|
-
export type BuyNowProps = {
|
|
26
|
-
/** Render function that receives buy now state and actions */
|
|
27
|
-
children: (props: BuyNowRenderProps) => React.ReactNode;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* A headless component that provides buy now functionality using the render props pattern.
|
|
32
|
-
*
|
|
33
|
-
* This component manages the state and actions for a "buy now" flow, allowing consumers
|
|
34
|
-
* to render their own UI while accessing the underlying buy now functionality.
|
|
35
|
-
*
|
|
36
|
-
* @param props - The component props
|
|
37
|
-
* @returns The rendered children with buy now state and actions
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```tsx
|
|
41
|
-
* <BuyNow>
|
|
42
|
-
* {({ isLoading, productName, redirectToCheckout, error, price, currency }) => (
|
|
43
|
-
* <div>
|
|
44
|
-
* <h2>{productName}</h2>
|
|
45
|
-
* <p>{price} {currency}</p>
|
|
46
|
-
* {error && <div className="error">{error}</div>}
|
|
47
|
-
* <button
|
|
48
|
-
* onClick={redirectToCheckout}
|
|
49
|
-
* disabled={isLoading}
|
|
50
|
-
* >
|
|
51
|
-
* {isLoading ? 'Processing...' : 'Buy Now'}
|
|
52
|
-
* </button>
|
|
53
|
-
* </div>
|
|
54
|
-
* )}
|
|
55
|
-
* </BuyNow>
|
|
56
|
-
* ```
|
|
57
|
-
*/
|
|
58
|
-
export function BuyNow(props: BuyNowProps) {
|
|
59
|
-
const {
|
|
60
|
-
redirectToCheckout,
|
|
61
|
-
loadingSignal,
|
|
62
|
-
productName,
|
|
63
|
-
errorSignal,
|
|
64
|
-
price,
|
|
65
|
-
currency,
|
|
66
|
-
} = useService(BuyNowServiceDefinition);
|
|
67
|
-
|
|
68
|
-
return props.children({
|
|
69
|
-
isLoading: loadingSignal.get(),
|
|
70
|
-
error: errorSignal.get(),
|
|
71
|
-
productName: productName,
|
|
72
|
-
redirectToCheckout,
|
|
73
|
-
price,
|
|
74
|
-
currency,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineService,
|
|
3
|
-
implementService,
|
|
4
|
-
ServiceFactoryConfig,
|
|
5
|
-
Signal,
|
|
6
|
-
} from "@wix/services-definitions";
|
|
7
|
-
import { SignalsServiceDefinition } from "@wix/services-definitions/core-services/signals";
|
|
8
|
-
import { productsV3 } from "@wix/stores";
|
|
9
|
-
import { getCheckoutUrlForProduct } from "../utils";
|
|
10
|
-
|
|
11
|
-
export const BuyNowServiceDefinition = defineService<{
|
|
12
|
-
redirectToCheckout: () => Promise<void>;
|
|
13
|
-
loadingSignal: Signal<boolean>;
|
|
14
|
-
errorSignal: Signal<string | null>;
|
|
15
|
-
}>("BuyNow");
|
|
16
|
-
|
|
17
|
-
export const BuyNowServiceImplementation = implementService.withConfig<{
|
|
18
|
-
productId: string;
|
|
19
|
-
variantId?: string;
|
|
20
|
-
productName: string;
|
|
21
|
-
price: string;
|
|
22
|
-
currency: string;
|
|
23
|
-
}>()(BuyNowServiceDefinition, ({ getService, config }) => {
|
|
24
|
-
const signalsService = getService(SignalsServiceDefinition);
|
|
25
|
-
const loadingSignal = signalsService.signal(false) as Signal<boolean>;
|
|
26
|
-
const errorSignal = signalsService.signal<string | null>(null) as Signal<
|
|
27
|
-
string | null
|
|
28
|
-
>;
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
redirectToCheckout: async () => {
|
|
32
|
-
loadingSignal.set(true);
|
|
33
|
-
try {
|
|
34
|
-
const checkoutUrl = await getCheckoutUrlForProduct(
|
|
35
|
-
config.productId,
|
|
36
|
-
config.variantId
|
|
37
|
-
);
|
|
38
|
-
window.location.href = checkoutUrl;
|
|
39
|
-
} catch (error) {
|
|
40
|
-
errorSignal.set(error as string);
|
|
41
|
-
loadingSignal.set(false);
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
loadingSignal,
|
|
45
|
-
errorSignal,
|
|
46
|
-
productName: config.productName,
|
|
47
|
-
price: config.price,
|
|
48
|
-
currency: config.currency,
|
|
49
|
-
};
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
export const loadBuyNowServiceInitialData = async (
|
|
53
|
-
productSlug: string,
|
|
54
|
-
variantId?: string
|
|
55
|
-
) => {
|
|
56
|
-
const res = await productsV3.getProductBySlug(productSlug, {
|
|
57
|
-
fields: ["CURRENCY"],
|
|
58
|
-
});
|
|
59
|
-
const product = res.product!;
|
|
60
|
-
|
|
61
|
-
const selectedVariantId =
|
|
62
|
-
variantId ?? product.variantsInfo?.variants?.[0]?._id;
|
|
63
|
-
|
|
64
|
-
const price =
|
|
65
|
-
product.variantsInfo?.variants?.find((v) => v._id === selectedVariantId)
|
|
66
|
-
?.price?.actualPrice?.amount ??
|
|
67
|
-
product.actualPriceRange?.minValue?.amount;
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
[BuyNowServiceDefinition]: {
|
|
71
|
-
productId: product._id!,
|
|
72
|
-
productName: product.name!,
|
|
73
|
-
price: price!,
|
|
74
|
-
currency: product.currency!,
|
|
75
|
-
...(typeof selectedVariantId !== "undefined"
|
|
76
|
-
? {
|
|
77
|
-
variantId: selectedVariantId!,
|
|
78
|
-
}
|
|
79
|
-
: {}),
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export const buyNowServiceBinding = <
|
|
85
|
-
T extends {
|
|
86
|
-
[key: string]: Awaited<
|
|
87
|
-
ReturnType<typeof loadBuyNowServiceInitialData>
|
|
88
|
-
>[typeof BuyNowServiceDefinition];
|
|
89
|
-
}
|
|
90
|
-
>(
|
|
91
|
-
servicesConfigs: T
|
|
92
|
-
) => {
|
|
93
|
-
return [
|
|
94
|
-
BuyNowServiceDefinition,
|
|
95
|
-
BuyNowServiceImplementation,
|
|
96
|
-
servicesConfigs[BuyNowServiceDefinition] as ServiceFactoryConfig<
|
|
97
|
-
typeof BuyNowServiceImplementation
|
|
98
|
-
>,
|
|
99
|
-
] as const;
|
|
100
|
-
};
|
package/src/services/index.ts
DELETED
package/src/utils/index.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { checkout } from "@wix/ecom";
|
|
2
|
-
import { redirects } from "@wix/redirects";
|
|
3
|
-
|
|
4
|
-
const CATLOG_APP_ID_V3 = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
|
|
5
|
-
|
|
6
|
-
export async function getCheckoutUrlForProduct(
|
|
7
|
-
productId: string,
|
|
8
|
-
variantId?: string
|
|
9
|
-
) {
|
|
10
|
-
const checkoutResult = await checkout.createCheckout({
|
|
11
|
-
lineItems: [
|
|
12
|
-
{
|
|
13
|
-
catalogReference: {
|
|
14
|
-
catalogItemId: productId,
|
|
15
|
-
appId: CATLOG_APP_ID_V3,
|
|
16
|
-
options: {
|
|
17
|
-
variantId,
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
quantity: 1,
|
|
21
|
-
},
|
|
22
|
-
],
|
|
23
|
-
channelType: checkout.ChannelType.WEB,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (!checkoutResult._id) {
|
|
27
|
-
throw new Error("Failed to create checkout");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const { redirectSession } = await redirects.createRedirectSession({
|
|
31
|
-
ecomCheckout: { checkoutId: checkoutResult._id },
|
|
32
|
-
callbacks: {
|
|
33
|
-
postFlowUrl: window.location.href,
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
return redirectSession?.fullUrl!;
|
|
38
|
-
}
|
package/src/vitest.setup.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import '@testing-library/jest-dom/vitest';
|
package/tsconfig.json
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
|
4
|
-
"module": "ESNext" /* Specify what module code is generated. */,
|
|
5
|
-
"moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
|
6
|
-
"resolveJsonModule": true /* Enable importing .json files. */,
|
|
7
|
-
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
|
8
|
-
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
|
9
|
-
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
|
10
|
-
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
|
11
|
-
"noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
|
|
12
|
-
"noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */,
|
|
13
|
-
"useUnknownInCatchVariables": true /* Default catch clause variables as 'unknown' instead of 'any'. */,
|
|
14
|
-
"noUnusedLocals": true /* Enable error reporting when local variables aren't read. */,
|
|
15
|
-
"noUnusedParameters": true /* Raise an error when a function parameter isn't read. */,
|
|
16
|
-
"noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */,
|
|
17
|
-
"noPropertyAccessFromIndexSignature": true /* Enforces using indexed accessors for keys declared using an indexed type. */,
|
|
18
|
-
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
|
19
|
-
"declaration": true,
|
|
20
|
-
"strict": true,
|
|
21
|
-
"noEmitOnError": false,
|
|
22
|
-
"jsx": "react-jsx"
|
|
23
|
-
},
|
|
24
|
-
"include": ["src/**/*"],
|
|
25
|
-
"exclude": [
|
|
26
|
-
"node_modules",
|
|
27
|
-
"dist",
|
|
28
|
-
"**/*.test.ts",
|
|
29
|
-
"**/*.test.tsx",
|
|
30
|
-
"**/*.spec.ts",
|
|
31
|
-
"**/*.spec.tsx",
|
|
32
|
-
"src/vitest.setup.ts"
|
|
33
|
-
]
|
|
34
|
-
}
|
package/vitest.config.ts
DELETED
|
File without changes
|