UNPKG

bitsnap-checkout

Version:

This is Bitsnap Checkout React library for easy integration with any website which is using React framework

1 lines 102 kB
{"version":3,"sources":["../src/components/checkout/BitsnapCart.tsx","../src/components/checkout/CartComponent.tsx","../src/components/checkout/CartComponentContent.tsx","../src/components/checkout/CartProvider.tsx","../src/components/checkout/constants.ts","../src/components/checkout/helper.methods.ts","../src/components/checkout/lib/err.ts","../src/components/checkout/state.ts","../src/components/checkout/methods.ts","../src/components/checkout/CountrySelector.tsx","../src/components/checkout/lib/round.number.ts","../src/components/checkout/LoadingIndicator.tsx","../src/components/checkout/SingleProduct.tsx","../src/components/webhook.handler.ts","../src/gen/proto/jobs/v1/integration_event_job_pb.ts","../src/gen/proto/common/v1/environment_pb.ts","../src/gen/proto/integrations/v1/event_data_pb.ts","../src/gen/proto/integrations/v1/order_pb.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport zod from \"zod\";\nimport CartComponent from \"./CartComponent\";\nimport { getCheckoutMethods, getProjectID, setProjectID } from \"./CartProvider\";\nimport { isErr } from \"./lib/err\";\nimport { useCheckoutStore } from \"./state\";\n\nenum CartEvent {\n ADD_TO_CART = \"ADD_TO_CART\",\n}\n\nconst cartAddToCartSchema = zod.object({\n id: zod.string(),\n isSubscription: zod.boolean().default(false),\n quantity: zod.number(),\n metadata: zod.record(zod.string(), zod.string().optional()).optional(),\n});\n\ntype CartAddToCartEvent = zod.infer<typeof cartAddToCartSchema>;\n\nfunction BitsnapCart({\n projectID,\n children,\n onVisibleChange,\n className,\n}: {\n projectID: string;\n children?: React.ReactNode;\n onVisibleChange?: (isVisible: boolean) => void;\n className?: string;\n}) {\n const { isCartVisible, showCart, hideCart } = useCheckoutStore();\n\n function sentPostMessageToIframe(msg: unknown) {\n const iframes = document.querySelectorAll(\"iframe\");\n\n iframes.forEach((iframe) => {\n iframe.contentWindow?.postMessage(msg, \"*\");\n });\n }\n\n useEffect(() => {\n onVisibleChange?.(isCartVisible);\n }, [isCartVisible]);\n\n useEffect(() => {\n setProjectID(projectID);\n }, [projectID]);\n\n useEffect(() => {\n if (!(\"cart\" in window)) {\n const projectID = getProjectID();\n if (projectID == null) {\n console.warn(\"There is no project ID configured.\");\n return;\n }\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-expect-error\n window[\"cart\"] = {\n buyProduct: async (args: unknown) => {\n try {\n const payload = zod\n .object({\n productID: zod.string(),\n email: zod.string().optional(),\n name: zod.string().optional(),\n country: zod.string().optional(),\n marketingAgreement: zod.boolean().optional(),\n })\n .parse(args);\n\n const methods = getCheckoutMethods(projectID);\n\n const result = await methods.justRedirectToPayment(payload);\n\n console.log(\"result\", result);\n if (isErr(result) == false) {\n window.location.href = result.url;\n } else {\n alert(\n \"Nie udało się przekierować do płatności. Spróbuj ponownie później.\",\n );\n }\n } catch (e) {\n console.warn(e);\n }\n },\n };\n }\n }, []);\n\n async function handleAddingToCart(id: string, payload?: unknown) {\n const projectID = getProjectID();\n if (projectID == null) {\n console.warn(\"There is no project ID configured.\");\n return;\n }\n\n try {\n const data = cartAddToCartSchema.parse(payload);\n\n if (data.isSubscription) {\n await handleAddingSubscriptionToCart(projectID, data);\n return;\n }\n\n const methods = getCheckoutMethods(projectID);\n\n await methods.addProduct({\n productID: data.id,\n quantity: data.quantity,\n metadata: data.metadata,\n });\n\n sentPostMessageToIframe({\n id: id,\n type: CartEvent.ADD_TO_CART,\n success: true,\n });\n showCart();\n } catch (e) {\n console.warn(\"cannot add item to cart\", e);\n }\n }\n\n async function handleAddingSubscriptionToCart(\n projectID: string,\n event: CartAddToCartEvent,\n ) {\n alert(\n `TODO, nie jest to jeszcze zrobione ${projectID} ${event.id} ${event.quantity}`,\n );\n }\n\n function setupEventListener() {\n if (\"__cart_is_listening\" in window) {\n return;\n }\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-expect-error\n window[\"__cart_is_listening\"] = true;\n window.addEventListener(\"message\", (event) => {\n if (\n typeof event.data[\"id\"] != \"undefined\" &&\n typeof event.data[\"type\"] != \"undefined\"\n ) {\n const { id, type } = event.data;\n switch (type) {\n case CartEvent.ADD_TO_CART.toString():\n handleAddingToCart(id, event.data[\"payload\"]).then().catch();\n break;\n default:\n break;\n }\n }\n });\n }\n\n useEffect(() => {\n setupEventListener();\n\n if (typeof window !== \"undefined\") {\n try {\n const parsedParams = new URLSearchParams(window.location.search);\n const refLink =\n parsedParams.get(\"ref\") ?? parsedParams.get(\"utm-source\");\n if (\n typeof localStorage != \"undefined\" &&\n refLink &&\n refLink.length > 0\n ) {\n localStorage.setItem(\"bitsnap-ref\", refLink);\n }\n } catch (e) {\n return;\n }\n }\n }, []);\n\n return (\n <>\n <button\n onClick={() => (isCartVisible ? hideCart() : showCart())}\n className={\n className ??\n \"ics-rounded-full hover:ics-bg-neutral-300 ics-transition ics-p-1\"\n }\n >\n {children ? (\n <>{children}</>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"lucide lucide-shopping-cart\"\n >\n <circle cx=\"8\" cy=\"21\" r=\"1\" />\n <circle cx=\"19\" cy=\"21\" r=\"1\" />\n <path d=\"M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12\" />\n </svg>\n )}\n </button>\n <CartComponent\n isVisible={isCartVisible}\n shouldHide={() => {\n hideCart();\n }}\n />\n </>\n );\n}\n\nexport default BitsnapCart;\n","import { useAutoAnimate } from \"@formkit/auto-animate/react\";\nimport { createPortal } from \"react-dom\";\nimport CartComponentContent from \"./CartComponentContent\";\nimport CartProvider from \"./CartProvider\";\n\ninterface Props {\n isVisible: boolean;\n shouldHide: () => void;\n}\n\nfunction CartComponent({ isVisible, shouldHide }: Props) {\n const [parent] = useAutoAnimate(/* optional config */);\n\n return (\n <div ref={parent} className={\"ics-z-[999999]\"}>\n {isVisible && (\n <>\n <div\n className={\n \"ics-fixed ics-top-0 ics-right-0 ics-left-0 ics-bottom-0 ics-bg-black/30 ics-cursor-pointer\"\n }\n onClick={shouldHide}\n ></div>\n <div\n className={\n \"ics-fixed ics-top-0 ics-right-0 ics-bottom-0 ics-w-full md:ics-w-[350px] xl:ics-w-[420px] dark:ics-bg-neutral-900 ics-bg-neutral-300 dark:ics-text-neutral-200 ics-text-neutral-900 ics-flex ics-flex-col\"\n }\n >\n <div\n className={\n \"ics-mx-3 ics-mt-7 ics-flex ics-justify-between ics-items-center\"\n }\n >\n <h1 className={\"text-2xl font-medium\"}>Koszyk</h1>\n <button\n className=\"ics-rounded-full dark:hover:ics-bg-neutral-700 hover:ics-bg-neutral-400 ics-p-2 ics-transition\"\n onClick={shouldHide}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"lucide lucide-x\"\n >\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n </button>\n </div>\n\n <CartComponentContent className={\"grow\"} />\n </div>\n </>\n )}\n </div>\n );\n}\n\nconst WrapperCartComponent = (props: Props) => {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n return createPortal(\n <CartProvider>\n <CartComponent {...props} />\n </CartProvider>,\n document.body,\n );\n};\n\nexport default WrapperCartComponent;\n","import { useAutoAnimate } from \"@formkit/auto-animate/react\";\nimport React from \"react\";\nimport { useMutation, useQuery } from \"react-query\";\nimport { useCartProvider } from \"./CartProvider\";\nimport CountrySelector from \"./CountrySelector\";\nimport { isErr } from \"./lib/err\";\nimport { formatCurrency } from \"./lib/round.number\";\nimport LoadingIndicator from \"./LoadingIndicator\";\nimport SingleProduct from \"./SingleProduct\";\n\nconst CartComponentContent = ({ className }: { className: string }) => {\n const provider = useCartProvider();\n const { mutateAsync: removeProduct } = useMutation(\n provider.removeProductFromCart,\n );\n const { mutateAsync: updateQuantity } = useMutation(provider.updateQuantity);\n const { mutateAsync: setCountryAsync } = useMutation(provider.setCountry);\n const { mutateAsync: clearCart } = useMutation(provider.clearCart);\n\n const [errMsg, setErrMsg] = React.useState(\"\");\n\n const [isCountryOpen, setIsCountryOpen] = React.useState(false);\n\n const {\n mutateAsync: continueToCheckoutAsync,\n isLoading: isContinueToCheckoutLoading,\n } = useMutation(provider.redirectToNextStep);\n\n const { data: availableCountries } = useQuery(\n \"cart-available-countries\",\n provider.getAvailableCountries,\n );\n const { data, isLoading, refetch } = useQuery(\"cart\", provider.getProducts);\n const { data: countryData, refetch: refetchCountry } = useQuery(\n \"cart-country\",\n provider.getCountry,\n );\n\n const [productsParent] = useAutoAnimate(/* optional config */);\n\n const products = isErr(data) ? undefined : data;\n\n const countries = isErr(availableCountries) ? [] : availableCountries;\n\n const sumOfProducts =\n products?.reduce((prev, curr) => {\n if (curr.details == null) {\n return prev;\n }\n return prev + curr.details.price * curr.quantity;\n }, 0) ?? 0;\n const currency = products?.[0]?.details?.currency ?? \"PLN\";\n\n const isSomeProductDeliverable =\n products?.some((product) => product.details?.isDeliverable === true) ??\n false;\n\n const selectedCountry =\n countryData != null && !isErr(countryData) && countryData != \"\"\n ? countryData\n : undefined;\n\n async function shouldUpdate(id: string, newQuantity?: number) {\n if (newQuantity != null) {\n if (newQuantity <= 0) {\n await removeProduct({ id: id });\n await refetch();\n return;\n }\n\n await updateQuantity({\n id: id,\n quantity: newQuantity,\n });\n await refetch();\n return;\n }\n }\n\n async function continueToCheckout() {\n try {\n setErrMsg(\"\");\n\n const response = await continueToCheckoutAsync();\n\n if (isErr(response)) {\n setErrMsg(`${response.error}`);\n return;\n }\n\n await clearCart();\n window.location.href = response.url;\n } catch (e: unknown) {\n setErrMsg(`${e}`);\n }\n }\n\n return (\n <div className={`${className} ics-flex ics-flex-col`} ref={productsParent}>\n {isLoading && (\n <div className={\"ics-flex ics-w-full ics-justify-center\"}>\n <LoadingIndicator />\n </div>\n )}\n\n {!isLoading && (products == null || products.length == 0) && (\n <div className={\"ics-flex ics-flex-col ics-gap-4 ics-p-4\"}>\n <p className={\"dark:ics-text-neutral-400 ics-text-neutral-700\"}>\n Brak produktów w koszyku.\n </p>\n </div>\n )}\n\n <div\n className={\"ics-max-h-[70vh] ics-overflow-clip ics-overflow-y-scroll\"}\n >\n {products != null && products.length > 0 && (\n <ul className={\"ics-mt-5\"}>\n {products.map((product) => (\n <React.Fragment key={product.id}>\n {product.details != null && (\n <li className={\"mb-3\"}>\n <SingleProduct\n quantity={product.quantity}\n details={product.details}\n shouldUpdate={(newQuantity) => {\n shouldUpdate(product.id, newQuantity).then().catch();\n }}\n />\n <hr className=\"ics-h-1 dark:ics-border-neutral-700 ics-border-neutral-400\" />\n </li>\n )}\n </React.Fragment>\n ))}\n </ul>\n )}\n </div>\n\n <div className=\"grow\"></div>\n\n {sumOfProducts > 0 && currency != null && (\n <>\n <div className=\"ics-mx-3 ics-flex ics-flex-col\">\n <div\n className={\n \"ics-flex ics-flex-row ics-justify-between ics-text-lg\"\n }\n >\n <p\n className={\n \"dark:ics-text-neutral-200 ics-text-neutral-800 ics-text-xl\"\n }\n >\n Suma:\n </p>\n <div className={\"ics-flex ics-flex-col ics-items-end\"}>\n <p\n className={\n \"dark:ics-text-neutral-200 ics-text-neutral-800 ics-font-medium\"\n }\n >\n {formatCurrency(sumOfProducts, currency)}\n </p>\n {isSomeProductDeliverable && (\n <p className={\"ics-opacity-70 ics-text-right ics-text-base\"}>\n + dostawa\n </p>\n )}\n </div>\n </div>\n </div>\n\n {countries && countries?.length > 1 && (\n <div>\n <h4\n className={\n \"ics-ml-3 ics-text-sm dark:ics-text-neutral-400 ics-text-neutral-700\"\n }\n >\n Wybierz kraj\n </h4>\n <CountrySelector\n id={Math.random().toString()}\n open={isCountryOpen}\n onToggle={() => {\n setIsCountryOpen(!isCountryOpen);\n }}\n onChange={(newValue) => {\n setCountryAsync(newValue).then(() => {\n refetchCountry().then().catch();\n });\n }}\n selectedValue={selectedCountry ?? \"\"}\n countries={countries}\n />\n </div>\n )}\n\n <div className={\"ics-mb-3 ics-flex ics-flex-col\"}>\n <button\n onClick={continueToCheckout}\n disabled={\n isLoading ||\n isContinueToCheckoutLoading ||\n selectedCountry == null\n }\n className={\n \"ics-px-3 ics-py-2 ics-my-2 ics-mx-2 ics-rounded-md disabled:ics-opacity-40 disabled:ics-cursor-not-allowed dark:ics-bg-neutral-300 dark:hover:ics-bg-neutral-100 dark:ics-text-neutral-800 hover:ics-bg-neutral-900 ics-text-neutral-200 ics-bg-neutral-800 ics-transition ics-font-bold\"\n }\n >\n {isContinueToCheckoutLoading ? \"Ładowanie...\" : \"Następny krok\"}\n </button>\n {errMsg.length > 0 && (\n <p className={\"ics-text-red-500 ics-text-sm ics-text-center\"}>\n {errMsg}\n </p>\n )}\n </div>\n </>\n )}\n </div>\n );\n};\n\nexport default CartComponentContent;\n","import { createContext, useContext } from \"react\";\nimport { QueryClient, QueryClientProvider } from \"react-query\";\nimport zod from \"zod\";\nimport { setCustomHost } from \"./constants\";\nimport { buildURL } from \"./helper.methods\";\nimport { Err, isErr } from \"./lib/err\";\nimport { LinkRequest } from \"./link.request.schema\";\nimport { createPaymentURL, injectReferenceToRequestIfNeeded } from \"./methods\";\nimport { SingleProduct } from \"./product.details.model\";\n\nexport const MARKETING_AGREEMENT_ID = \"__m_a\";\n\nexport interface CartMethods {\n addProduct(args: {\n productID: string;\n quantity: number;\n metadata?: { [key: string]: string | undefined };\n }): Promise<Err | void>;\n\n getProducts: () => Promise<\n | Err\n | {\n id: string;\n productID: string;\n quantity: number;\n metadata?: { [key: string]: string | undefined };\n details?: SingleProduct;\n }[]\n >;\n\n updateQuantity(args: { id: string; quantity: number }): Promise<Err | void>;\n\n removeProductFromCart: (args: { id: string }) => Promise<Err | void>;\n\n clearCart: () => Promise<Err | void>;\n\n getNumberOfElementsInCart: () => Promise<number>;\n\n setCountry: (country: string) => Promise<Err | void>;\n getCountry: () => Promise<Err | string>;\n getAvailableCountries: () => Promise<Err | { name: string; code: string }[]>;\n\n redirectToNextStep: () => Promise<Err | { url: string }>;\n\n justRedirectToPayment: (args: {\n email?: string;\n productID: string;\n name?: string;\n country?: string;\n marketingAgreement?: boolean;\n }) => Promise<\n | Err\n | {\n url: string;\n }\n >;\n}\n\nconst CartProviderContext = createContext<CartMethods | undefined>(undefined);\n\nexport var bitsnapProjectID: string | undefined = undefined;\nexport function setProjectID(projectID: string) {\n bitsnapProjectID = projectID;\n}\n\nexport function getProjectID(): string | undefined {\n if (bitsnapProjectID != null) {\n return bitsnapProjectID;\n }\n const me = document.querySelector(\n 'script[data-id][data-name=\"internal-cart\"]',\n );\n const projectID = me?.getAttribute(\"data-id\");\n return projectID ?? undefined;\n}\n\nfunction getNewHostIfExist(): string | undefined {\n const me = document.querySelector(\n 'script[data-id][data-name=\"internal-cart\"]',\n );\n const customHost = me?.getAttribute(\"data-custom-host\");\n return customHost ?? undefined;\n}\n\nconst CartProvider = ({ children }: { children: React.ReactNode }) => {\n const queryClient = new QueryClient();\n\n const projectID = getProjectID();\n if (projectID == null) {\n return <></>;\n }\n\n const checkoutMethods = getCheckoutMethods(projectID);\n\n return (\n <QueryClientProvider client={queryClient}>\n <CartProviderContext.Provider value={checkoutMethods}>\n {children}\n </CartProviderContext.Provider>\n </QueryClientProvider>\n );\n};\n\nexport const useCartProvider = () => {\n const context = useContext(CartProviderContext);\n\n if (context === undefined) {\n throw new Error(\"useCartProvider must be used within a CartProvider\");\n }\n\n return context;\n};\n\nexport default CartProvider;\n\nconst checkoutSchema = zod.object({\n country: zod.string().optional(),\n products: zod\n .array(\n zod.object({\n id: zod.string(),\n productID: zod.string(),\n quantity: zod.number(),\n metadata: zod.record(zod.string(), zod.string().optional()).optional(),\n }),\n )\n .optional(),\n});\nconst emptyCheckout: Checkout = {\n country: undefined,\n products: [],\n};\ntype Checkout = zod.infer<typeof checkoutSchema>;\n\nconst checkoutKey = \"checkout\";\n\nfunction getCheckout(): Checkout {\n try {\n const value = localStorage.getItem(checkoutKey);\n if (value == null) {\n return emptyCheckout;\n }\n return checkoutSchema.parse(JSON.parse(value));\n } catch (e) {\n return emptyCheckout;\n }\n}\n\nfunction saveCheckout(model: Checkout) {\n localStorage.setItem(checkoutKey, JSON.stringify(model));\n}\n\nexport const getCheckoutMethods: (projectID: string) => CartMethods = (\n projectID,\n) => {\n const newHost = getNewHostIfExist();\n if (newHost != null) {\n setCustomHost(newHost);\n }\n return {\n async clearCart(): Promise<Err | void> {\n const empty = structuredClone(emptyCheckout);\n empty.country = getCheckout()?.country;\n saveCheckout(empty);\n },\n\n async getAvailableCountries(): Promise<\n Err | { name: string; code: string }[]\n > {\n const result = await fetch(buildURL(projectID, \"/countries\"), {\n method: \"GET\",\n });\n\n if (result.status != 200) {\n return [];\n }\n\n try {\n return zod\n .array(\n zod.object({\n name: zod.string(),\n code: zod.string(),\n }),\n )\n .parse(await result.json());\n } catch (e) {\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-expect-error\n return Err(e.toString(), \"internal\");\n }\n },\n\n async getCountry(): Promise<Err | string> {\n let country = getCheckout()?.country;\n if (country == null) {\n country = \"PL\";\n const checkout = getCheckout();\n checkout.country = country;\n saveCheckout(checkout);\n }\n\n return country;\n },\n\n async getNumberOfElementsInCart(): Promise<number> {\n return (\n getCheckout()?.products?.reduce(\n (acc, product) => acc + product.quantity,\n 0,\n ) ?? 0\n );\n },\n\n async getProducts(): Promise<\n | Err\n | {\n id: string;\n productID: string;\n quantity: number;\n metadata?: { [p: string]: string | undefined };\n details?: SingleProduct;\n }[]\n > {\n const products = getCheckout()?.products ?? [];\n\n const productIds = Array.from(\n new Set(products.map((product) => product.productID)),\n );\n\n const params = new URLSearchParams();\n params.set(\"ids\", productIds.join(\",\"));\n\n const result = await fetch(\n buildURL(projectID, `/products?${params.toString()}`),\n {\n method: \"GET\",\n },\n );\n\n if (result.status != 200) {\n return [];\n }\n\n const payload: {\n success: boolean;\n message?: string | undefined;\n result?: SingleProduct[] | undefined;\n } = await result.json();\n\n products.forEach((product) => {\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-expect-error\n product[\"details\"] = payload.result?.find((el) => {\n if (el.id === product.productID) {\n return true;\n }\n if (el.variants != null && el.variants.length > 0) {\n const index = el.variants.findIndex(\n (variant) => variant.id === product.productID,\n );\n return index !== -1;\n }\n return false;\n });\n });\n\n // @ts-ignore\n return products\n .filter((el) => \"details\" in el)\n .map((el) => {\n el.details = resolveProductDetailsFromSingleProduct(\n el.productID,\n el.details as SingleProduct,\n );\n return el;\n });\n },\n\n async removeProductFromCart(args: { id: string }): Promise<Err | void> {\n const checkout = getCheckout();\n\n const newCheckout = {\n ...checkout,\n products: checkout?.products?.filter(\n (product) => product.id !== args.id,\n ),\n };\n saveCheckout(newCheckout);\n },\n\n async setCountry(country: string): Promise<Err | void> {\n const checkout = getCheckout();\n checkout.country = country;\n saveCheckout(checkout);\n },\n\n async addProduct(args: {\n productID: string;\n quantity: number;\n metadata?: { [p: string]: string | undefined };\n }): Promise<Err | void> {\n const checkout = getCheckout();\n if (checkout.products == null) {\n checkout.products = [];\n }\n checkout.products.push({\n id: Math.random().toString(36).substring(7),\n productID: args.productID,\n quantity: args.quantity,\n metadata: args.metadata,\n });\n saveCheckout(checkout);\n },\n\n async updateQuantity(args: {\n id: string;\n quantity: number;\n }): Promise<Err | void> {\n const checkout = getCheckout();\n checkout.products = checkout.products?.map((product) => {\n if (product.id === args.id) {\n product.quantity = args.quantity;\n }\n return product;\n });\n saveCheckout(checkout);\n },\n\n async redirectToNextStep(): Promise<Err | { url: string }> {\n const checkout = getCheckout();\n\n if (checkout.products == null || checkout.products.length == 0) {\n return Err(\"cart-is-empty\", \"badInput\");\n }\n const mergedMetadata = checkout.products.reduce(\n (acc, product) => {\n if (product.metadata != null) {\n Object.keys(product.metadata).forEach((key) => {\n const value = product.metadata?.[key];\n if (value != null) {\n acc[key] = value;\n }\n });\n }\n return acc;\n },\n {} as Record<string, string>,\n );\n\n const payload: LinkRequest = {\n items: checkout.products.map((el) => {\n return {\n id: el.productID,\n quantity: el.quantity,\n };\n }),\n askForNote: true,\n countries: checkout.country ? [checkout.country] : undefined,\n metadata: mergedMetadata,\n };\n\n const paymentResponse = await createPaymentURL(payload);\n\n if (isErr(paymentResponse)) {\n console.warn(\"cannot create payment URL\", paymentResponse.error);\n return paymentResponse;\n }\n\n return {\n url: paymentResponse.url,\n };\n },\n\n async justRedirectToPayment(args: {\n email?: string;\n productID: string;\n name?: string;\n country?: string;\n marketingAgreement?: boolean;\n }): Promise<Err | { url: string }> {\n let payload: LinkRequest = {\n items: [\n {\n id: args.productID,\n quantity: 1,\n },\n ],\n askForNote: false,\n details:\n args.email || args.name\n ? {\n name: args.name,\n email: args.email,\n }\n : undefined,\n };\n\n payload = injectReferenceToRequestIfNeeded(payload);\n\n if (args.country == null) {\n args.country = \"pl\";\n }\n\n if (payload.details == null) {\n payload.details = {};\n }\n if (payload.details.address == null) {\n payload.details.address = {};\n }\n payload.details.address.country = args.country;\n\n if (args.marketingAgreement === true) {\n payload.additionalAgreements = [\n {\n id: MARKETING_AGREEMENT_ID,\n name: \"\",\n required: true,\n answer: true,\n },\n ];\n }\n\n const result = await fetch(buildURL(projectID, \"/buy\"), {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n });\n console.log(\"CODE\", result.status);\n\n if (result.status != 200) {\n console.warn(\n \"result\",\n await result.text(),\n result.status,\n result.statusText,\n );\n return Err(\"internal-error\", \"internal\");\n }\n\n const response: { url: string; sessionID: string } = await result.json();\n\n console.log(response.url);\n return {\n url: response.url,\n };\n },\n };\n};\n\nfunction resolveProductDetailsFromSingleProduct(\n id: string,\n product: SingleProduct,\n) {\n if (id == product.id) {\n return product;\n }\n\n const variant = product.variants?.find((v) => v.id === id);\n if (variant == null) {\n return product;\n }\n\n return {\n ...product,\n id: variant.id,\n name: product.name + \" \" + variant.name,\n price: variant.price,\n currency: variant.currency,\n metadata: product.metadata,\n availableQuantity: variant.availableQuantity,\n isDeliverable: variant.isDeliverable,\n images: variant.images ?? product.images,\n };\n}\n","export let HOST = \"https://bitsnap.pl\";\n\nexport function setCustomHost(host: string) {\n HOST = host;\n}\n","import { HOST } from \"./constants\";\n\nexport function buildURL(projectID: string, path: string): string {\n return `${HOST}/api/integrations/${projectID}/public-commerce${path}`;\n}\n","// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nexport type Err = {\n ERR: true\n error: unknown\n type?: ErrTypes\n}\n\ntype ErrTypes = 'internal' | 'badInput' | 'notFound';\n\nexport function isErr(x: unknown): x is Err {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return typeof x === 'object' && x != null && 'ERR' in x;\n}\n\nexport function Err(message: string, type?: ErrTypes): Err {\n return { ERR: true, error: message, type: type }\n}\n\nexport async function tryFail<T>(\n f: (() => Promise<T>) | (() => T)\n): Promise<T | Err> {\n try {\n return await f()\n } catch (e) {\n return { ERR: true, error: e }\n }\n}\n\nexport function assertOk<T>(x: T | Err) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if (isErr(x)) throw Error((x.error as any).toString());\n}\n","import { create } from 'zustand';\n\ninterface CheckoutStore {\n isCartVisible: boolean;\n showCart: () => void;\n hideCart: () => void;\n}\n\nexport const useCheckoutStore = create<CheckoutStore>((set) => ({\n isCartVisible: false,\n showCart: () => set((state) => ({ ...state, isCartVisible: true })),\n hideCart: () => set((state) => ({ ...state, isCartVisible: false })),\n}))\n","import { getCheckoutMethods, getProjectID } from \"./CartProvider\";\nimport { HOST } from \"./constants\";\nimport { buildURL } from \"./helper.methods\";\nimport { Err } from \"./lib/err\";\nimport { LinkRequest } from \"./link.request.schema\";\nimport { useCheckoutStore } from \"./state\";\n\n// deprecated, use Bitsnap.addProductToCart()\nexport async function addProductToCart(\n id: string,\n quantity: number = 1,\n metadata?: Record<string, string | undefined>,\n) {\n return Bitsnap.addProductToCart(id, quantity, metadata);\n}\n\n// deprecated, use Bitsnap.showCart()\nexport function showCart() {\n return Bitsnap.showCart();\n}\n\n// deprecated, use Bitsnap.hideCart()\nexport function hideCart() {\n return Bitsnap.hideCart();\n}\n\nexport namespace Bitsnap {\n export async function addProductToCart(\n id: string,\n quantity: number = 1,\n metadata?: Record<string, string | undefined>,\n ) {\n const projectID = getProjectID();\n if (projectID == null) {\n throw new Error(\"No project ID found\");\n }\n\n const methods = getCheckoutMethods(projectID);\n\n const err = await methods.addProduct({\n productID: id,\n quantity: quantity,\n metadata: metadata,\n });\n if (err != null) {\n return err;\n }\n\n return undefined;\n }\n\n export function showCart() {\n useCheckoutStore.setState({ isCartVisible: true });\n }\n\n export function hideCart() {\n useCheckoutStore.setState({ isCartVisible: false });\n }\n}\n\nexport async function createPaymentURL(request: LinkRequest) {\n const projectID = getProjectID();\n if (projectID == null) {\n throw new Error(\"No project ID found\");\n }\n\n request = injectReferenceToRequestIfNeeded(request);\n\n const result = await fetch(buildURL(projectID, \"/buy\"), {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(request),\n });\n\n if (result.status != 200) {\n console.warn(\n \"result\",\n await result.text(),\n result.status,\n result.statusText,\n );\n return Err(\"internal-error\", \"internal\");\n }\n\n const response: { url: string; sessionID: string } = await result.json();\n\n return {\n url: response.url,\n };\n}\n\nexport async function createCheckout(\n request: LinkRequest & { apiKey?: string; testMode?: boolean },\n) {\n const projectID = getProjectID();\n if (projectID == null) {\n throw new Error(\"No project ID found\");\n }\n\n const headers = {\n \"Content-Type\": \"application/json\",\n ...(request.apiKey != null\n ? { Authorization: `Bearer ${request.apiKey}` }\n : {}),\n };\n\n const path = request.testMode\n ? `/api/payment/link/auto/${projectID}/test`\n : `/api/payment/link/auto/${projectID}`;\n\n delete request.apiKey;\n delete request.testMode;\n\n const response = await fetch(HOST + path, {\n method: \"POST\",\n headers,\n body: JSON.stringify(request),\n });\n\n const payload: {\n url: string;\n } = await response.json();\n\n return {\n status: \"ok\",\n redirectURL: payload.url,\n };\n}\n\nfunction getReferenceIfPossible(): string | undefined {\n if (typeof localStorage == \"undefined\") {\n return undefined;\n }\n const refLink = localStorage.getItem(\"bitsnap-ref\");\n if (refLink == null) {\n return undefined;\n }\n return refLink;\n}\n\nexport function injectReferenceToRequestIfNeeded(\n request: LinkRequest,\n): LinkRequest {\n const ref = getReferenceIfPossible();\n if (ref == null) {\n return request;\n }\n\n if (request.metadata == null) {\n request.metadata = {};\n }\n request.metadata[\"ref\"] = ref;\n return request;\n}\n","import {AnimatePresence, motion} from \"framer-motion\";\nimport {type MutableRefObject, useEffect, useRef, useState} from \"react\";\n\nexport interface CountrySelectorProps {\n id: string;\n open: boolean;\n disabled?: boolean;\n onToggle: () => void;\n onChange: (value: string) => void;\n selectedValue: string;\n countries: { name: string; code: string }[];\n}\n\nfunction CountrySelector(\n {\n id,\n open,\n disabled = false,\n onToggle,\n onChange,\n selectedValue,\n countries\n }: CountrySelectorProps) {\n const ref = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const mutableRef = ref as MutableRefObject<HTMLDivElement | null>;\n\n const handleClickOutside = (event: MouseEvent) => {\n if (\n mutableRef.current &&\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-expect-error\n !mutableRef.current.contains(event.target) &&\n open\n ) {\n onToggle();\n setQuery(\"\");\n }\n };\n\n window.document.addEventListener(\"mousedown\", handleClickOutside);\n return () => {\n document.removeEventListener(\"mousedown\", handleClickOutside);\n };\n }, [ref]);\n\n useEffect(() => {\n if (selectedValue == null && countries.length > 0) {\n onChange(countries[0].code);\n }\n }, []);\n\n const [query, setQuery] = useState(\"\");\n\n return (\n <div ref={ref}>\n <div className=\"relative\">\n <button\n type=\"button\"\n className={`${\n disabled ? \"ics-bg-neutral-100\" : \"ics-bg-extra-light-white\"\n } ics-relative ics-w-full ics-rounded-2xl ics-shadow-sm ics-pl-8 ics-pr-10 ics-py-3 ics-text-left ics-cursor-default focus:ics-outline-none focus:ics-ring-1 focus:ics-ring-blue-500 focus:ics-border-blue-500 sm:ics-text-sm`}\n aria-haspopup=\"listbox\"\n aria-expanded=\"true\"\n aria-labelledby=\"listbox-label\"\n onClick={onToggle}\n disabled={disabled}\n >\n <span className=\"ics-truncate ics-flex ics-items-center\">\n <img\n alt={`${selectedValue}`}\n src={`https://purecatamphetamine.github.io/country-flag-icons/3x2/${selectedValue}.svg`}\n className={\"ics-inline ics-mr-2 ics-h-4 ics-rounded-sm\"}\n />\n {countries.find(el => el.code === selectedValue)?.name}\n </span>\n <span\n className={`ics-absolute ics-inset-y-0 ics-right-0 ics-flex ics-items-center ics-pr-2 ics-pointer-events-none ${\n disabled ? \"ics-hidden\" : \"\"\n }`}\n >\n <svg\n className=\"h-5 w-5 text-light-purple\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z\"\n clipRule=\"evenodd\"\n />\n </svg>\n </span>\n </button>\n\n <AnimatePresence>\n {open && (\n <motion.ul\n initial={{opacity: 0}}\n animate={{opacity: 1}}\n exit={{opacity: 0}}\n transition={{duration: 0.1}}\n className=\"ics-absolute ics-z-10 -ics-mt-80 ics-w-full dark:ics-bg-neutral-800 ics-bg-white ics-shadow-lg ics-max-h-80 ics-rounded-md ics-text-body-regular ics-ring-1 ics-ring-black ics-ring-opacity-5 focus:ics-outline-none sm:ics-text-body-regular\"\n tabIndex={-1}\n role=\"listbox\"\n aria-labelledby=\"listbox-label\"\n aria-activedescendant=\"listbox-option-3\"\n >\n <div className=\"ics-sticky ics-top-0 ics-z-10 ics-bg-white dark:ics-bg-neutral-800\">\n <li className=\"dark:ics-text-neutral-200 ics-text-neutral-900 ics-cursor-default ics-select-none ics-relative ics-py-2 ics-px-3\">\n <input\n type=\"search\"\n name=\"search\"\n autoComplete={\"off\"}\n className=\"ics-block ics-w-full ics-outline-none sm:ics-text-body-regular dark:ics-text-neutral-400 ics-text-dark-blue ics-bg-transparent ics-border-light-purple ics-rounded-md placeholder:ics-text-light-purple\"\n placeholder={\"Znajdź kraj\"}\n onChange={(e) => setQuery(e.target.value)}\n />\n </li>\n <hr/>\n </div>\n\n <div\n className={\n \"ics-max-h-64 ics-scrollbar ics-scrollbar-track-gray-100 ics-scrollbar-thumb-gray-300 hover:ics-scrollbar-thumb-gray-600 ics-scrollbar-thumb-rounded ics-scrollbar-thin ics-overflow-y-scroll\"\n }\n >\n {countries.filter((country) =>\n country.name.toLowerCase().startsWith(query.toLowerCase())\n ).length === 0 ? (\n <li className=\"ics-text-light-purple ics-cursor-default ics-select-none ics-relative ics-py-2 ics-pl-3 ics-pr-9\">\n No countries found\n </li>\n ) : (\n countries.filter((country) =>\n country.name.toLowerCase().startsWith(query.toLowerCase())\n ).map((value, index) => {\n return (\n <li\n key={`${id}-${index}`}\n className=\"ics-text-dark-blue ics-cursor-default ics-select-none ics-relative ics-py-2 ics-pl-3 ics-pr-9 ics-flex ics-items-center hover:ics-bg-extra-light-white ics-transition\"\n id=\"listbox-option-0\"\n role=\"option\"\n onClick={() => {\n onChange(value.code);\n setQuery(\"\");\n onToggle();\n }}\n >\n <img\n alt={`${value.code}`}\n src={`https://purecatamphetamine.github.io/country-flag-icons/3x2/${value.code}.svg`}\n className={\"ics-inline ics-mr-2 ics-h-4 ics-rounded-sm\"}\n />\n\n <span className=\"ics-font-normal ics-truncate\">\n {value.name}\n </span>\n {value.code === selectedValue ? (\n <span\n className=\"ics-text-blue-600 ics-absolute ics-inset-y-0 ics-right-0 ics-flex ics-items-center ics-pr-8\">\n <svg\n className=\"h-5 w-5\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n clipRule=\"evenodd\"\n />\n </svg>\n </span>\n ) : null}\n </li>\n );\n })\n )}\n </div>\n </motion.ul>\n )}\n </AnimatePresence>\n </div>\n </div>\n )\n}\n\n\nexport default CountrySelector","export function round(num: number, numberOfDecimals: number = 2): number {\n return Number(\n +(\n Math.round(Number(num + \"e+\" + numberOfDecimals)) +\n \"e-\" +\n numberOfDecimals\n )\n );\n}\n\nexport function formatCurrency(amount: number, currency: string): string {\n const formatter = Intl.NumberFormat(navigator.language, {\n style: \"currency\",\n currency: currency,\n currencyDisplay: 'symbol',\n });\n\n return formatter.format(amount / 100);\n}","const LoadingIndicator = ({className}: { className?: string }) => {\n return (\n <LoadingSpinner className={className ?? ''}/>\n );\n}\n\nexport default LoadingIndicator;\n\nconst LoadingSpinner = ({ size, color, className }: { size?: string; color?: string; className?: string }) => {\n const spinnerStyle = {\n borderTop: `4px solid ${color}`,\n borderLeft: `4px solid ${color}`,\n borderBottom: `4px solid ${color}`,\n borderRight: '4px solid transparent',\n width: size ?? '30px',\n height: size ?? '30px',\n borderRadius: '50%',\n };\n\n return <div className={className + ' animate-spin'} style={spinnerStyle}></div>;\n};\n","import { useEffect, useState } from \"react\";\nimport { formatCurrency } from \"./lib/round.number\";\nimport type { SingleProduct } from \"./product.details.model\";\n\n\nconst SingleProduct = ({quantity, details, shouldUpdate}: {\n quantity: number;\n metadata?: { [key: string]: string | undefined };\n details: SingleProduct;\n shouldUpdate: (newQuantity?: number) => void;\n}) => {\n\n return (\n <div className={'ics-flex ics-items-center ics-gap-3'}>\n <img className={'ics-aspect-auto ics-max-w-[30%]'} src={details.image_url ?? ''} alt={details.name}/>\n\n <div className={'ics-flex ics-flex-col'}>\n <p className={'ics-font-medium'}>{details.name}</p>\n <p className={'ics-text-sm'}>{formatCurrency(details.price, details.currency)}</p>\n <div className={'ics-flex ics-justify-between'}>\n <QuantityComponent className={\"\"} quantity={quantity} shouldUpdate={shouldUpdate}/>\n <button className={'ics-text-sm ics-font-medium'} onClick={() => shouldUpdate(0)}>Usuń</button>\n </div>\n </div>\n </div>\n );\n}\n\nexport default SingleProduct;\n\nconst QuantityComponent = (\n {\n quantity,\n shouldUpdate,\n className\n }: {\n quantity: number;\n shouldUpdate: (newQuantity: number) => void;\n className: string;\n }) => {\n const [quantityString, setQuantityString] = useState(quantity.toString());\n const [quantityValue, setQuantityValue] = useState(quantity);\n\n useEffect(() => {\n shouldUpdate(quantityValue);\n }, [quantityValue]);\n\n function setNewQuantity(newQuantity: string) {\n setQuantityString(newQuantity);\n if (newQuantity.length == 0) {\n return;\n }\n const parsedInt = parseInt(newQuantity);\n if (isNaN(parsedInt)) {\n setQuantityValue(1);\n setQuantityString('');\n return;\n }\n if (parsedInt == 0 || parsedInt < 0) {\n setQuantityValue(1);\n setQuantityString(\"1\");\n return;\n }\n setQuantityValue(parsedInt);\n setQuantityString(parsedInt.toString());\n }\n\n function increaseQuantity() {\n setQuantityValue(quantity + 1);\n setQuantityString((quantity + 1).toString());\n }\n\n function decreaseQuantity() {\n if (quantity > 1) {\n setQuantityString((quantity - 1).toString());\n setQuantityValue(quantity - 1);\n return;\n }\n setQuantityString(quantity.toString());\n setQuantityValue(quantity);\n }\n\n return (\n <div className={`ics-flex ${className} ics-items-center ics-border dark:ics-border-neutral-400 ics-border-neutral-700 ics-rounded-md ics-my-1`}>\n <button onClick={decreaseQuantity} className={'ics-px-2 ics-py-1'}>-</button>\n <input\n className={'ics-w-8 ics-bg-transparent ics-text-center'}\n value={quantityString}\n onInput={(e) => { setNewQuantity(e.currentTarget.value) }}\n type=\"text\"\n />\n <button onClick={increaseQuantity} className={'ics-px-2 ics-py-1'}>+</button>\n </div>\n );\n}\n","import { fromJson } from \"@bufbuild/protobuf\";\nimport crypto from \"crypto\";\nimport {\n type IntegrationEventJob,\n IntegrationEventJobSchema,\n} fro