UNPKG

@noaignite/react-centra-checkout

Version:

React components and helpers for Centra checkout api

336 lines (335 loc) 14.7 kB
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