diff --git a/blocks/subscriptions-block/_index.scss b/blocks/subscriptions-block/_index.scss
index 5827ec0bcf..88f015e8a4 100644
--- a/blocks/subscriptions-block/_index.scss
+++ b/blocks/subscriptions-block/_index.scss
@@ -204,3 +204,45 @@
@include scss.block-components("offer");
@include scss.block-properties("offer");
}
+
+.b-paywall {
+ &__overlay {
+ &-content {
+ @include scss.block-components("paywall-overlay-content");
+ @include scss.block-properties("paywall-overlay-content");
+ }
+
+ @include scss.block-components("paywall-overlay");
+ @include scss.block-properties("paywall-overlay");
+ }
+
+ &__subscription-dialog {
+ &-link-prompt {
+ @include scss.block-components("paywall-subscription-dialog-reason-prompt");
+ @include scss.block-properties("paywall-subscription-dialog-reason-prompt");
+
+ &-pre-link {
+ @include scss.block-components("paywall-subscription-dialog-link-prompt-pre-link");
+ @include scss.block-properties("paywall-subscription-dialog-link-prompt-pre-link");
+ }
+
+ &-link {
+ @include scss.block-components("paywall-subscription-dialog-link-prompt-link");
+ @include scss.block-properties("paywall-subscription-dialog-link-prompt-link");
+ }
+ }
+
+ &-offer-info{
+ @include scss.block-components("paywall-subscription-dialog-offer-info");
+ @include scss.block-properties("paywall-subscription-dialog-offer-info");
+ }
+
+ &-subheadline{
+ @include scss.block-components("paywall-subscription-dialog-subheadline");
+ @include scss.block-properties("paywall-subscription-dialog-subheadline");
+ }
+
+ @include scss.block-components("paywall-subscription-dialog");
+ @include scss.block-properties("paywall-subscription-dialog");
+ }
+}
diff --git a/blocks/subscriptions-block/components/PaywallOffer/index.jsx b/blocks/subscriptions-block/components/PaywallOffer/index.jsx
new file mode 100644
index 0000000000..582a18edb7
--- /dev/null
+++ b/blocks/subscriptions-block/components/PaywallOffer/index.jsx
@@ -0,0 +1,109 @@
+import React, { useEffect, useRef, useState } from "react";
+import { isServerSide } from "@wpmedia/arc-themes-components";
+import isUrl from "is-url";
+import useOffer from "../useOffer";
+import SubscriptionOverlay from "../SubscriptionOverlay";
+import SubscriptionDialog from "../SubscriptionDialog";
+
+const isPaywallCampaignURL = (payWallCode) => payWallCode && isUrl(payWallCode);
+
+const PaywallOffer = ({
+ isLoggedIn,
+ actionText,
+ actionUrl,
+ campaignCode = null,
+ displayMode,
+ linkPrompt,
+ linkText,
+ linkUrl,
+ reasonPrompt,
+ usePortal = true,
+ className
+}) => {
+ // the paywall code (otherwise known as a campaign code)
+ const [payWallCode, setPayWallCode] = useState();
+
+ const { offer, fetchOffer } = useOffer({ campaignCode });
+
+ /**
+ * payWallOffer is the most updated offer that is returned from
+ * the fetchOffer method in the useOffer hook.
+ * We can't use the offer object that is returned from the useOffer
+ * directly as that causes a recursive call to the second useEffect.
+ *
+ * When the payWallOffer is updated in the second hook it's current
+ * value will be applied to the selectedOffer state to update the dom.
+ */
+ const payWallOffer = useRef(offer);
+
+ const [selectedOffer, setSelectedOffer] = useState(payWallOffer.current);
+
+ /**
+ * If campaignCode is passed in as a prop,
+ * use that. Otherwise use what is returned from
+ * usePaywall hook and at the very least, set it
+ * to "default"
+ */
+ useEffect(() => {
+ const campaign = campaignCode || "default";
+ setPayWallCode(campaign);
+ }, [campaignCode]);
+
+ // This will grab the offer corresponding to the paywall code
+ useEffect(() => {
+ const fetchNewOffer = async () => {
+ payWallOffer.current = await fetchOffer(payWallCode);
+ if (payWallOffer.current) {
+ setSelectedOffer(payWallOffer.current);
+ }
+ };
+ if (
+ payWallCode &&
+ !isUrl(payWallCode) &&
+ (!payWallOffer.current || payWallOffer.current.pw !== payWallCode)
+ ) {
+ fetchNewOffer();
+ }
+ return () => {
+ payWallOffer.current = null;
+ };
+ }, [payWallCode, fetchOffer]);
+
+ /**
+ * Return null if server side, not paywalled, doesn't have an offer to show
+ * or if the user is logged in.
+ */
+ if (isServerSide() || !selectedOffer) {
+ return null;
+ }
+
+ /**
+ * Need to determine the campaign code.
+ * First we see if we have been provided with a campaignCode prop.
+ * If not then we check if the payWallCode is not a url, if its
+ * not, then we use that. If it a url (Why would it be a URL??) then
+ * we just set the campaign code to "default"
+ */
+ const campaign = campaignCode || (!isPaywallCampaignURL(payWallCode) ? payWallCode : "default");
+ const actionUrlFinal =
+ !campaign || campaign === "default" ? actionUrl : `${actionUrl}?campaign=${campaign}`;
+
+ return (
+
+
+
+ );
+};
+
+export default PaywallOffer;
\ No newline at end of file
diff --git a/blocks/subscriptions-block/components/PaywallOffer/index.test.js b/blocks/subscriptions-block/components/PaywallOffer/index.test.js
new file mode 100644
index 0000000000..285cdc3802
--- /dev/null
+++ b/blocks/subscriptions-block/components/PaywallOffer/index.test.js
@@ -0,0 +1,122 @@
+import React from "react";
+import { render } from "@testing-library/react";
+import isUrl from "is-url";
+
+import { isServerSide } from "@wpmedia/arc-themes-components";
+
+import PaywallOffer from ".";
+import usePaywall from "../usePaywall";
+import useOffer from "../useOffer";
+
+jest.mock("@wpmedia/arc-themes-components", () => ({
+ __esModule: true,
+ isServerSide: jest.fn(() => false),
+}));
+
+jest.mock("is-url", () => ({
+ __esModule: true,
+ default: jest.fn(() => false),
+}));
+
+jest.mock("../../components/useOffer");
+jest.mock("../../../identity-block");
+jest.mock("../../components/usePaywall");
+
+useOffer.mockReturnValue({
+ offer: {
+ pageTitle: "this the offer title",
+ pageSubTitle: "this the offer subtitle",
+ },
+ fetchOffer: () => ({
+ pageTitle: "this the offer title",
+ pageSubTitle: "this the offer subtitle",
+ }),
+});
+
+usePaywall.mockReturnValue({
+ campaignCode: "default",
+ isPaywalled: true,
+ isRegisterwalled: false,
+});
+
+/**
+ * Below I pass usePortal to false that will in return
+ * pass this down to the Subscription Overlay.
+ * This boolean prevents ReactDOM.createPortal from being used.
+ * Just checking with isServerSide doesn't work. Jest and Enzyme
+ * still have poor support for ReactDOM.createPortal, so we need a way
+ * to conditionally render ReactDOM.createPortal.
+ */
+describe("The PaywallOffer component ", () => {
+ it("returns null if serverSide", () => {
+ isServerSide.mockReturnValue(true);
+ render(
+
+ );
+ expect(screen.html()).toBe(null);
+ isServerSide.mockReset();
+ });
+
+ it("renders with correct markup", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Subscribe to continue reading.")).not.toBeNull();
+ expect(screen.getByText("Already a subscriber?")).not.toBeNull();
+ expect(screen.getByText("Log In.").closest("a")).toHaveAttribute("href", "/account/login");
+
+ expect(screen.getByText("this the offer title")).not.toBeNull();
+ expect(screen.getByText("this the offer subtitle")).not.toBeNull();
+ expect(screen.getByText("Subscribe")).not.toBeNull();
+ expect(screen.getByText("Subscribe").closest("a")).toHaveAttribute(
+ "href",
+ "/offer/?campaign=defaultish"
+ );
+ });
+
+ it("renders campaignCode if its a url", () => {
+ isUrl.mockReturnValue(true);
+ render(
+
+ );
+ expect(screen.getByText("Subscribe")).not.toBeNull();
+ expect(screen.getByText("Subscribe").closest("a")).toHaveAttribute(
+ "href",
+ "/offer/?campaign=./"
+ );
+ isUrl.mockReset();
+ });
+
+ it("renders without a query param if campaignCode is not passed", () => {
+ isUrl.mockReturnValue(true);
+ render( );
+ expect(screen.getByRole("button")).not.toBeNull();
+ expect(screen.getByText("Subscribe")).not.toBeNull();
+ expect(screen.getByRole("button").toHaveAttribute("href", "/offer/"));
+ isUrl.mockReset();
+ });
+});
diff --git a/blocks/subscriptions-block/components/RegwallOffer/index.jsx b/blocks/subscriptions-block/components/RegwallOffer/index.jsx
new file mode 100644
index 0000000000..a67ffe546a
--- /dev/null
+++ b/blocks/subscriptions-block/components/RegwallOffer/index.jsx
@@ -0,0 +1,41 @@
+import React from "react";
+
+import { isServerSide } from "@wpmedia/arc-themes-components";
+import SubscriptionOverlay from "../SubscriptionOverlay";
+import SubscriptionDialog from "../SubscriptionDialog";
+
+const RegwallOffer = ({
+ actionText,
+ actionUrl,
+ displayMode,
+ headlineText,
+ linkPrompt,
+ linkText,
+ linkUrl,
+ reasonPrompt,
+ subheadlineText,
+ usePortal = true,
+ className,
+}) => {
+ if (isServerSide()) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default RegwallOffer;
\ No newline at end of file
diff --git a/blocks/subscriptions-block/components/RegwallOffer/index.test.js b/blocks/subscriptions-block/components/RegwallOffer/index.test.js
new file mode 100644
index 0000000000..5e9f2c920d
--- /dev/null
+++ b/blocks/subscriptions-block/components/RegwallOffer/index.test.js
@@ -0,0 +1,69 @@
+import React from "react";
+import { render } from "@testing-library/react";
+
+import { isServerSide } from "@wpmedia/arc-themes-components";
+
+import RegwallOffer from ".";
+
+jest.mock("@wpmedia/arc-themes-components", () => ({
+ __esModule: true,
+ isServerSide: jest.fn(() => false),
+}));
+
+jest.mock("is-url", () => ({
+ __esModule: true,
+ default: jest.fn(() => false),
+}));
+
+/**
+ * Below I pass usePortal to false that will in return
+ * pass this down to the Subscription Overlay.
+ * This boolean prevents ReactDOM.createPortal from being used.
+ * Just checking with isServerSide doesn't work. Jest and Enzyme
+ * still have poor support for ReactDOM.createPortal, so we need a way
+ * to conditionally render ReactDOM.createPortal.
+ */
+describe("The RegwallOffer component ", () => {
+ it("returns null if serverSide", () => {
+ isServerSide.mockReturnValue(true);
+ render(
+
+ );
+ expect(screen.html()).toBe(null);
+ isServerSide.mockReset();
+ });
+
+ it("renders with correct markup", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Subscribe to continue reading.")).not.toBeNull();
+ expect(screen.getByText("Already a subscriber?")).not.toBeNull();
+ expect(screen.getByText("Log In.").closest("a")).toHaveAttribute("href", "/account/login");
+
+ expect(screen.getByText("Headline")).not.toBeNull();
+ expect(screen.getByText("Subheadline")).not.toBeNull();
+ expect(screen.getByText("Subscribe").closest("a")).toHaveAttribute("href", "/account/signup");
+ });
+});
diff --git a/blocks/subscriptions-block/components/SubscriptionDialog/index.jsx b/blocks/subscriptions-block/components/SubscriptionDialog/index.jsx
new file mode 100644
index 0000000000..2cde99708c
--- /dev/null
+++ b/blocks/subscriptions-block/components/SubscriptionDialog/index.jsx
@@ -0,0 +1,72 @@
+import React from "react";
+
+import PropTypes from "@arc-fusion/prop-types";
+import { Heading, Link, Button, Stack } from "@wpmedia/arc-themes-components";
+
+const SubscriptionDialog = ({
+ isLoggedIn,
+ actionText,
+ actionUrl,
+ reasonPrompt,
+ headline,
+ linkText,
+ linkPrompt,
+ linkUrl,
+ subHeadline,
+ className,
+}) => {
+
+ return (
+
+
+ {reasonPrompt ? (
+
+ ) : null}
+ {!isLoggedIn ? (
+
+ {linkPrompt ? (
+
+ ) : null}
+
+ {linkText}
+
+
+ ) : null}
+
+
+ {headline ? : null}
+ {subHeadline ? (
+
+ ) : null}
+
+ {actionUrl && actionText ? (
+
+ {actionText}
+
+ ) : null}
+
+ );
+};
+
+SubscriptionDialog.propTypes = {
+ isLoggedIn: PropTypes.boolean,
+ actionText: PropTypes.string,
+ actionUrl: PropTypes.string,
+ reasonPrompt: PropTypes.oneOfType([PropTypes.string, PropTypes.element, PropTypes.node]),
+ headline: PropTypes.oneOfType([PropTypes.string, PropTypes.element, PropTypes.node]),
+ linkText: PropTypes.string.isRequired,
+ linkPrompt: PropTypes.string,
+ linkUrl: PropTypes.string.isRequired,
+ subHeadline: PropTypes.oneOfType([PropTypes.string, PropTypes.element, PropTypes.node]),
+};
+
+export default SubscriptionDialog;
diff --git a/blocks/subscriptions-block/components/SubscriptionDialog/index.story.jsx b/blocks/subscriptions-block/components/SubscriptionDialog/index.story.jsx
new file mode 100644
index 0000000000..1f14769d2b
--- /dev/null
+++ b/blocks/subscriptions-block/components/SubscriptionDialog/index.story.jsx
@@ -0,0 +1,51 @@
+import React from "react";
+
+import SubscriptionDialog from ".";
+
+export default {
+ title: "Blocks/Subscriptions/Components/Subscription Dialog",
+ parameters: {
+ chromatic: { viewports: [320, 1023, 1200] },
+ },
+};
+
+export const allFields = () => (
+
+);
+
+export const minimalFields = () => ;
+
+export const withButton = () => (
+
+);
+
+export const withReason = () => (
+
+);
+
+export const withLinkPrompt = () => (
+
+);
+
+export const withHeadline = () => (
+
+);
+
+export const withSubheadline = () => (
+
+);
\ No newline at end of file
diff --git a/blocks/subscriptions-block/components/SubscriptionDialog/index.test.js b/blocks/subscriptions-block/components/SubscriptionDialog/index.test.js
new file mode 100644
index 0000000000..4f9d2cb4d6
--- /dev/null
+++ b/blocks/subscriptions-block/components/SubscriptionDialog/index.test.js
@@ -0,0 +1,66 @@
+import React from "react";
+import { render } from "@testing-library/react";
+
+import SubscriptionDialog from ".";
+
+describe("SubscriptionDialog", () => {
+ it("renders with minimal required properties", () => {
+ render( );
+
+ expect(screen.getByText('Log In.').closest('a')).toHaveAttribute('href', '/');
+ });
+
+ it("renders the button as a link", () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Press Me!').closest('a')).toHaveAttribute('href', '/');
+ });
+
+ it("does not render if required action part is missing", () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Log in').closest('a')).toHaveAttribute('href', '/');
+ });
+
+ it("does not render if other required action part is missing", () => {
+ render( );
+
+ expect(screen.getByText('Log in').closest('a')).toHaveAttribute('href', '/');
+ });
+
+ it("renders the headline", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Headline")).not.toBeNull();
+ });
+
+ it("renders the subheadlines", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Headline 2")).not.toBeNull();
+ });
+
+ it("renders the reason", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("You need to do this.")).not.toBeNull();
+ });
+
+ it("renders the link prompt text", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("You should log in.")).not.toBeNull();
+ });
+});
\ No newline at end of file
diff --git a/blocks/subscriptions-block/components/SubscriptionOverlay/index.jsx b/blocks/subscriptions-block/components/SubscriptionOverlay/index.jsx
new file mode 100644
index 0000000000..f6a76de787
--- /dev/null
+++ b/blocks/subscriptions-block/components/SubscriptionOverlay/index.jsx
@@ -0,0 +1,103 @@
+import React, { useEffect, useState, useRef } from "react";
+import * as ReactDOM from "react-dom";
+
+import { isServerSide } from "@wpmedia/arc-themes-components";
+
+export const Portal = ({ children }) => {
+ if (isServerSide()) return null;
+
+ return ReactDOM.createPortal(children, document.body);
+};
+
+/**
+ * The usePortal param should always be true, with the exception
+ * of unit testing where portal needs to be false to prevent
+ * jest and enzyme errors because
+ * Portals are not currently supported by the server renderer.
+ *
+ * Just checking for isServerSide is not enough.
+ *
+ * `displayMode` param is unused at this time.
+ * We implemented 'bottomHalf' below.
+ * It can be ['bottomHalf', 'full', 'modal'].
+ *
+ */
+const SubscriptionOverlay = ({ children, usePortal = true, className }) => {
+ const overlayRef = useRef();
+ const contentRef = useRef();
+ const [scrollDelta, setScrollDelta] = useState(0);
+ const [overlayTouchLast, setOverlayTouchLast] = useState(0);
+ const [contentTouchLast, setContentTouchLast] = useState(0);
+
+ useEffect(() => {
+ const disableScroll = (event) => event.preventDefault();
+ const scrollElement = overlayRef.current.ownerDocument.scrollingElement;
+ const { overflow } = scrollElement.style;
+
+ scrollElement.addEventListener("scroll", disableScroll);
+ scrollElement.style.overflow = "hidden";
+ scrollElement.style.maxHeight = "100vh";
+
+ return () => {
+ scrollElement.removeEventListener("scroll", disableScroll);
+ scrollElement.style.overflow = overflow;
+ scrollElement.style.maxHeight = "auto";
+ };
+ }, [overlayRef]);
+
+ useEffect(() => {
+ const contentTopFactor = overlayRef.current.scrollTop / overlayRef.current.clientHeight;
+
+ if (contentRef.current.scrollTop >= 0 && contentTopFactor < 0.25) {
+ overlayRef.current.scrollTop += scrollDelta;
+ } else {
+ contentRef.current.scrollTop += scrollDelta;
+ }
+ }, [contentRef, overlayRef, scrollDelta]);
+
+ const renderInternalOverlay = () => (
+ {
+ setScrollDelta(event.deltaY);
+ }}
+ onTouchMove={(event) => {
+ setScrollDelta(overlayTouchLast - event.changedTouches[0].clientY);
+ setOverlayTouchLast(event.changedTouches[0].clientY);
+ }}
+ onTouchStart={(event) => {
+ setOverlayTouchLast(event.changedTouches[0].clientY);
+ }}
+ role="alert"
+ >
+ {
+ setScrollDelta(event.deltaY);
+ }}
+ onTouchMove={(event) => {
+ setScrollDelta(contentTouchLast - event.changedTouches[0].clientY);
+ setContentTouchLast(event.changedTouches[0].clientY);
+ }}
+ onTouchStart={(event) => {
+ setContentTouchLast(event.changedTouches[0].clientY);
+ }}
+ ref={contentRef}
+ >
+ {children}
+
+
+ );
+
+ const renderOverlay = () => {
+ if (!usePortal || isServerSide()) {
+ return <>{renderInternalOverlay()}>;
+ }
+ return {renderInternalOverlay()} ;
+ };
+
+ return renderOverlay();
+};
+
+export default SubscriptionOverlay;
diff --git a/blocks/subscriptions-block/components/SubscriptionOverlay/index.story.jsx b/blocks/subscriptions-block/components/SubscriptionOverlay/index.story.jsx
new file mode 100644
index 0000000000..711ccb3e9b
--- /dev/null
+++ b/blocks/subscriptions-block/components/SubscriptionOverlay/index.story.jsx
@@ -0,0 +1,140 @@
+import React from "react";
+
+import SubscriptionOverlay from ".";
+
+export default {
+ title: "Blocks/Subscriptions/Components/Subscription Overlay",
+ parameters: {
+ chromatic: { viewports: [320, 1200] },
+ },
+};
+
+const LongArticle = () => {
+ const textStyle = {
+ margin: "0.5em auto",
+ padding: "0.5em",
+ maxWidth: "60vw",
+ };
+ return (
+
+ Focusable element
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce elementum tincidunt placerat.
+ Integer bibendum, libero sed rhoncus dapibus, ipsum nulla condimentum est, in gravida sapien
+ risus nec libero. Nunc consectetur purus urna, id vehicula ante hendrerit at. Vivamus ac leo
+ id lorem commodo scelerisque. Vestibulum rhoncus sed lacus eget tempus. Quisque odio elit,
+ euismod a ipsum eu, posuere aliquet justo. Ut finibus est sapien, vitae mattis neque
+ tincidunt et. Etiam tincidunt congue sem. Curabitur tempus, nisl eget ornare gravida, magna
+ tortor ultricies elit, eu commodo dolor dui vel quam. Suspendisse potenti. Vestibulum
+ dignissim elit tortor, vel scelerisque mauris consectetur eget.
+
+
+ Vestibulum blandit vel neque sollicitudin venenatis. Maecenas erat velit, eleifend at nisi
+ sed, vulputate fermentum nibh. Vestibulum ut elementum ex. Vivamus convallis dolor eget
+ dictum facilisis. Duis maximus tortor vitae nisi viverra luctus. Vivamus ultrices laoreet
+ lectus, non dictum massa efficitur nec. Integer vitae nunc elementum, imperdiet metus ut,
+ mattis lacus. Cras finibus lorem sed risus accumsan, et tempus nulla cursus.
+
+
+ In in massa vitae mauris sagittis cursus. Donec fringilla elit a lacus placerat varius.
+ Nulla aliquet pulvinar eros eu egestas. Ut sed odio sed risus scelerisque efficitur. Nullam
+ at pretium est. Integer tristique tristique magna ut tempus. Fusce sagittis tincidunt
+ tristique. Praesent ac interdum diam. Etiam rutrum, dui eu iaculis volutpat, nisl nunc
+ vehicula lacus, vitae rhoncus metus leo sit amet nibh. Praesent aliquet erat id fermentum
+ ultrices. Integer ac arcu sit amet mauris aliquam aliquet. Integer et nunc vel lectus congue
+ luctus non in tellus. Mauris quis neque gravida, mollis quam id, auctor odio. Donec
+ placerat, sapien eget rhoncus varius, nulla eros aliquet neque, vel maximus nibh libero id
+ erat.
+
+
+ Integer et dolor ut nulla mattis lobortis vitae sed enim. In quis dolor nec ex gravida
+ sollicitudin. Fusce cursus eleifend fringilla. Donec magna risus, laoreet pulvinar justo ut,
+ dignissim vulputate velit. Nulla vehicula, tellus eu condimentum hendrerit, sem leo
+ facilisis purus, pellentesque viverra turpis mi a odio. Maecenas blandit diam tincidunt,
+ volutpat magna sed, viverra justo. Quisque non mollis sem, eget fringilla felis. Aliquam
+ maximus urna vitae velit sollicitudin, vel fringilla enim scelerisque. Vestibulum varius
+ lacinia dui. Praesent fringilla metus id enim fringilla, nec finibus dolor condimentum.
+ Donec non lorem purus. Donec sit amet dolor libero. Aliquam egestas facilisis dapibus.
+
+
+ Maecenas non eros eget nulla efficitur pulvinar. Duis varius bibendum tellus vitae feugiat.
+ In vel ipsum non lorem tincidunt aliquet non vitae purus. Vivamus nec dolor id mauris
+ efficitur accumsan. Vestibulum ullamcorper convallis odio. Fusce rhoncus nibh sed metus
+ sodales, vel efficitur metus fringilla. Pellentesque ut erat ex.
+
+
+ Phasellus ac mattis enim. Sed non massa ut nulla aliquet pellentesque et vel est. Proin
+ consequat massa a ipsum placerat luctus. Nullam ultrices, tortor sit amet mattis vestibulum,
+ nulla justo pretium purus, eu lacinia lacus libero eu nibh. In tellus ex, molestie ut enim
+ a, pharetra convallis arcu. Duis sit amet ultricies sem, eget rhoncus erat. Nullam volutpat,
+ risus cursus tincidunt dapibus, libero ligula tempus tortor, eget fringilla diam eros et mi.
+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis
+ egestas. Donec sagittis, lorem ut porta mollis, magna quam luctus mi, id condimentum odio
+ ligula sed odio. Duis sagittis consequat metus, nec convallis nisl consequat a. Duis at
+ turpis cursus, gravida ipsum in, fermentum sem. Donec venenatis, lacus nec molestie
+ pulvinar, magna sem hendrerit ex, ac efficitur augue magna nec lorem. Vivamus et vehicula
+ mi. Vestibulum blandit lorem et massa semper dignissim. Aliquam vehicula et urna at finibus.
+ Nam non ante tempus, sodales odio in, pretium justo.
+
+
+ Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
+ Praesent interdum ultrices odio sed vestibulum. Quisque iaculis turpis sed tincidunt
+ pellentesque. Vestibulum molestie dui purus, quis maximus felis scelerisque ut. Donec sit
+ amet diam faucibus dui blandit viverra. Pellentesque ultricies lorem orci, ut tempor eros
+ finibus a. Fusce ac venenatis nisi. Etiam quis elit lacinia, tincidunt ligula sed, aliquet
+ libero. Donec eu elit ut ipsum maximus dignissim et vitae libero. Suspendisse potenti. Nulla
+ ullamcorper molestie nulla, sed accumsan nibh vulputate in. Phasellus pharetra accumsan
+ lacus vitae congue.
+
+
+ Pellentesque non pellentesque velit. Nullam id enim erat. In eu condimentum mi. Proin sit
+ amet blandit tortor. Morbi vehicula vehicula magna, in pretium arcu hendrerit a. Vivamus a
+ sem sem. Nam vestibulum lectus id augue facilisis tincidunt. Nullam finibus semper nisi.
+ Duis vehicula tellus vitae sem facilisis ultricies. Integer et nunc at tortor ultrices
+ congue at vitae risus. Aenean ut nunc id quam pulvinar vehicula eu et libero. Ut nec erat et
+ ex vulputate vestibulum. Nam mattis pretium felis, sit amet pellentesque justo ultricies et.
+ Donec consectetur eleifend fermentum.
+
+
+ Proin in ex quam. Quisque eget tellus tellus. Donec ac tortor feugiat enim condimentum
+ euismod. Mauris ac maximus leo. Nullam at tempor leo. Praesent vitae diam in diam iaculis
+ pharetra. Suspendisse a nulla quis quam dictum pharetra viverra ac nulla. Vestibulum
+ sollicitudin nunc vel odio blandit scelerisque. Nam eget sem et diam molestie tempor eget ut
+ lorem. Pellentesque pretium rutrum vulputate. Cras fermentum sapien eu mollis tristique.
+ Quisque pulvinar massa est, sodales sodales felis sodales at. Fusce aliquet, massa ac
+ posuere tincidunt, erat lacus ullamcorper nunc, eget scelerisque augue nibh eu dui. Donec
+ sodales neque bibendum arcu lacinia, sit amet sollicitudin neque tincidunt.
+
+
+ Suspendisse vel egestas orci, vitae dictum velit. Donec imperdiet ipsum mauris, eget mollis
+ lectus gravida congue. Vestibulum porttitor id dolor id sagittis. Sed diam ligula, dictum
+ ullamcorper tincidunt quis, vehicula eget ex. Aliquam hendrerit dapibus pharetra. Duis ut
+ metus convallis, placerat magna id, lobortis purus. Vestibulum ut massa id dolor placerat
+ consectetur. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac
+ turpis egestas. Nam at interdum arcu, id feugiat dolor. Etiam aliquet vestibulum dolor vel
+ fermentum.
+
+
+ );
+};
+
+export const simpleContent = () => (
+
+ This is some overlay content
+
+);
+
+export const multiPageContent = () => (
+
+
+
+);
+
+export const scrollDisabledBackground = () => (
+ <>
+
+
+
+
+ >
+);
\ No newline at end of file
diff --git a/blocks/subscriptions-block/components/SubscriptionOverlay/index.test.jsx b/blocks/subscriptions-block/components/SubscriptionOverlay/index.test.jsx
new file mode 100644
index 0000000000..aa3394f999
--- /dev/null
+++ b/blocks/subscriptions-block/components/SubscriptionOverlay/index.test.jsx
@@ -0,0 +1,117 @@
+import React, { useState } from "react";
+import { render } from "@testing-library/react";
+
+import SubscriptionOverlay from ".";
+
+Object.defineProperty(global.document, "ownerDocument", {
+ value: global.document,
+});
+Object.defineProperty(global.document, "scrollingElement", {
+ value: global.document,
+});
+Object.defineProperty(global.document, "style", {
+ value: { overflow: "auto" },
+});
+
+describe("SubscriptionOverlay", () => {
+ it("renders with minimal required properties", () => {
+ render( );
+
+ expect(screen.find(".xpmedia-subscription-overlay").at(0)).toExist();
+ });
+
+ it("renders required a11y properties", () => {
+ render( );
+
+ expect(screen.getByRole("alert").length).toEqual(1);
+ });
+
+ it("renders a child in the appropriate container", () => {
+ render(
+
+
+
+ );
+
+ expect(screen.find(".xpmedia-subscription-overlay-content > .findThis").at(0)).toExist();
+ });
+
+ it("handles scrolling", () => {
+ // Scrolling is difficult to test functionality in Enzyme jsDoc so this just test the events
+ render(
+
+ Some Text
+
+ );
+ const contentContainer = screen.find(".xpmedia-subscription-overlay-content").at(0);
+
+ const eventUp = {
+ deltaY: 1,
+ };
+ contentContainer.simulate("wheel", eventUp);
+
+ const eventDown = {
+ deltaY: -1,
+ };
+ contentContainer.simulate("wheel", eventDown);
+
+ expect(contentContainer).toExist();
+ });
+
+ it("handles touch events", () => {
+ // Scrolling is difficult to test functionality in Enzyme jsDoc so this just test the events
+ render(
+
+ Some Text
+
+ );
+ const contentContainer = screen.find(".xpmedia-subscription-overlay-content").at(0);
+
+ const eventStart = {
+ changedTouches: [{ clientY: 9 }],
+ };
+ contentContainer.simulate("touchstart", eventStart);
+
+ const eventUp = {
+ changedTouches: [{ clientY: 10 }],
+ };
+ contentContainer.simulate("touchmove", eventUp);
+
+ const eventDown = {
+ changedTouches: [{ clientY: 9 }],
+ };
+ contentContainer.simulate("touchmove", eventDown);
+
+ expect(contentContainer).toExist();
+ });
+
+ it("cleans itself up", () => {
+ const ShowHide = () => {
+ const [visible, setVisible] = useState(false);
+ const Button = () => (
+
+ setVisible(!visible)} type="button">
+ {visible ? "Hide" : "Show"}
+
+
+ );
+ return (
+ <>
+
+ {visible ? (
+
+
+
+ ) : null}
+ >
+ );
+ };
+ render( );
+
+
+ expect(system.find(".xpmedia-subscription-overlay").at(0)).toExist();
+
+ system.find(".xpmedia-subscription-overlay-content button").at(0).simulate("click");
+ expect(system.find(".xpmedia-subscription-overlay").at(0)).not.toExist();
+ });
+});
\ No newline at end of file
diff --git a/blocks/subscriptions-block/components/usePaywall.js b/blocks/subscriptions-block/components/usePaywall.js
new file mode 100644
index 0000000000..3a5686a4ea
--- /dev/null
+++ b/blocks/subscriptions-block/components/usePaywall.js
@@ -0,0 +1,115 @@
+import { useEffect, useState } from "react";
+
+import { useFusionContext } from "fusion:context";
+import getProperties from "fusion:properties";
+
+import { useIdentity } from "@wpmedia/identity-block";
+import { isServerSide } from "@wpmedia/arc-themes-components";
+
+const usePaywall = () => {
+ const { arcSite, globalContent } = useFusionContext();
+ const contentIdentifier = globalContent?.canonical_url;
+ const contentRestriction = globalContent?.content_restrictions?.content_code;
+ const contentType = globalContent?.type;
+
+ const { api } = getProperties(arcSite);
+
+ const [campaignCode, setCampaignCode] = useState();
+ const [results, setResults] = useState();
+ const [isPaywalled, setIsPaywalled] = useState(false);
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
+ const [triggeredRule, setTriggeredRule] = useState();
+ const { Identity, isInitialized: isIdentityInitialized } = useIdentity();
+
+ // eslint-disable-next-line no-underscore-dangle
+ const rules = (!isServerSide() && window?.ArcP?._rules) || [];
+ const apiOrigin = api?.retail?.origin;
+
+ useEffect(() => {
+ if (isIdentityInitialized) {
+ Identity.isLoggedIn().then(setIsLoggedIn);
+ }
+ }, [Identity, isIdentityInitialized]);
+
+ useEffect(() => {
+ const runPaywall = async () => {
+ // Subs ArcP.run assumes https://, so we need to strip it from the endpoint origin.
+ setResults(
+ await window?.ArcP?.run({
+ apiOrigin: apiOrigin.replace(/^https?:\/\//i, ""),
+ contentIdentifier,
+ contentRestriction,
+ contentType,
+ Identity,
+ paywallFunction: (campaign) => {
+ setCampaignCode(campaign);
+ setIsPaywalled(true);
+ },
+ section: globalContent.taxonomy?.primary_section._id,
+ })
+ );
+ };
+
+ if (
+ apiOrigin &&
+ contentIdentifier &&
+ contentRestriction &&
+ isIdentityInitialized &&
+ !isPaywalled &&
+ !isServerSide()
+ ) {
+ runPaywall();
+ }
+ }, [apiOrigin, globalContent, Identity, isIdentityInitialized, isPaywalled]);
+
+ useEffect(() => {
+ if (results?.triggered && rules?.length) {
+ const { id: triggerId, rc: triggerCount } = results.triggered;
+
+ const triggeringRule = rules.find(({ id }) => triggerId === id);
+
+ // Rule is bypassed by registered or subscribed users
+ // Registered e: [true], ent:[false]
+ // Subscribed (SKU): e: [true, SKU1,...], ent: [false]
+ // Subscribed (entitlement): e: [false], ent: [true, ENT1, ...]
+ if (triggeringRule?.e?.length === 1 && triggeringRule?.e[0] === true && isLoggedIn) {
+ // we currently only support rule triggers of ['>', count]
+ const triggerableRules = ({ rt: [op, count] }) => op === ">" && triggerCount > count;
+ const byDescendingTriggerCount = ({ rt: [, a] }, { rt: [, b] }) => b - a;
+ const withRestrictedStatus = ({ e: [hasOpportunity, skuId] }) => hasOpportunity && !!skuId;
+
+ const paywallableRule = rules
+ .filter(triggerableRules)
+ .sort(byDescendingTriggerCount)
+ .find(withRestrictedStatus);
+
+ setTriggeredRule(
+ paywallableRule && paywallableRule !== triggeringRule ? paywallableRule : triggeringRule
+ );
+ } else {
+ setTriggeredRule(triggeringRule);
+ }
+ }
+ }, [results, rules, isLoggedIn]);
+
+ if (isServerSide()) {
+ return {
+ campaignCode: undefined,
+ isPaywalled: false,
+ isRegisterwalled: false,
+ };
+ }
+
+ return {
+ isLoggedIn,
+ campaignCode,
+ isPaywalled,
+ isRegisterwalled:
+ triggeredRule?.e?.length === 1 &&
+ triggeredRule?.e[0] === true &&
+ triggeredRule?.ent?.length === 1 &&
+ triggeredRule?.ent[0] === false,
+ };
+};
+
+export default usePaywall;
diff --git a/blocks/subscriptions-block/components/usePaywall.test.js b/blocks/subscriptions-block/components/usePaywall.test.js
new file mode 100644
index 0000000000..c4a6873344
--- /dev/null
+++ b/blocks/subscriptions-block/components/usePaywall.test.js
@@ -0,0 +1,257 @@
+import React from "react";
+import { render } from "@testing-library/react";
+
+import { isServerSide } from "@wpmedia/arc-themes-components";
+import { useFusionContext } from "fusion:context";
+
+import usePaywall from "./usePaywall";
+
+jest.mock("@arc-publishing/sdk-identity", () => ({
+ __esModule: true,
+ default: {
+ apiOrigin: "",
+ options: jest.fn(),
+ },
+}));
+
+jest.mock("@wpmedia/identity-block", () => ({
+ __esModule: true,
+ useIdentity: jest.fn(() => ({
+ Identity: {
+ isLoggedIn: jest.fn(() => Promise.resolve(true)),
+ },
+ isInitialized: true,
+ })),
+}));
+
+jest.mock("@wpmedia/engine-theme-sdk", () => ({
+ __esModule: true,
+ isServerSide: jest.fn(() => false),
+}));
+
+jest.mock("fusion:properties", () =>
+ jest.fn(() => ({
+ // arcSite
+ api: {
+ retail: {
+ origin: "http://origin/",
+ },
+ },
+ }))
+);
+
+jest.mock("fusion:context", () => ({
+ __esModule: true,
+ useFusionContext: jest.fn(() => ({
+ arcSite: "TestSite",
+ globalContent: {
+ canonical_url: "http://canonical/",
+ content_restrictions: {
+ content_code: "restriction_content_code",
+ },
+ taxonomy: {
+ primary_section: {
+ _id: "primary_section_id",
+ },
+ },
+ type: "contentType",
+ },
+ })),
+}));
+
+global.window.ArcP = {
+ _rules: [
+ {
+ e: [true],
+ id: "rule1",
+ rt: [">", 1],
+ },
+ ],
+ run: jest.fn((obj) => {
+ obj.paywallFunction("campaign");
+ return {
+ triggered: {
+ e: [true],
+ id: "rule1",
+ rc: 1,
+ },
+ };
+ }),
+};
+
+const getPaywallObject = () => {
+ let paywallObject;
+ const Test = () => {
+ paywallObject = usePaywall();
+ return null;
+ };
+ render( );
+ return paywallObject;
+};
+
+describe("Identity usePaywall Hook", () => {
+ beforeEach(() => {
+ useFusionContext.mockReturnValue({
+ arcSite: "TestSite1",
+ globalContent: {
+ canonical_url: "http://canonical/",
+ content_restrictions: {
+ content_code: "restriction_content_code",
+ },
+ taxonomy: {
+ primary_section: {
+ _id: "primary_section_id",
+ },
+ },
+ type: "contentType",
+ },
+ });
+ });
+
+ afterEach(() => {
+ useFusionContext.mockReset();
+ });
+
+ it("initially renders with paywall flag", () => {
+ const paywallObject = getPaywallObject();
+ expect(paywallObject.isPaywalled).toBe(true);
+ });
+
+ it("properly initializes and sets isPaywalled true", () => {
+ const paywallObject = getPaywallObject();
+ expect(paywallObject.isPaywalled).toBe(true);
+ });
+
+ it("returns null if serverSide", () => {
+ isServerSide.mockReturnValue(true);
+ const paywallObject = getPaywallObject();
+ expect(paywallObject.isPaywalled).toBe(false);
+ isServerSide.mockReset();
+ });
+
+ it("returns null if there are no results", () => {
+ window.ArcP.run.mockImplementation(() => null);
+ const paywallObject = getPaywallObject();
+ expect(paywallObject.isPaywalled).toBe(false);
+ window.ArcP.run.mockReset();
+ });
+});
+
+describe("Identity usePaywall Hook rule handling", () => {
+ beforeEach(() => {
+ useFusionContext.mockReturnValue({
+ arcSite: "TestSite3",
+ globalContent: {
+ canonical_url: "http://canonical/",
+ content_restrictions: {
+ content_code: "restriction_content_code",
+ },
+ taxonomy: {
+ primary_section: {
+ _id: "primary_section_id",
+ },
+ },
+ type: "contentType",
+ },
+ });
+ });
+
+ afterEach(() => {
+ useFusionContext.mockReset();
+ });
+
+ it("handles simple paywalled rule", () => {
+ const expectedResult = {
+ triggered: {
+ e: [true, "content paywall status"],
+ id: "rule1",
+ rc: 2,
+ },
+ };
+
+ global.window.ArcP = {
+ _rules: [
+ {
+ e: [true],
+ id: "rule1",
+ rt: [">", 1],
+ },
+ ],
+ run: jest.fn((obj) => {
+ obj.paywallFunction("campaign");
+ return expectedResult;
+ }),
+ };
+
+ const paywallObject = getPaywallObject();
+ expect(paywallObject.isPaywalled).toBe(true);
+ });
+
+ it("handles multiple rules matching when triggered", () => {
+ const expectedResult = {
+ triggered: {
+ e: [true],
+ id: "rule2",
+ rc: 3,
+ },
+ };
+
+ global.window.ArcP = {
+ _rules: [
+ {
+ e: [true],
+ id: "rule1",
+ rt: [">", 1],
+ },
+ {
+ e: [true, "content paywall status"],
+ id: "rule2",
+ rt: [">", 1],
+ },
+ ],
+ run: jest.fn((obj) => {
+ obj.paywallFunction("campaign");
+ return expectedResult;
+ }),
+ };
+
+ const paywallObject = getPaywallObject();
+ expect(paywallObject.isPaywalled).toBe(true);
+ });
+
+ it("handles multiple rules matching when triggered is different", () => {
+ const expectedResult = {
+ triggered: {
+ e: [true],
+ id: "rule1",
+ rc: 3,
+ },
+ };
+
+ global.window.ArcP = {
+ _rules: [
+ {
+ e: [true],
+ id: "rule1",
+ rt: [">", 1],
+ },
+ {
+ e: [true, "content paywall status"],
+ id: "rule2",
+ rt: [">", 1],
+ },
+ ],
+ run: jest.fn((obj) => {
+ obj.paywallFunction("campaign");
+ return expectedResult;
+ }),
+ };
+
+ const paywallObject = getPaywallObject();
+ expect(paywallObject.isPaywalled).toBe(true);
+ });
+
+ afterEach(() => {
+ useFusionContext.mockReset();
+ });
+});
\ No newline at end of file
diff --git a/blocks/subscriptions-block/features/paywall/default.jsx b/blocks/subscriptions-block/features/paywall/default.jsx
new file mode 100644
index 0000000000..311ac8dcf5
--- /dev/null
+++ b/blocks/subscriptions-block/features/paywall/default.jsx
@@ -0,0 +1,151 @@
+import React from "react";
+import { isServerSide } from "@wpmedia/arc-themes-components";
+import PropTypes from "@arc-fusion/prop-types";
+
+import { useFusionContext } from "fusion:context";
+
+import PaywallOffer from "../../components/PaywallOffer";
+import RegwallOffer from "../../components/RegwallOffer";
+import usePaywall from "../../components/usePaywall";
+
+const BLOCK_CLASS_NAME = "b-paywall";
+
+const Paywall = ({ customFields }) => {
+ const { isAdmin } = useFusionContext();
+
+ const {
+ adminViewState,
+ displayMode,
+ linkText,
+ linkUrl,
+ payActionText,
+ payActionUrl,
+ payLinkPrompt,
+ payReasonPrompt,
+ registerActionText,
+ registerActionUrl,
+ registerHeaderText,
+ registerSubHeaderText,
+ registerReasonPrompt,
+ } = customFields;
+
+ const { isLoggedIn, isPaywalled, isRegisterwalled } = usePaywall();
+
+ if (!isServerSide()) {
+ if ((!isAdmin && isRegisterwalled) || (isAdmin && adminViewState === "showRegwall")) {
+ return (
+
+ );
+ }
+ if ((!isAdmin && isPaywalled) || (isAdmin && adminViewState === "showPaywall")) {
+ return (
+
+ );
+ }
+ }
+ return null;
+};
+
+Paywall.propTypes = {
+ customFields: PropTypes.shape({
+ adminViewState: PropTypes.oneOf(["hide", "showRegwall", "showPaywall"]).tag({
+ defaultValue: "hide",
+ description: "Determines which view is shown here in the admin screen. ",
+ label: "Preview",
+ labels: {
+ hide: "Hide",
+ showRegwall: "Show Registration Wall",
+ showPaywall: "Show Payment Wall",
+ },
+ }),
+ displayMode: PropTypes.oneOf(["bottomHalf", "full", "modal"]).tag({
+ defaultValue: "bottomHalf",
+ description: "Determines how the dialog will present itself to the user when required.",
+ hidden: true,
+ label: "Paywall Display Mode",
+ labels: {
+ bottomHalf: "Show as bottom half of screen",
+ full: "Show as a full page cover",
+ modal: "Show as a modal dialog",
+ },
+ }),
+ payLinkPrompt: PropTypes.string.tag({
+ label: "Log In text for paywall",
+ defaultValue: "Already a subscriber?",
+ }),
+ linkText: PropTypes.string.tag({
+ label: "Log In link text",
+ defaultValue: "Log In.",
+ }),
+ linkUrl: PropTypes.string.tag({
+ label: "Log In link URL",
+ defaultValue: "/account/login/",
+ }),
+ payActionText: PropTypes.string.tag({
+ group: "Payment Wall",
+ label: "CTA button text",
+ defaultValue: "Subscribe",
+ }),
+ payActionUrl: PropTypes.string.tag({
+ group: "Payment Wall",
+ label: "CTA button URL",
+ defaultValue: "/offer/",
+ }),
+ payReasonPrompt: PropTypes.string.tag({
+ group: "Payment Wall",
+ label: "Reason prompt text",
+ defaultValue: "Subscribe to continue reading.",
+ }),
+ registerActionText: PropTypes.string.tag({
+ group: "Registration Wall",
+ label: "CTA button text",
+ defaultValue: "",
+ }),
+ registerActionUrl: PropTypes.string.tag({
+ group: "Registration Wall",
+ label: "CTA button URL",
+ defaultValue: "/account/signup/",
+ }),
+ registerHeaderText: PropTypes.string.tag({
+ group: "Registration Wall",
+ label: "Header Text",
+ defaultValue: "",
+ }),
+ registerSubHeaderText: PropTypes.string.tag({
+ group: "Registration Wall",
+ label: "Subheader Text",
+ defaultValue: "",
+ }),
+ registerReasonPrompt: PropTypes.string.tag({
+ group: "Registration Wall",
+ label: "Reason prompt text",
+ defaultValue: "Register to continue reading.",
+ }),
+ }),
+};
+
+Paywall.icon = "tag-dollar";
+Paywall.label = "Subscriptions Paywall - Arc Block";
+
+export default Paywall;
diff --git a/blocks/subscriptions-block/features/paywall/default.test.jsx b/blocks/subscriptions-block/features/paywall/default.test.jsx
new file mode 100644
index 0000000000..e02fc1ea1a
--- /dev/null
+++ b/blocks/subscriptions-block/features/paywall/default.test.jsx
@@ -0,0 +1,144 @@
+import React from "react";
+import { render } from "@testing-library/react";
+
+import { useFusionContext } from "fusion:context";
+import getProperties from "fusion:properties";
+
+import Paywall from "./default";
+import usePaywall from "../../components/usePaywall";
+
+jest.mock("../../components/useOffer");
+jest.mock("../../components/usePaywall");
+jest.mock("../../components/PaywallOffer", () => () => );
+jest.mock("../../components/RegwallOffer", () => () => );
+
+jest.mock("fusion:context", () => ({
+ __esModule: true,
+ useFusionContext: jest.fn(() => ({
+ isAdmin: false,
+ })),
+}));
+
+describe("The Paywall feature", () => {
+ it("renders Paywall when isPaywalled is true and isRegisterwalled is false", () => {
+ usePaywall.mockReturnValue({
+ isPaywalled: true,
+ isRegisterwalled: false,
+ });
+ getProperties.mockReturnValue({
+ locale: "en",
+ });
+ render(
+
+ );
+
+ expect(screen.containsMatchingElement( )).toEqual(true);
+ });
+
+ it("renders Regwall when isRegisterwalled is true", () => {
+ usePaywall.mockReturnValue({
+ isPaywalled: true,
+ isRegisterwalled: true,
+ });
+ getProperties.mockReturnValue({
+ locale: "en",
+ });
+ render(
+
+ );
+
+ expect(screen.containsMatchingElement( )).toEqual(true);
+ });
+
+ it("renders Regwall component when isAdmin and adminViewState is showRegwall", () => {
+ useFusionContext.mockReturnValue({
+ isAdmin: true,
+ });
+ getProperties.mockReturnValue({
+ locale: "en",
+ });
+ render(
+
+ );
+
+ expect(screen.containsMatchingElement( )).toEqual(true);
+ });
+
+ it("renders Paywall component when isAdmin and adminViewState is showPaywall", () => {
+ useFusionContext.mockReturnValue({
+ isAdmin: true,
+ });
+ getProperties.mockReturnValue({
+ locale: "en",
+ });
+ render(
+
+ );
+
+ expect(screen.containsMatchingElement( )).toEqual(true);
+ });
+});
\ No newline at end of file
diff --git a/blocks/subscriptions-block/themes/news.json b/blocks/subscriptions-block/themes/news.json
index d0279ae3d9..d9b8f9a6ea 100644
--- a/blocks/subscriptions-block/themes/news.json
+++ b/blocks/subscriptions-block/themes/news.json
@@ -400,5 +400,99 @@
}
}
}
+ },
+ "paywall-overlay": {
+ "styles": {
+ "default": {
+ "background-color": "rgba(0, 0, 0, 0.63)",
+ "bottom": 0,
+ "left": 0,
+ "right": 0,
+ "top": 0,
+ "overflow": "auto",
+ "pointer-events": "all",
+ "position": "fixed",
+ "height": "100vh",
+ "z-index": 1
+ }
+ }
+ },
+ "paywall-overlay-content": {
+ "styles": {
+ "default": {
+ "align-items": "center",
+ "justify-content": "center",
+ "background-color": "var(--global-white)",
+ "display": "flex",
+ "flex-direction": "column",
+ "margin-top": "50vh",
+ "max-height": "75vh",
+ "min-height": "50vh",
+ "overflow": "auto",
+ "components": {
+ "stack": {
+ "width": "100%"
+ }
+ }
+ }
+ }
+ },
+ "paywall-subscription-dialog": {
+ "styles": {
+ "default": {
+ "padding": "var(--global-spacing-6)",
+ "align-items": "center",
+ "grid-template-rows": "auto",
+ "gap": "var(--global-spacing-6)",
+ "font-family": "var(--font-family-primary)",
+ "components": {
+ "stack": {
+ "align-items": "center",
+ "grid-template-rows": "auto"
+ }
+ }
+ }
+ }
+ },
+ "paywall-subscription-dialog-reason-prompt": {
+ "styles": {
+ "default": {
+ "font-family": "var(--font-family-primary)"
+ }
+ }
+ },
+ "paywall-subscription-dialog-link-prompt-pre-link": {
+ "styles": {
+ "default": {
+ "margin-right": "var(--global-spacing-2)"
+ }
+ }
+ },
+ "paywall-subscription-dialog-link-prompt-link": {
+ "styles": {
+ "default": {
+ "font-weight": "var(--heading-level-4-font-weight)"
+ }
+ }
+ },
+ "paywall-subscription-dialog-offer-info":{
+ "styles": {
+ "default": {
+ "gap": "var(--global-spacing-4)",
+ "components": {
+ "heading": {
+ "font-size": "var(--heading-level-2-font-size)",
+ "text-align": "center"
+ }
+ }
+ }
+ }
+ },
+ "paywall-subscription-dialog-subheadline":{
+ "styles": {
+ "default": {
+ "font-size": "var(--heading-level-6-font-size)"
+ }
+ }
}
}