diff --git a/blocks/subscriptions-block/_index.scss b/blocks/subscriptions-block/_index.scss index f58a46ee1c..c380d4f7ef 100644 --- a/blocks/subscriptions-block/_index.scss +++ b/blocks/subscriptions-block/_index.scss @@ -136,24 +136,40 @@ @include scss.block-properties("checkout-payment"); } - &__test { - &-1 { - @include scss.block-components("checkout-payment-test-1"); - @include scss.block-properties("checkout-payment-test-1"); + &__orderCard { + &--productPrice { + @include scss.block-components("checkout-order-card-productPrice"); + @include scss.block-properties("checkout-order-card-productPrice"); } - &-2 { - @include scss.block-components("checkout-payment-test-2"); - @include scss.block-properties("checkout-payment-test-2"); + &__card--features { + &--feature-item { + @include scss.block-components("checkout-order-card--features--item"); + @include scss.block-properties("checkout-order-card--features--item"); + } + @include scss.block-components("checkout-order-card--features"); + @include scss.block-properties("checkout-order-card--features"); } - &-row { - @include scss.block-components("checkout-payment-test-row"); - @include scss.block-properties("checkout-payment-test-row"); + &__summary { + &--dueToday{ + @include scss.block-components("checkout-order-card--summary--dueToday"); + @include scss.block-properties("checkout-order-card--summary--dueToday"); + } + &--details{ + &--item{ + @include scss.block-components("checkout-order-card--summary--details--item"); + @include scss.block-properties("checkout-order-card--summary--details--item"); + } + @include scss.block-components("checkout-order-card--summary--details"); + @include scss.block-properties("checkout-order-card--summary--details"); + } + @include scss.block-components("checkout-order-card--summary"); + @include scss.block-properties("checkout-order-card--summary"); } - @include scss.block-components("checkout-payment-test"); - @include scss.block-properties("checkout-payment-test"); + @include scss.block-components("checkout-order-card"); + @include scss.block-properties("checkout-order-card"); } @include scss.block-components("checkout"); diff --git a/blocks/subscriptions-block/components/OfferCard/index.jsx b/blocks/subscriptions-block/components/OfferCard/index.jsx index 7cd4adb89d..f026511480 100644 --- a/blocks/subscriptions-block/components/OfferCard/index.jsx +++ b/blocks/subscriptions-block/components/OfferCard/index.jsx @@ -2,7 +2,25 @@ import React from "react"; import PropTypes from "@arc-fusion/prop-types"; import { Heading, Button, Stack, Paragraph, Icon } from "@wpmedia/arc-themes-components"; -// TO-DO: Change the Icon to Check +export const FeatureDetails = ({ features, className }) => { + if (features.length) { + return ( + + ); + } + return null; +}; + const OfferCard = ({ headline, subHeadline, @@ -21,20 +39,7 @@ const OfferCard = ({ ) : null} - - {features.length ? ( -
    - {features.map((feat) => ( -
  • - - -
  • - ))} -
- ) : null} + ); @@ -46,8 +51,8 @@ OfferCard.propTypes = { features: PropTypes.arrayOf( PropTypes.shape({ featureText: PropTypes.string, - }) + }), ), }; -export default OfferCard; \ No newline at end of file +export default OfferCard; diff --git a/blocks/subscriptions-block/components/OfferCard/index.test-ignore.jsx b/blocks/subscriptions-block/components/OfferCard/index.test.jsx similarity index 57% rename from blocks/subscriptions-block/components/OfferCard/index.test-ignore.jsx rename to blocks/subscriptions-block/components/OfferCard/index.test.jsx index 24351184e1..2f5ac46676 100644 --- a/blocks/subscriptions-block/components/OfferCard/index.test-ignore.jsx +++ b/blocks/subscriptions-block/components/OfferCard/index.test.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { render } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import OfferCard from "."; const props = { @@ -24,51 +24,59 @@ describe("OfferCard", () => { expect(screen.getByRole("button")).not.toBeNull(); - const ul = getByRole("list"); - expect(ul).toBeInTheDocument(); + const ul = screen.getByRole("list"); + expect(ul.childElementCount).toBe(2); // eslint-disable-line - expect(screen.getByText(props.features[0].featureText)).not.toBeNull(); - expect(screen.getByText(props.features[1].featureText)).not.toBeNull(); + const ulByClass = document.getElementsByClassName("test-block__card--features--feature-item"); // eslint-disable-line + expect(ulByClass.length).toBe(2); + + expect(screen.getByText(props.features[0].featureText)).not.toBeNull(); // eslint-disable-line + expect(screen.getByText(props.features[1].featureText)).not.toBeNull(); // eslint-disable-line }); it("does not render headline if not present", () => { - const { container } = render( + render( , ); - expect(container.querySelector(".b-offer__card h1")).not.toBeInTheDocument(); + const headingElement = document.getElementsByClassName('.b-offer__card h1'); // eslint-disable-line + expect(headingElement.length).toBe(0) }); it("does not render subHeadline if not present", () => { - const { container } = render( + render( , ); - expect(container.querySelector(".b-offer__card p")).not.toBeInTheDocument(); + const headingElement = document.getElementsByClassName('.b-offer__card p'); // eslint-disable-line + expect(headingElement.length).toBe(0) }); it("does not render button if no actionText and no ActionEvent", () => { render(); - expect(screen.getByRole("button")).not.toBeNull(); + const button = screen.queryByRole("button"); + expect(button).toBeNull(); }); it("does not render button if no actionText", () => { render(); - expect(screen.getByRole("button")).not.toBeNull(); + const button = screen.queryByRole("button"); + expect(button).toBeNull(); }); it("does not render button if no actionEvent", () => { render(); - expect(screen.getByRole("button")).not.toBeNull(); + const button = screen.queryByRole("button"); + expect(button).toBeNull(); }); it("does not render features", () => { - const { container } = render(); + render(); - const features = expect(container.querySelector(".b-offer__card--features li")); - expect(features.length).toBe(0); + const ulByClass = document.getElementsByClassName("test-block__card--features--feature-item"); // eslint-disable-line + expect(ulByClass.length).toBe(0); }); }); diff --git a/blocks/subscriptions-block/components/OfferToProductList/index.jsx b/blocks/subscriptions-block/components/OfferToProductList/index.jsx index 2fc47c5a55..8ceb23ee03 100644 --- a/blocks/subscriptions-block/components/OfferToProductList/index.jsx +++ b/blocks/subscriptions-block/components/OfferToProductList/index.jsx @@ -1,9 +1,12 @@ import React from "react"; import PropTypes from "@arc-fusion/prop-types"; -import useSales from "../useSales"; import { Grid } from "@wpmedia/arc-themes-components"; +import useSales from "../useSales"; import OfferCard from "../OfferCard"; +export const ARCXP_CART = 'ArcXP_cart'; +export const ARCXP_CAMPAIGN = 'ArcXP_campaignName'; + const OfferToProductList = ({ offer, isLoggedIn, checkoutURL, loginURL, className }) => { const { Sales } = useSales(); @@ -21,7 +24,7 @@ const OfferToProductList = ({ offer, isLoggedIn, checkoutURL, loginURL, classNam products[productIdx].attributes.length !== 0 ? products[productIdx].attributes.map((feature) => ({ featureText: feature.value, - })) + })) : []; const { sku } = products[productIdx]; const { priceCode } = strategies[strategiesIdx]; @@ -32,21 +35,26 @@ const OfferToProductList = ({ offer, isLoggedIn, checkoutURL, loginURL, classNam actionText: strategies[strategiesIdx].summary, actionEvent: () => { Sales.clearCart() - .then(() => + .then(() => { Sales.addItemToCart([ { sku, priceCode, quantity: 1, }, - ]) - ) + ]); + const allValidUntil = offer?.campaigns?.map(c => c.validUntil !== undefined && !Number.isNaN(c.validUntil)); + const maxEndDate = allValidUntil.length ? Math.max(allValidUntil) : null; + const liveCampaing = offer?.campaigns?.find(c => c.validUntil === null || c.validUntil === maxEndDate); + localStorage.setItem(ARCXP_CAMPAIGN, liveCampaing?.name); + }) .then(() => { if (isLoggedIn) { window.location.href = checkoutURL; return; } - window.location.href = `${loginURL}?redirect=${checkoutURL}`; + localStorage.setItem(ARCXP_CART, JSON.stringify({sku, priceCode})); + window.location.href = `${loginURL}&redirect=${checkoutURL}`; }); }, features, @@ -75,7 +83,7 @@ const OfferToProductList = ({ offer, isLoggedIn, checkoutURL, loginURL, classNam className={className} /> ))} - + ); }; diff --git a/blocks/subscriptions-block/components/OfferToProductList/index.test-ignore.jsx b/blocks/subscriptions-block/components/OfferToProductList/index.test.jsx similarity index 93% rename from blocks/subscriptions-block/components/OfferToProductList/index.test-ignore.jsx rename to blocks/subscriptions-block/components/OfferToProductList/index.test.jsx index 8aceae2519..30c9ad38f7 100644 --- a/blocks/subscriptions-block/components/OfferToProductList/index.test-ignore.jsx +++ b/blocks/subscriptions-block/components/OfferToProductList/index.test.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { act, render } from "@testing-library/react"; +import {render, waitFor } from "@testing-library/react"; import OfferToProductList from "./index"; import useOffer from "../useOffer"; import OfferCard from "../OfferCard"; @@ -177,31 +177,34 @@ jest.mock("fusion:properties", () => script: "https://corecomponents-the-gazette-prod.cdn.arcpublishing.com/arc/subs/p.min.js", }, }, - })) + })), ); +jest.mock("../OfferCard", () => + (jest.fn(() => null)) + ); + jest.mock("@arc-publishing/sdk-sales"); jest.mock("../../components/useOffer"); + useOffer.mockReturnValue({ offer: sampleOffer, fetchOffer: () => sampleOffer, }); describe("The OfferToProductList component", () => { - it("renders the correct number of offer cards", () => { - const { container } = render( + it("renders the correct number of offer cards", async () => { + render( + />, ); - act(() => { - container.setProps({}); - }); + const mockedChildComponent = OfferCard; - expect(container.find(OfferCard)).toHaveLength(4); + await waitFor(() => expect(mockedChildComponent).toHaveBeenCalledTimes(4)); }); -}); \ No newline at end of file +}); diff --git a/blocks/subscriptions-block/components/OrderInformation/index.jsx b/blocks/subscriptions-block/components/OrderInformation/index.jsx new file mode 100644 index 0000000000..77e83e6970 --- /dev/null +++ b/blocks/subscriptions-block/components/OrderInformation/index.jsx @@ -0,0 +1,87 @@ +import React from "react"; + +import { usePhrases, Link, Heading, Paragraph, Stack } from "@wpmedia/arc-themes-components"; +import { FeatureDetails } from "../OfferCard"; +import currency from "../../utils/currency"; + +const OrderSummary = ({ orderDetails, className }) => { + const phrases = usePhrases(); + + return ( + + {phrases.t("checkout-block.order-summary")} + + +

{phrases.t("checkout-block.subtotal")}

+

{`${currency(orderDetails?.currency)}${orderDetails?.subtotal}`}

+
+ +

{phrases.t("checkout-block.salesTax")}

+

+ {orderDetails?.tax > 0 + ? `${currency(orderDetails?.currency)}${orderDetails?.tax}` + : "--"} +

+
+
+
+

{phrases.t("checkout-block.due-today")}

+

{`${currency(orderDetails?.currency)}${orderDetails?.total}`}

+
+
+ ); +}; + +const ProductPriceDetails = ({ + details = [], + showPriceDescription, + showProductFeatures, + className, +}) => { + if (details?.items?.length) { + return details?.items?.map((item) => + + {item.priceName} + {showPriceDescription && ( + + )} + {showProductFeatures && ( + + )} + + ); + } + return null; +}; + +const OrderInformation = ({ + id, + offerURL, + showOfferURL, + showPriceDescription, + showProductFeatures, + orderDetails, + className, +}) => { + const phrases = usePhrases(); + + return ( +
+ + {showOfferURL && ( + {phrases.t("checkout-block.view-subscription-offers")} + )} + +
+ ); +}; + +export default OrderInformation; diff --git a/blocks/subscriptions-block/components/OrderInformation/index.test.jsx b/blocks/subscriptions-block/components/OrderInformation/index.test.jsx new file mode 100644 index 0000000000..3246b92ef0 --- /dev/null +++ b/blocks/subscriptions-block/components/OrderInformation/index.test.jsx @@ -0,0 +1,174 @@ +import React from "react"; +import { render, screen, within } from "@testing-library/react"; +import '@testing-library/jest-dom'; +import OrderInformation from "./index"; +import currency from "../../utils/currency"; + +const orderDetails = { + currency: "COP", + shipping: 0, + subtotal: 20000, + tax: 100, + taxSupported: true, + total: 20100, + items: [ + { + name: "COP Currency", + price: 20000, + priceCode: "Q6R7UO", + priceDescription: "

with tax description price

", + priceName: "All access Annual", + priceSummary: "

with tax summary price

", + productDescription: "

COP Currency description

", + quantity: 1, + shortDescription: "

COP Currency description

", + sku: "0987", + subtotal: 20100, + tax: 0, + taxInclusive: undefined, + total: 20000, + productAttributes: [ + { + featureText: "

Unlimited access to The Daily Intelligencer

", + }, + { + featureText: "

Save $40

", + }, + { + featureText: "

A bonus subscription to share

", + }, + ], + }, + ], +}; + +jest.mock("@arc-publishing/sdk-identity", () => ({ + __esModule: true, + default: { + apiOrigin: "", + options: jest.fn(), + }, +})); + +jest.mock("fusion:properties", () => + jest.fn(() => ({ + api: { + identity: { + origin: "https://corecomponents-arc-demo-3-prod.api.cdn.arcpublishing.com", + }, + retail: { + origin: "https://corecomponents-arc-demo-3-prod.api.cdn.arcpublishing.com", + endpoint: "/retail/public/v1/offer/live/", + }, + }, + })), +); + +jest.mock("fusion:context", () => ({ + __esModule: true, + useFusionContext: () => ({ + arcSite: "Test Site", + }), +})); + +const className = 'checkout'; + +describe('Order Information component', () => { + it("renders order info", () => { + const showProductF = true; + render( + , + ); + expect(screen.getByText(orderDetails?.items?.[0]?.priceName)).toBeVisible(); + expect(screen.getByText("checkout-block.order-summary")).toBeVisible(); + expect(screen.getByText("checkout-block.subtotal")).toBeVisible(); + expect( + screen.getByText(`${currency(orderDetails?.currency)}${orderDetails?.subtotal}`), + ).toBeVisible(); + expect(screen.getByText("checkout-block.salesTax")).toBeVisible(); + expect( + screen.getByText(`${currency(orderDetails?.currency)}${orderDetails?.total}`), + ).toBeVisible(); + expect(screen.getByText("checkout-block.due-today")).toBeVisible(); + + const headingElement = document.getElementsByClassName('c-heading'); // eslint-disable-line + expect(headingElement.length).toBe(2) + }); + + it("renders price description", () => { + const showPriceDesc = true; + const showProductFeat = true; + render( + , + ); + expect(screen.getByText("with tax description price")).toBeVisible(); + }); + + it("renders features", () => { + const showOfferUrl = true + const showPriceDesc = false; + const showProductFeat = true; + render( + , + ); + const list = screen.getByRole("list"); + const { getAllByRole } = within(list); + const items = getAllByRole("listitem"); + expect(items.length).toBe(3); + }); + + it("renders link to offer", () => { + const showOfferUrl = true + const showPriceDesc = false; + const showProductFeat = true; + render( + , + ); + expect( + screen.getByRole("link", { name: "checkout-block.view-subscription-offers" }), + ).toHaveAttribute("href", "/offer/"); + + }); + + it("items is empty", ()=>{ + const showOfferUrl = true + const showPriceDesc = false; + const showProductFeat = true; + render( + , + ); + const headingElement = document.getElementsByClassName('c-heading'); // eslint-disable-line + expect(headingElement.length).toBe(1) + }) +}); diff --git a/blocks/subscriptions-block/components/useOffer.test-ignore.jsx b/blocks/subscriptions-block/components/useOffer.test-ignore.jsx deleted file mode 100644 index 6c9d489c98..0000000000 --- a/blocks/subscriptions-block/components/useOffer.test-ignore.jsx +++ /dev/null @@ -1,195 +0,0 @@ -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -jest.mock("fusion:context", () => ({ - useFusionContext: jest.fn(() => ({ - arcSite: "arc-demo-3", - })), -})); - -jest.mock("fusion:properties", () => - jest.fn(() => ({ - api: { - retail: { - origin: "https://corecomponents-arc-demo-3-prod.api.cdn.arcpublishing.com", - endpoint: "/retail/public/v1/offer/live/", - }, - }, - })) -); - -const testOfferResponse = { - name: "Premium Offer", - disclaimerText: null, - largeImage: null, - mediumImage: null, - smallImage: null, - pageSubTitle: "

Free trial: first month free, then $10/mo

", - pageTitle: "

Default subscription

", - templateName: "bottom-drawer", - campaigns: [ - { - canRenew: true, - canRestart: true, - canStart: true, - name: "augpromo", - validFrom: 1629216600000, - validUntil: 1630468800000, - }, - ], - products: [ - { - sku: "premium", - description: "

Get access to premium content

", - image: null, - imageAction: null, - name: "Premium Content", - thumbnail: null, - maxSubscriptionAssociations: 0, - attributes: [ - { - name: "p", - value: "

get access to sports, business, cooking sections

", - }, - ], - pricingStrategies: [ - { - pricingStrategyId: 1169, - priceCode: "1B1HCQ", - name: "Premium free trial", - description: "

free trial price

", - gift: false, - summary: null, - currencyCode: "USD", - currencyDisplayFormat: "symbol", - currencyLocale: "en-US", - rates: [ - { - amount: "0.00", - billingCount: 1, - billingFrequency: "Month", - durationCount: 1, - duration: "Month", - }, - { - amount: "10.00", - billingCount: 1, - billingFrequency: "Month", - durationCount: 1, - duration: "UntilCancelled", - }, - ], - taxInclusive: false, - }, - { - pricingStrategyId: 1168, - priceCode: "H7SCJB", - name: "premium all access", - description: "

Get access to premium content for a low rate

", - gift: false, - summary: null, - currencyCode: "USD", - currencyDisplayFormat: "symbol", - currencyLocale: "en-US", - rates: [ - { - amount: "10.00", - billingCount: 1, - billingFrequency: "Month", - durationCount: 1, - duration: "UntilCancelled", - }, - ], - taxInclusive: false, - }, - ], - defaultSwgProduct: false, - }, - ], - attributes: [], - default: true, -}; - -function TestOfferComponent({ code }) { - const { offer, isFetching, error } = useOffer({ - campaignCode: code, - }); - if (error) { - return
Error: {error}
; - } - if (isFetching || !offer) { - return
Fetching
; - } - return
; -} - -global.fetch = jest.fn(() => - Promise.resolve({ - json: () => Promise.resolve(testOfferResponse), - }) -); - -let container = null; -beforeEach(() => { - jest.clearAllMocks(); - fetch.mockClear(); - container = document.createElement("div"); - document.body.appendChild(container); -}); - -afterEach(() => { - unmountComponentAtNode(container); - container.remove(); - container = null; -}); - -it("useOffer hook runs correctly", async () => { - act(() => { - render( - , - container - ); - }); - expect(container.textContent).toBe("Fetching"); - await act(() => sleep(500)); - expect(container.textContent).toBe("Default subscription"); -}); - -it("use offer uses fallback default string", async () => { - act(() => { - render( - , - container - ); - }); - expect(container.textContent).toBe("Fetching"); - await act(() => sleep(500)); - expect(container.textContent).toBe("Default subscription"); -}); - -it("useOffer hook handles an error state during fetch", async () => { - global.fetch = jest.fn(() => Promise.reject(new Error("Error #2"))); - - act(() => { - render( - , - container - ); - }); - expect(container.textContent).toBe("Fetching"); - await act(() => sleep(500)); - expect(container.textContent).toContain("Error #2"); -}); \ No newline at end of file diff --git a/blocks/subscriptions-block/components/useOffer.test.jsx b/blocks/subscriptions-block/components/useOffer.test.jsx new file mode 100644 index 0000000000..d139d64003 --- /dev/null +++ b/blocks/subscriptions-block/components/useOffer.test.jsx @@ -0,0 +1,184 @@ +import { waitFor, renderHook } from "@testing-library/react"; +import useOffer from "./useOffer"; + +jest.mock("fusion:context", () => ({ + useFusionContext: jest.fn(() => ({ + arcSite: "arc-demo-3", + })), +})); + +const testOfferResponse = { + name: "Premium Offer", + disclaimerText: null, + largeImage: null, + mediumImage: null, + smallImage: null, + pageSubTitle: "

Free trial: first month free, then $10/mo

", + pageTitle: "

Default subscription

", + templateName: "bottom-drawer", + campaigns: [ + { + canRenew: true, + canRestart: true, + canStart: true, + name: "augpromo", + validFrom: 1629216600000, + validUntil: 1630468800000, + }, + ], + products: [ + { + sku: "premium", + description: "

Get access to premium content

", + image: null, + imageAction: null, + name: "Premium Content", + thumbnail: null, + maxSubscriptionAssociations: 0, + attributes: [ + { + name: "p", + value: "

get access to sports, business, cooking sections

", + }, + ], + pricingStrategies: [ + { + pricingStrategyId: 1169, + priceCode: "1B1HCQ", + name: "Premium free trial", + description: "

free trial price

", + gift: false, + summary: null, + currencyCode: "USD", + currencyDisplayFormat: "symbol", + currencyLocale: "en-US", + rates: [ + { + amount: "0.00", + billingCount: 1, + billingFrequency: "Month", + durationCount: 1, + duration: "Month", + }, + { + amount: "10.00", + billingCount: 1, + billingFrequency: "Month", + durationCount: 1, + duration: "UntilCancelled", + }, + ], + taxInclusive: false, + }, + { + pricingStrategyId: 1168, + priceCode: "H7SCJB", + name: "premium all access", + description: "

Get access to premium content for a low rate

", + gift: false, + summary: null, + currencyCode: "USD", + currencyDisplayFormat: "symbol", + currencyLocale: "en-US", + rates: [ + { + amount: "10.00", + billingCount: 1, + billingFrequency: "Month", + durationCount: 1, + duration: "UntilCancelled", + }, + ], + taxInclusive: false, + }, + ], + defaultSwgProduct: false, + }, + ], + attributes: [], + default: true, +}; + +global.fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve(testOfferResponse), + }), +); + +jest.mock("fusion:properties", () => { + const retailOrigin = "https://corecomponents-arc-demo-3-prod.api.cdn.arcpublishing.com"; + const retailEndpoint = "/retail/public/v1/offer/live/"; + return jest.fn(() => ({ + __esModule: true, + api: { + retail: { + origin: retailOrigin, + endpoint: retailEndpoint, + }, + }, + })); +}); + +describe("useOffer", () => { + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + it("useOffer hook runs correctly", async () => { + const { result } = renderHook(() => useOffer({ campaignCode: "default" })); + + expect(result.current.error).toBe(null); + expect(result.current.offer).toBe(null); + expect(result.current.isFetching).toBe(true); + + await waitFor(() => expect(result.current.error).toEqual(null)); + await waitFor(() => expect(result.current.offer).toEqual(testOfferResponse)); + await waitFor(() => expect(result.current.isFetching).toEqual(false)); + }); + + it("use offer uses fallback default string", async () => { + const { result } = renderHook(() => useOffer({ campaignCode: "" })); + + expect(result.current.error).toBe(null); + expect(result.current.offer).toBe(null); + expect(result.current.isFetching).toBe(true); + + await waitFor(() => expect(result.current.error).toEqual(null)); + await waitFor(() => expect(result.current.offer).toEqual(testOfferResponse)); + await waitFor(() => expect(result.current.isFetching).toEqual(false)); + }); + + it("useOffer hook handles an error state during fetch", async () => { + global.fetch = jest.fn().mockRejectedValue(new Error("fetch error")); + + const { result } = renderHook(() => useOffer({ campaignCode: "default" })); + + expect(result.current.error).toBe(null); + expect(result.current.offer).toBe(null); + expect(result.current.isFetching).toBe(true); + + await waitFor(() => + expect(result.current.error).toEqual("Error in fetching retail offers: Error: fetch error"), + ); + await waitFor(() => expect(result.current.offer).toEqual(null)); + await waitFor(() => expect(result.current.isFetching).toEqual(false)); + }); + + it("useOffer hook handles an error state during fetch", async () => { + global.fetch = jest.fn().mockRejectedValue(new Error("fetch error")); + + const { result } = renderHook(() => useOffer({ campaignCode: "default" })); + + expect(result.current.error).toBe(null); + expect(result.current.offer).toBe(null); + expect(result.current.isFetching).toBe(true); + + await waitFor(() => + expect(result.current.error).toEqual("Error in fetching retail offers: Error: fetch error"), + ); + await waitFor(() => expect(result.current.offer).toEqual(null)); + await waitFor(() => expect(result.current.isFetching).toEqual(false)); + }); +}); + diff --git a/blocks/subscriptions-block/components/useOrder.jsx b/blocks/subscriptions-block/components/useOrder.jsx new file mode 100644 index 0000000000..766bf8913e --- /dev/null +++ b/blocks/subscriptions-block/components/useOrder.jsx @@ -0,0 +1,152 @@ +import { useState, useEffect } from "react"; + +import { useSales } from "@wpmedia/arc-themes-components"; +import { ARCXP_CAMPAIGN } from "./OfferToProductList"; +import useOffer from "./useOffer"; + +const useOrder = (orderNumber) => { + const { Sales } = useSales(); + + const [order, setOrder] = useState(); + const [orderDetails, setOrderDetails] = useState(); + const [cart, setCart] = useState(); + const [cartDetails, setCartDetails] = useState(); + const [error, setError] = useState(); + + const [campaignName, setCampaignName] = useState(); + + const { offer } = useOffer({ + campaignCode: campaignName, + }); + + useEffect(() => { + const getCart = async () => { + try { + const currentCart = await Sales.getCart(); + if(currentCart?.items?.length){ + setCart(currentCart); + } + } catch (e) { + setError(e); + } + }; + getCart(); + + const campaignNameStored = localStorage.getItem(ARCXP_CAMPAIGN); + setCampaignName(campaignNameStored); + // eslint-disable-next-line + }, []); + + useEffect(() => { + const getOrder = async () => { + try { + const currentOrder = await Sales.getOrderDetails(orderNumber); + if(currentOrder?.items?.length){ + setOrder(currentOrder); + } + } catch (e) { + setError(e); + } + }; + if (orderNumber) { + getOrder(); + } + // eslint-disable-next-line + }, [orderNumber]); + + useEffect(() => { + const getProductPriceDetailsFromOffer = (sku, priceCode) => { + let productAttributes; + let productDescription; + let priceName; + let priceDescription; + let priceSummary; + let rates; + + const productBySku = offer?.products?.find((item) => item?.sku === sku); + + if (productBySku) { + productAttributes = + typeof productBySku?.attributes !== "undefined" && productBySku?.attributes?.length !== 0 + ? productBySku?.attributes.map((feature) => ({ + featureText: feature.value, + })) + : []; + productDescription = productBySku?.description; + const pricingStrategy = productBySku?.pricingStrategies?.find( + (price) => price?.priceCode === priceCode, + ); + + priceName = pricingStrategy?.name; + priceDescription = pricingStrategy?.description; + priceSummary = pricingStrategy?.summary; + rates = pricingStrategy?.rates; + } + + return { + productAttributes, + productDescription, + priceName, + priceDescription, + priceSummary, + rates, + }; + }; + + if (offer) { + if (order) { + const itemsDetail = order?.items?.map((item) => { + const { + productAttributes, + productDescription, + priceName, + priceDescription, + priceSummary, + rates, + } = getProductPriceDetailsFromOffer(item?.sku, item?.priceCode); + return { + ...item, + productAttributes, + productDescription, + priceName, + priceDescription, + priceSummary, + rates, + }; + }); + setOrderDetails({ ...order, items: itemsDetail }); + } else if (cart) { + const itemsDetail = cart?.items?.map((item) => { + const { + productAttributes, + productDescription, + priceName, + priceDescription, + priceSummary, + rates, + taxInclusive, + } = getProductPriceDetailsFromOffer(item?.sku, item?.priceCode); + return { + ...item, + productAttributes, + productDescription, + priceName, + priceDescription, + priceSummary, + rates, + taxInclusive, + }; + }); + setCartDetails({ ...cart, items: itemsDetail }); + } + } + }, [order, cart, offer]); + + return { + cartDetails, + orderDetails, + error, + }; +}; + +export default useOrder; diff --git a/blocks/subscriptions-block/components/useOrder.test.jsx b/blocks/subscriptions-block/components/useOrder.test.jsx new file mode 100644 index 0000000000..1290d634a5 --- /dev/null +++ b/blocks/subscriptions-block/components/useOrder.test.jsx @@ -0,0 +1,387 @@ +import { useSales } from "@wpmedia/arc-themes-components"; +import { waitFor, renderHook } from "@testing-library/react"; + +import useOrder from "./useOrder"; +import useOffer from "./useOffer"; + +// Mock setTimeout and clearTimeout +jest.useFakeTimers(); + +jest.mock("@arc-publishing/sdk-identity", () => ({ + __esModule: true, + default: { + apiOrigin: "", + options: jest.fn(), + }, +})); + +jest.mock("fusion:properties", () => + jest.fn(() => ({ + api: { + identity: { + origin: "https://corecomponents-arc-demo-3-prod.api.cdn.arcpublishing.com", + }, + retail: { + origin: "https://corecomponents-arc-demo-3-prod.api.cdn.arcpublishing.com", + endpoint: "/retail/public/v1/offer/live/", + }, + }, + })), +); + +jest.mock("fusion:context", () => ({ + __esModule: true, + useFusionContext: () => ({ + arcSite: "Test Site", + }), +})); + +const cartDetail = { + total: 20000, + subtotal: 20000, + tax: 0, + shipping: 0, + currency: "COP", + taxSupported: true, + items: [ + { + sku: "0987", + quantity: 1, + shortDescription: "

COP Currency description

", + name: "COP Currency", + price: 20000, + tax: 0, + subtotal: 20000, + total: 20000, + priceCode: "Q6R7UO", + eventId: null, + ownerClientId: null, + attributes: [], + gift: false, + }, + ], +}; + +const mockSales = { + getCart: jest.fn(() => Promise.resolve(cartDetail)), + getOrderDetails: jest.fn(() => {}), +}; + +jest.mock("@wpmedia/arc-themes-components", () => ({ + ...jest.requireActual("@wpmedia/arc-themes-components"), + useIdentity: jest.fn(() => ({ + isInitialized: true, + })), + useSales: jest.fn(() => ({ + isInitialized: true, + Sales: { + ...mockSales, + }, + })), +})); + +const localStorageMock = (() => { + let store = {}; + + return { + getItem: (key) => store[key], + setItem: (key, value) => { + store[key] = value.toString(); + }, + clear: () => { + store = {}; + }, + }; +})(); + +const sampleOffer = { + name: "test_campaign", + disclaimerText: null, + largeImage: null, + mediumImage: null, + smallImage: null, + pageSubTitle: "

test_campaign

", + pageTitle: "

test_campaign

", + templateName: "test-template", + campaigns: [ + { + canRenew: true, + canRestart: true, + canStart: true, + name: "demoCampaign", + validFrom: 1710888000000, + validUntil: 1711060801000, + }, + ], + products: [ + { + sku: "0987", + description: "

COP Currency description

", + image: null, + imageAction: null, + name: "COP Currency", + thumbnail: null, + maxSubscriptionAssociations: 0, + attributes: [ + { + name: "test-style", + value: "

Unlimited access to The Daily Intelligencer

", + }, + { + name: "test-style", + value: "

Save $40

", + }, + { + name: "test-style", + value: "

A bonus subscription to share

", + }, + ], + pricingStrategies: [ + { + pricingStrategyId: 4186, + priceCode: "Q6R7UO", + name: "All access Annual", + description: "

with tax description price

", + gift: false, + summary: "

with tax summary price

", + currencyCode: "COP", + currencyDisplayFormat: "symbol", + currencyLocale: "es-CO", + rates: [ + { + amount: "20000.00", + billingCount: 1, + billingFrequency: "Month", + durationCount: 1, + duration: "UntilCancelled", + }, + ], + taxInclusive: false, + }, + { + pricingStrategyId: 4187, + priceCode: "Z53LGY", + name: "taxIncluded priceName", + description: "

tax included priceDescription

", + gift: false, + summary: "

tax included priceSummary

", + currencyCode: "COP", + currencyDisplayFormat: "symbol", + currencyLocale: "es-CO", + rates: [ + { + amount: "40000.00", + billingCount: 1, + billingFrequency: "Month", + durationCount: 1, + duration: "UntilCancelled", + }, + ], + taxInclusive: true, + }, + ], + defaultSwgProduct: false, + }, + ], + attributes: [], + default: true, +}; + +jest.mock("./useOffer"); +useOffer.mockReturnValue({ + offer: sampleOffer, + fetchOffer: () => sampleOffer, + isFetching: false, +}); + +const emptyCart = { + total: 0, + subtotal: 0, + tax: 0, + shipping: 0, + items: [], + currency: "USD", + taxSupported: true, +}; + +describe("The OfferToProductList component", () => { + beforeEach(() => { + Object.defineProperty(window, "localStorage", { + value: localStorageMock, + }); + localStorage.setItem("ArcXP_campaignName", "demoCampaign"); + }); + + it("User has a cart", async () => { + const { result } = renderHook(() => useOrder()); + + expect(result.current.cartDetails).toBe(undefined); + expect(result.current.orderDetails).toBe(undefined); + expect(result.current.error).toBe(undefined); + + const newCartDetails = { + ...cartDetail, + items: [ + { + ...cartDetail?.items?.[0], + productAttributes: [ + { featureText: "

Unlimited access to The Daily Intelligencer

" }, + { + featureText: "

Save $40

", + }, + { + featureText: "

A bonus subscription to share

", + }, + ], + productDescription: "

COP Currency description

", + priceName: "All access Annual", + priceDescription: "

with tax description price

", + priceSummary: "

with tax summary price

", + rates: [ + { + amount: "20000.00", + billingCount: 1, + billingFrequency: "Month", + durationCount: 1, + duration: "UntilCancelled", + }, + ], + }, + ], + }; + + await waitFor(() => expect(result.current.cartDetails).toEqual(newCartDetails)); + await waitFor(() => expect(result.current.orderDetails).toEqual(undefined)); + await waitFor(() => expect(result.current.error).toEqual(undefined)); + }); + + it("User has order", async () => { + + const orderDetail = { + total: 22000, + subtotal: 20000, + tax: 2000, + shipping: 0, + items: [ + { + sku: "0987", + quantity: 1, + shortDescription: "

COP Currency description

", + name: "COP Currency", + price: 20000, + tax: 2000, + subtotal: 20000, + total: 22000, + priceCode: "Q6R7UO", + eventId: null, + ownerClientId: null, + attributes: [], + gift: false, + }, + ], + currency: "COP", + orderNumber: "6OGJP2GGVJ7GT8L3", + status: "Pending", + email: "laurapinb@gmail.com", + phone: "NA", + firstName: "laura", + lastName: "pin", + orderDateUTC: 1711936392300, + taxDelegated: false, + paymentPending: false, + }; + + useSales.mockReturnValueOnce({ + isInitialized: true, + Sales: { + getCart: jest.fn(() => Promise.resolve(emptyCart)), + getOrderDetails: jest.fn(() => + Promise.resolve(orderDetail) + ), + }, + }); + + const newOrderDetail = { + ...orderDetail, + items: [ + { + ...orderDetail?.items?.[0], + productAttributes: [ + { featureText: "

Unlimited access to The Daily Intelligencer

" }, + { + featureText: "

Save $40

", + }, + { + featureText: "

A bonus subscription to share

", + }, + ], + productDescription: "

COP Currency description

", + priceName: "All access Annual", + priceDescription: "

with tax description price

", + priceSummary: "

with tax summary price

", + rates: [ + { + amount: "20000.00", + billingCount: 1, + billingFrequency: "Month", + durationCount: 1, + duration: "UntilCancelled", + }, + ], + }, + ], + }; + + const { result } = renderHook(() => useOrder("6OGJP2GGVJ7GT8L3")); + + expect(result.current.cartDetails).toBe(undefined); + expect(result.current.orderDetails).toBe(undefined); + expect(result.current.error).toBe(undefined); + + await waitFor(() => expect(result.current.cartDetails).toEqual(undefined)); + await waitFor(() => expect(result.current.orderDetails).toEqual(newOrderDetail)); + await waitFor(() => expect(result.current.error).toEqual(undefined)); + }); + + it("getOrderDetail is returning an error", async () => { + const error = { code: "200019", message: "Access Denied" }; + useSales.mockReturnValueOnce({ + isInitialized: true, + Sales: { + getCart: jest.fn(() => Promise.resolve(emptyCart)), + getOrderDetails: jest.fn().mockRejectedValueOnce(error), + }, + }); + + const { result } = renderHook(() => useOrder("6OGJP2GGVJ7GT8L3")); + + expect(result.current.cartDetails).toBe(undefined); + expect(result.current.orderDetails).toBe(undefined); + expect(result.current.error).toBe(undefined); + + await waitFor(() => expect(result.current.cartDetails).toEqual(undefined)); + await waitFor(() => expect(result.current.orderDetails).toEqual(undefined)); + await waitFor(() => expect(result.current.error).toEqual(error)); + }); + + it("getCart is returning an error", async () => { + const error = { code: "0", message: "Unexpected Server error" }; + useSales.mockReturnValueOnce({ + isInitialized: true, + Sales: { + getCart: jest.fn().mockRejectedValueOnce(error), + getOrderDetails: jest.fn(() => {}), + }, + }); + + const { result } = renderHook(() => useOrder()); + + expect(result.current.cartDetails).toBe(undefined); + expect(result.current.orderDetails).toBe(undefined); + expect(result.current.error).toBe(undefined); + + await waitFor(() => expect(result.current.cartDetails).toEqual(undefined)); + await waitFor(() => expect(result.current.orderDetails).toEqual(undefined)); + await waitFor(() => expect(result.current.error).toEqual(error)); + }); +}); diff --git a/blocks/subscriptions-block/intl.json b/blocks/subscriptions-block/intl.json index 07358dc45b..9f22f06403 100644 --- a/blocks/subscriptions-block/intl.json +++ b/blocks/subscriptions-block/intl.json @@ -6848,5 +6848,14 @@ "vi": "Cập nhật phương thức thanh toán", "zh-CN": "更新付款方式", "zh-TW": "更新付款方式" + }, + "checkout-block.view-subscription-offers": { + "en": "View subscription offers" + }, + "checkout-block.subtotal": { + "en": "Subtotal" + }, + "checkout-block.salesTax": { + "en": "Sales tax" } } \ No newline at end of file diff --git a/blocks/subscriptions-block/themes/news.json b/blocks/subscriptions-block/themes/news.json index a1223d662f..5c8940e0c7 100644 --- a/blocks/subscriptions-block/themes/news.json +++ b/blocks/subscriptions-block/themes/news.json @@ -401,6 +401,126 @@ } } }, + "checkout-order-card": { + "styles": { + "default": { + "display": "flex", + "flex-direction": "column", + "padding-block-start": "var(--global-spacing-4)", + "padding-inline-end": "var(--global-spacing-4)", + "padding-block-end": "var(--global-spacing-4)", + "padding-inline-start": "var(--global-spacing-4)", + "gap": "var(--global-spacing-5)", + "column-gap": "var(--global-spacing-5)", + "components": { + "link": { + "color": "var(--text-color)", + "text-decoration": "underline", + "font-size": "var(--global-font-size-4)", + "font-weight": "var(--global-font-weight-4)" + } + } + } + } + }, + "checkout-order-card-productPrice": { + "styles": { + "default": { + "display": "flex", + "flex-direction": "column", + "gap": "var(--global-spacing-5)", + "column-gap": "var(--global-spacing-5)", + "components": { + "heading": { + "font-size": "var(--global-font-size-9)", + "font-weight": "var(--global-font-weight-7)", + "line-height": "var(--global-line-height-6)" + } + } + } + } + }, + "checkout-order-card--features": { + "styles": { + "default": { + "align-self": "baseline" + } + } + }, + "checkout-order-card--features--item": { + "styles": { + "default": { + "align-items": "center", + "display": "flex", + "margin-block-end": "var(--global-spacing-2)", + "components": { + "icon": { + "fill": "var(--status-color-success)", + "display": "flex", + "inline-size": "var(--global-spacing-4)", + "block-size": "var(--global-spacing-4)", + "margin-block-start": "0", + "margin-inline-end": "var(--global-spacing-2)", + "margin-block-end": "0", + "margin-inline-start": "0" + } + } + } + } + }, + "checkout-order-card--summary": { + "styles": { + "default": { + "gap": "var(--global-spacing-2)", + "components": { + "heading": { + "font-family": "var(--font-family-secondary)", + "font-weight": "var(--global-font-weight-7)", + "font-size": "var(--global-font-size-4)", + "padding-block-end": "var(--global-spacing-2)", + "border-block-end-width": "1px", + "border-block-end-style": "solid", + "border-block-end-color": "var(--border-color)" + } + } + } + } + }, + "checkout-order-card--summary--dueToday": { + "styles": { + "default": { + "display": "flex", + "justify-content": "space-between", + "padding-block-start": "var(--global-spacing-2)", + "border-block-start-width": "1px", + "border-block-start-style": "solid", + "border-block-start-color": "var(--border-color)", + "font-family": "var(--font-family-secondary)", + "font-weight": "var(--global-font-weight-7)", + "font-size": "var(--global-font-size-7)", + "inline-size": "100%", + "align-items": "flex-start" + } + } + }, + "checkout-order-card--summary--details":{ + "styles": { + "default": { + "gap": "var(--global-spacing-2)" + } + } + }, + "checkout-order-card--summary--details--item": { + "styles": { + "default": { + "flex-direction": "row", + "justify-content": "space-between", + "font-family": "var(--font-family-primary)", + "font-weight": "var(--global-font-weight-4)", + "font-size": "var(--global-font-size-3)" + } + } + }, "offer": { "styles": { "default": { diff --git a/blocks/subscriptions-block/utils/currency.test.jsx b/blocks/subscriptions-block/utils/currency.test.jsx index bd73f71416..1905a22b98 100644 --- a/blocks/subscriptions-block/utils/currency.test.jsx +++ b/blocks/subscriptions-block/utils/currency.test.jsx @@ -8,4 +8,8 @@ describe("Currency", () => { it("returns null if currency is not supported", () => { expect(currency("HKD")).toBe(null); }); + + it("returns null if currency is sent", () => { + expect(currency()).toBe(null); + }); }); \ No newline at end of file diff --git a/locale/en.json b/locale/en.json index d8234a2270..e0f85f7d17 100644 --- a/locale/en.json +++ b/locale/en.json @@ -465,6 +465,7 @@ "checkout-block.Zambia": "Zambia", "checkout-block.Zimbabwe": "Zimbabwe", "checkout-block.back-to-offer-page": "Back to subscription selection", + "checkout-block.view-subscription-offers": "View subscription offers", "checkout-block.cardNumber": "Card number", "checkout-block.cardholderName": "Name on card", "checkout-block.cardholderName-requirements": "Please enter name on card", @@ -473,6 +474,8 @@ "checkout-block.country": "Country or region", "checkout-block.country-requirements": "Please select country or region", "checkout-block.due-today": "Due today", + "checkout-block.subtotal": "Subtotal", + "checkout-block.salesTax": "Sales tax", "checkout-block.email": "Email address", "checkout-block.email-requirements": "Please enter a valid email address", "checkout-block.empty-cart-message": "Select from one of our offers",