@noaignite/react-centra-checkout
Version:
React components and helpers for Centra checkout api
336 lines (335 loc) • 14.7 kB
JavaScript
import { ApiClient } from "./ApiClient.js";
import { CentraEvents } from "./internal/CentraEvents.js";
import { isPlainObject } from "@noaignite/utils";
import cookies from "js-cookie";
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { jsx } from "react/jsx-runtime";
//#region src/Context.tsx
const defaultApiClient = ApiClient.default;
const centraEvents = CentraEvents.default;
const SELECTION_INITIAL_VALUE = {
countries: [],
languages: [],
location: {},
paymentFields: {},
paymentMethods: [],
selection: {
address: {},
discounts: {},
items: [],
totals: {}
},
shippingMethods: []
};
const CentraHandlersContext = createContext(null);
const CentraSelectionContext = createContext(null);
/**
* Perform `callback` when `Promise` contains a `CheckoutApi.SelectionResponse`
*
* @param promise - Promise that may contain `selection` response
* @param callback - Callback that should be called with the `promise` if the results of the `promise` contains a `CheckoutApi.SelectionResponse`
*/
const onSelectionResponse = async (promise, callback) => {
const results = await promise;
if (isPlainObject(results) && "selection" in results && Boolean(results.selection)) return callback(promise);
return results;
};
/**
* React Context provider that is required to use the `useCentra` and `useCentraHandlers` hooks.
* Provides state management and related handlers of the users current Centra selection via the Checkout API.
* The handlers could be used for adding products to the selection and making purchases, for example.
*/
function CentraProvider(props) {
const { apiClient: apiClientProp, apiUrl, children, disableInit = false, initialSelection, paymentFailedPage, paymentReturnPage, receiptPage, tokenExpires = 365, tokenName = "centra-checkout-token", tokenCookieOptions = null } = props;
const apiClient = apiClientProp ?? defaultApiClient;
const [selection, setSelection] = useState(initialSelection ?? SELECTION_INITIAL_VALUE);
const centraCheckoutScript = "selection" in selection && selection.selection?.centraCheckoutScript;
if (apiUrl) apiClient.baseUrl = apiUrl;
if (initialSelection?.token) apiClient.headers.set("api-token", initialSelection.token);
const selectionApiCall = useCallback(async (apiCall) => {
window.CentraCheckout?.suspend();
const response = typeof apiCall === "function" ? await apiCall() : await apiCall;
setSelection(response);
window.CentraCheckout?.resume();
return response;
}, []);
const centraCheckoutCallback = useCallback(async (event) => {
const response = await apiClient.request("PUT", `payment-fields`, event.detail);
if ("selection" in response && response.selection) setSelection(response);
window.CentraCheckout?.resume(event.detail.additionalFields?.suspendIgnore);
centraEvents.dispatch("centra_checkout_callback", response);
}, [apiClient]);
const init = useCallback(async (selectionData) => {
let response;
const apiToken = cookies.get(tokenName);
if (apiToken) apiClient.headers.set("api-token", apiToken);
if (!selectionData) response = await apiClient.request("GET", "selection");
else response = selectionData;
if ("selection" in response && response.selection) {
setSelection(response);
if (response.token && response.token !== apiToken) {
apiClient.headers.set("api-token", response.token);
cookies.set(tokenName, response.token, {
expires: tokenExpires,
...tokenCookieOptions
});
}
}
}, [
tokenName,
apiClient,
tokenExpires,
tokenCookieOptions
]);
const addItem = useCallback((item, quantity = 1) => onSelectionResponse(apiClient.request("POST", `items/${item}/quantity/${quantity}`), selectionApiCall), [apiClient, selectionApiCall]);
const addBundleItem = useCallback((item, data) => onSelectionResponse(apiClient.request("POST", `items/bundles/${item}`, data), selectionApiCall), [apiClient, selectionApiCall]);
const addGiftCertificate = useCallback((giftCertificate) => onSelectionResponse(apiClient.request("POST", `items/gift-certificates/${giftCertificate}`), selectionApiCall), [apiClient, selectionApiCall]);
const addCustomGiftCertificate = useCallback((giftCertificate, amount) => onSelectionResponse(apiClient.request("POST", `items/gift-certificates/${giftCertificate}/amount/${amount}`), selectionApiCall), [apiClient, selectionApiCall]);
const increaseCartItem = useCallback((line) => onSelectionResponse(apiClient.request("POST", `lines/${line}/quantity/1`), selectionApiCall), [apiClient, selectionApiCall]);
const decreaseCartItem = useCallback((line) => onSelectionResponse(apiClient.request("DELETE", `lines/${line}/quantity/1`), selectionApiCall), [apiClient, selectionApiCall]);
const removeCartItem = useCallback((line) => onSelectionResponse(apiClient.request("DELETE", `lines/${line}`), selectionApiCall), [apiClient, selectionApiCall]);
const updateCartItemQuantity = useCallback((line, quantity) => onSelectionResponse(apiClient.request("PUT", `lines/${line}/quantity/${quantity}`), selectionApiCall), [apiClient, selectionApiCall]);
const updateCartItemSize = useCallback((cartItem, item) => selectionApiCall(async () => {
await apiClient.request("DELETE", `lines/${cartItem.line}`);
return await apiClient.request("POST", `items/${item}/quantity/${cartItem.quantity}`);
}), [apiClient, selectionApiCall]);
const addVoucher = useCallback((voucher) => onSelectionResponse(apiClient.request("POST", "vouchers", { voucher }), selectionApiCall), [apiClient, selectionApiCall]);
const removeVoucher = useCallback((voucher) => onSelectionResponse(apiClient.request("DELETE", `vouchers/${voucher}`), selectionApiCall), [apiClient, selectionApiCall]);
const updateCountry = useCallback((country, data) => onSelectionResponse(apiClient.request("PUT", `countries/${country}`, data), selectionApiCall), [apiClient, selectionApiCall]);
const updateLanguage = useCallback((language) => onSelectionResponse(apiClient.request("PUT", `languages/${language}`), selectionApiCall), [apiClient, selectionApiCall]);
const updateShippingMethod = useCallback((shippingMethod) => onSelectionResponse(apiClient.request("PUT", `shipping-methods/${shippingMethod}`), selectionApiCall), [apiClient, selectionApiCall]);
const updatePaymentMethod = useCallback((paymentMethod) => onSelectionResponse(apiClient.request("PUT", `payment-methods/${paymentMethod}`), selectionApiCall), [apiClient, selectionApiCall]);
const updatePaymentFields = useCallback(async (data) => onSelectionResponse(apiClient.request("PUT", `payment-fields`, data), selectionApiCall), [apiClient, selectionApiCall]);
const submitPayment = useCallback(async (data) => {
const response = await apiClient.request("POST", "payment", {
paymentReturnPage: typeof paymentReturnPage === "function" ? paymentReturnPage(selection) : paymentReturnPage,
paymentFailedPage: typeof paymentFailedPage === "function" ? paymentFailedPage(selection) : paymentFailedPage,
...data
});
if ("errors" in response) throw new Error(Object.entries(response.errors).map((key, value) => `${key}: ${value}`).join(","));
switch (response.action) {
case "redirect":
if (response.url) window.location.href = response.url;
break;
case "success":
window.location.href = `${receiptPage}/${response.token}`;
break;
case "javascript":
if (response.code) {
const script = document.createElement("script");
const text = document.createTextNode(response.code);
script.appendChild(text);
document.body.appendChild(script);
}
break;
default: return response;
}
return response;
}, [
apiClient,
paymentFailedPage,
paymentReturnPage,
receiptPage,
selection
]);
const addBackInStockSubscription = useCallback((data) => onSelectionResponse(apiClient.request("POST", "back-in-stock-subscription", data), selectionApiCall), [apiClient, selectionApiCall]);
const addNewsletterSubscription = useCallback((data) => onSelectionResponse(apiClient.request("POST", "newsletter-subscription", data), selectionApiCall), [apiClient, selectionApiCall]);
const loginCustomer = useCallback((email, password) => onSelectionResponse(apiClient.request("POST", `login/${email}`, { password }), selectionApiCall), [apiClient, selectionApiCall]);
const logoutCustomer = useCallback(() => onSelectionResponse(apiClient.request("POST", `logout`), selectionApiCall), [apiClient, selectionApiCall]);
const registerCustomer = useCallback((data) => onSelectionResponse(apiClient.request("POST", `register`, data), selectionApiCall), [apiClient, selectionApiCall]);
const resetCustomerPassword = useCallback((i, id, newPassword) => onSelectionResponse(apiClient.request("POST", `password-reset`, {
i,
id,
newPassword
}), selectionApiCall), [apiClient, selectionApiCall]);
/** Resets the selection. Useful if you need a fresh `api-token` (when a user exits a campaign site, for example). */
const resetSelection = useCallback(() => {
apiClient.headers.delete("api-token");
cookies.remove(tokenName);
return init();
}, [
apiClient.headers,
init,
tokenName
]);
const sendCustomerResetPasswordEmail = useCallback((email, linkUri) => onSelectionResponse(apiClient.request("POST", `password-reset-email/${email}`, { linkUri }), selectionApiCall), [apiClient, selectionApiCall]);
const updateCustomer = useCallback((data) => onSelectionResponse(apiClient.request("PUT", `customer/update`, data), selectionApiCall), [apiClient, selectionApiCall]);
const updateCustomerAddress = useCallback((data) => onSelectionResponse(apiClient.request("PUT", `address`, data), selectionApiCall), [apiClient, selectionApiCall]);
const updateCustomerEmail = useCallback((newEmail) => onSelectionResponse(apiClient.request("PUT", `email`, { newEmail }), selectionApiCall), [apiClient, selectionApiCall]);
const updateCustomerPassword = useCallback((password, newPassword) => onSelectionResponse(apiClient.request("PUT", `password`, {
password,
newPassword
}), selectionApiCall), [apiClient, selectionApiCall]);
const updateCampaignSite = useCallback((uri) => onSelectionResponse(apiClient.request("PUT", `campaign-site`, { uri }), selectionApiCall), [apiClient, selectionApiCall]);
useEffect(() => {
if (!disableInit) init();
document.addEventListener("centra_checkout_callback", centraCheckoutCallback);
return () => {
document.removeEventListener("centra_checkout_callback", centraCheckoutCallback);
};
}, [
disableInit,
init,
centraCheckoutCallback
]);
useEffect(() => {
let script = null;
if (centraCheckoutScript) {
script = document.createElement("script");
script.innerHTML = centraCheckoutScript;
script.id = "centra-checkout-script";
document.head.appendChild(script);
}
return () => {
if (script) document.head.removeChild(script);
};
}, [centraCheckoutScript]);
const centraHandlersContext = useMemo(() => ({
addItem,
addBundleItem,
addGiftCertificate,
addBackInStockSubscription,
addCustomGiftCertificate,
addNewsletterSubscription,
addVoucher,
decreaseCartItem,
increaseCartItem,
init,
loginCustomer,
logoutCustomer,
registerCustomer,
removeCartItem,
removeVoucher,
resetCustomerPassword,
resetSelection,
sendCustomerResetPasswordEmail,
submitPayment,
updateCartItemQuantity,
updateCartItemSize,
updateCountry,
updateCustomer,
updateCustomerAddress,
updateCustomerEmail,
updateCustomerPassword,
updateLanguage,
updatePaymentFields,
updatePaymentMethod,
updateShippingMethod,
updateCampaignSite
}), [
addItem,
addBundleItem,
addGiftCertificate,
addBackInStockSubscription,
addCustomGiftCertificate,
addNewsletterSubscription,
addVoucher,
decreaseCartItem,
increaseCartItem,
init,
loginCustomer,
logoutCustomer,
registerCustomer,
removeCartItem,
removeVoucher,
resetCustomerPassword,
resetSelection,
sendCustomerResetPasswordEmail,
submitPayment,
updateCartItemQuantity,
updateCartItemSize,
updateCountry,
updateCustomer,
updateCustomerAddress,
updateCustomerEmail,
updateCustomerPassword,
updateLanguage,
updatePaymentFields,
updatePaymentMethod,
updateShippingMethod,
updateCampaignSite
]);
const centraContext = useMemo(() => ({
...selection,
apiUrl,
apiClient
}), [
selection,
apiUrl,
apiClient
]);
return /* @__PURE__ */ jsx(CentraHandlersContext.Provider, {
value: centraHandlersContext,
children: /* @__PURE__ */ jsx(CentraSelectionContext.Provider, {
value: centraContext,
children
})
});
}
/**
* This hook returns the centra selection
*/
function useCentraSelection() {
const context = useContext(CentraSelectionContext);
if (context === null) throw new Error([
"@noaignite/react-centra-checkout: `useCentraSelection` may only be",
"used inside the `CentraProvider` react tree, please declare it at a",
"higher level."
].join(" "));
return context;
}
/**
* This hook returns update handlers
*/
function useCentraHandlers() {
const context = useContext(CentraHandlersContext);
if (context === null) throw new Error([
"@noaignite/react-centra-checkout: `useCentraHandlers` may only be",
"used inside the `CentraProvider` react tree, please declare it at a",
"higher level."
].join(" "));
return context;
}
/**
* Returns the latest order receipt given a selection token
*/
function useCentraReceipt(token) {
const [result, setResult] = useState({});
const { apiUrl } = useCentraSelection();
if (!token) console.error("@noaignite/react-centra-checkout: useReceipt requires a selection id");
useEffect(() => {
const tempApiClient = new ApiClient(apiUrl);
tempApiClient.headers.set("api-token", token);
tempApiClient.request("GET", "receipt").then((response) => {
setResult(response);
});
}, [apiUrl, token]);
return result;
}
/**
* Returns the latest orders for the currently logged in user
* @param from - Display orders from this index. Defaults to 0.
* @param size - Display this many orders. Defaults lists all orders.
*/
function useCentraOrders(from, size, apiClient = defaultApiClient) {
const [result, setResult] = useState({});
useEffect(() => {
apiClient.request("POST", "orders", {
...from && { from },
...size && { size }
}).then((response) => {
setResult(response);
});
}, [
apiClient,
from,
size
]);
return result;
}
function useCentraEvents() {
return CentraEvents.default;
}
//#endregion
export { CentraHandlersContext, CentraProvider, CentraSelectionContext, SELECTION_INITIAL_VALUE, useCentraEvents, useCentraHandlers, useCentraOrders, useCentraReceipt, useCentraSelection };
//# sourceMappingURL=Context.js.map