AW-136-personal-video-to-trial-page

This commit is contained in:
Daniil Chemerkin 2024-07-05 15:40:22 +00:00
parent 2d60805bcb
commit cb911cdf80
26 changed files with 2630 additions and 175 deletions

2308
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,12 +14,16 @@
"build:prod": "tsc && vite build --mode production" "build:prod": "tsc && vite build --mode production"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@lottiefiles/dotlottie-react": "^0.6.4", "@lottiefiles/dotlottie-react": "^0.6.4",
"@mui/material": "^5.15.21",
"@reduxjs/toolkit": "^1.9.5", "@reduxjs/toolkit": "^1.9.5",
"@smakss/react-scroll-direction": "^4.0.4", "@smakss/react-scroll-direction": "^4.0.4",
"@stripe/react-stripe-js": "^2.3.1", "@stripe/react-stripe-js": "^2.3.1",
"@stripe/stripe-js": "^2.1.9", "@stripe/stripe-js": "^2.1.9",
"apng-js": "^1.1.1", "apng-js": "^1.1.1",
"core-js": "^3.37.1",
"framer-motion": "^11.0.8", "framer-motion": "^11.0.8",
"html-react-parser": "^3.0.16", "html-react-parser": "^3.0.16",
"i18next": "^22.5.0", "i18next": "^22.5.0",
@ -31,6 +35,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-ga4": "^2.1.0", "react-ga4": "^2.1.0",
"react-i18next": "^12.3.1", "react-i18next": "^12.3.1",
"react-pdf": "8.0.2",
"react-player": "^2.16.0", "react-player": "^2.16.0",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-router-dom": "^6.11.2", "react-router-dom": "^6.11.2",
@ -40,6 +45,7 @@
"unique-names-generator": "^4.7.1" "unique-names-generator": "^4.7.1"
}, },
"devDependencies": { "devDependencies": {
"@types/core-js": "^2.5.8",
"@types/node": "^20.5.1", "@types/node": "^20.5.1",
"@types/react": "^18.2.6", "@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.4",

View File

@ -30,6 +30,7 @@ import {
Paywall, Paywall,
Payment, Payment,
UserVideos, UserVideos,
UserPDF,
} from './resources' } from './resources'
const api = { const api = {
@ -83,6 +84,8 @@ const api = {
makePayment: createMethod<Payment.PayloadPost, Payment.ResponsePost>(Payment.createRequestPost), makePayment: createMethod<Payment.PayloadPost, Payment.ResponsePost>(Payment.createRequestPost),
// User videos // User videos
getUserVideos: createMethod<UserVideos.PayloadGet, UserVideos.ResponseGet>(UserVideos.createRequest), getUserVideos: createMethod<UserVideos.PayloadGet, UserVideos.ResponseGet>(UserVideos.createRequest),
// User PDF
getUserPDFCompatibility: createMethod<UserPDF.PayloadGet, UserPDF.ResponseGet>(UserPDF.createRequest),
} }
export type ApiContextValue = typeof api export type ApiContextValue = typeof api

View File

@ -0,0 +1,21 @@
import routes from "@/routes";
import { getAuthHeaders } from "../utils";
interface Payload {
token: string;
}
export type PayloadGet = Payload;
export interface IUserPDFCompatibility {
url: string
}
type ResponseGetSuccess = IUserPDFCompatibility;
export type ResponseGet = ResponseGetSuccess;
export const createRequest = ({ token }: PayloadGet): Request => {
const url = new URL(routes.server.getUserPDFCompatibility());
return new Request(url, { method: "GET", headers: getAuthHeaders(token) });
};

View File

@ -28,3 +28,4 @@ export * as Palmistry from "./Palmistry";
export * as Paywall from "./Paywall"; export * as Paywall from "./Paywall";
export * as Payment from "./Payment"; export * as Payment from "./Payment";
export * as UserVideos from "./UserVideos"; export * as UserVideos from "./UserVideos";
export * as UserPDF from "./UserPDF";

View File

@ -2,13 +2,15 @@ import styles from "./styles.module.css";
interface IFullScreenModalProps { interface IFullScreenModalProps {
className?: string; className?: string;
classNameContent?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
children: JSX.Element; children: JSX.Element;
isOpen: boolean; isOpen: boolean;
} }
function FullScreenModal({ function FullScreenModal({
className, className = "",
classNameContent = "",
children, children,
style, style,
isOpen, isOpen,
@ -16,11 +18,11 @@ function FullScreenModal({
return ( return (
<div <div
style={{ ...style }} style={{ ...style }}
className={`${styles["modal"]} ${className || ""} ${ className={`${styles["modal"]} ${className} ${isOpen ? styles.open : ""}`}
isOpen ? styles.open : ""
}`}
> >
<div className={styles["modal__content"]}>{children}</div> <div className={`${styles["modal__content"]} ${classNameContent}`}>
{children}
</div>
</div> </div>
); );
} }

View File

@ -15,12 +15,14 @@
will-change: opacity; will-change: opacity;
animation: disappearance 3s ease; animation: disappearance 3s ease;
animation-fill-mode: forwards; animation-fill-mode: forwards;
pointer-events: none;
} }
.open { .open {
opacity: 1; opacity: 1;
animation: appearance 3s ease; animation: appearance 3s ease;
animation-fill-mode: forwards; animation-fill-mode: forwards;
pointer-events: auto;
} }
.modal__content { .modal__content {

View File

@ -2,7 +2,7 @@ import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import routes from "@/routes"; import routes from "@/routes";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useApi, useApiCall } from "@/api"; import { UserPDF, useApi, useApiCall } from "@/api";
import { Asset } from "@/api/resources/Assets"; import { Asset } from "@/api/resources/Assets";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import BlurringSubstrate from "../BlurringSubstrate"; import BlurringSubstrate from "../BlurringSubstrate";
@ -27,6 +27,10 @@ import TextWithFinger from "../TextWithFinger";
// IPredictionMoon, // IPredictionMoon,
// } from "../PredictionMoonsSlider"; // } from "../PredictionMoonsSlider";
import { predictionMoonsPeriods } from "@/data"; import { predictionMoonsPeriods } from "@/data";
import FullScreenModal from "../FullScreenModal";
import { useDynamicSize } from "@/hooks/useDynamicSize";
import PDFViewer from "../PDFViewer";
import BackButton from "../pages/ABDesign/v1/ui/BackButton";
// import WallpapersZodiacSign from "../WallpapersZodiacSign"; // import WallpapersZodiacSign from "../WallpapersZodiacSign";
// import ThermalSlider from "../ThermalSlider"; // import ThermalSlider from "../ThermalSlider";
// import MoonPhaseTracker from "../MoonPhaseTracker"; // import MoonPhaseTracker from "../MoonPhaseTracker";
@ -38,7 +42,6 @@ const buttonTextFormatter = (text: string): JSX.Element => {
return ( return (
<> <>
<strong>{sentences[0]}</strong> <strong>{sentences[0]}</strong>
<br />
<span style={{ fontSize: "12px" }}>{sentences[1]}</span> <span style={{ fontSize: "12px" }}>{sentences[1]}</span>
</> </>
); );
@ -56,7 +59,7 @@ function HomePage(): JSX.Element {
const zodiacSign = getZodiacSignByDate(birthdate); const zodiacSign = getZodiacSignByDate(birthdate);
const [asset, setAsset] = useState<Asset>(); const [asset, setAsset] = useState<Asset>();
const [moonsAssets, setMoonsAssets] = useState<Asset[]>([]); const [moonsAssets, setMoonsAssets] = useState<Asset[]>([]);
moonsAssets moonsAssets;
const api = useApi(); const api = useApi();
const homeConfig = useSelector(selectors.selectHome); const homeConfig = useSelector(selectors.selectHome);
@ -64,15 +67,17 @@ function HomePage(): JSX.Element {
const onboardingConfigHome = useSelector(selectors.selectOnboardingHome); const onboardingConfigHome = useSelector(selectors.selectOnboardingHome);
const compatibilities = useSelector(selectors.selectCompatibilities); const compatibilities = useSelector(selectors.selectCompatibilities);
compatibilities compatibilities;
const user = useSelector(selectors.selectUser); const user = useSelector(selectors.selectUser);
user user;
const [isShowOnboardingHome, setIsShowOnboardingHome] = useState( const [isShowOnboardingHome, setIsShowOnboardingHome] = useState(
!onboardingConfigHome?.isShown !onboardingConfigHome?.isShown
); );
const [isShowPDF, setIsShowPDF] = useState(false);
useEffect(() => { useEffect(() => {
dispatch( dispatch(
actions.onboardingConfig.update({ actions.onboardingConfig.update({
@ -83,6 +88,10 @@ function HomePage(): JSX.Element {
); );
}, [dispatch]); }, [dispatch]);
const handleCompatibilityPDF = () => {
setIsShowPDF(true);
};
const handleCompatibility = () => { const handleCompatibility = () => {
dispatch( dispatch(
actions.siteConfig.update({ actions.siteConfig.update({
@ -123,6 +132,17 @@ function HomePage(): JSX.Element {
// isPending // isPending
} = useApiCall<Asset[]>(assetsData); } = useApiCall<Asset[]>(assetsData);
const getUserPDFCompatibility = useCallback(async () => {
const pdf = await api.getUserPDFCompatibility({
token,
});
return pdf;
}, [api, token]);
const { data: PDFCompatibility } = useApiCall<UserPDF.ResponseGet>(
getUserPDFCompatibility
);
useEffect(() => { useEffect(() => {
if (assets) { if (assets) {
setAsset(assets[getRandomArbitrary(0, assets?.length || 0)]); setAsset(assets[getRandomArbitrary(0, assets?.length || 0)]);
@ -171,6 +191,14 @@ function HomePage(): JSX.Element {
saveFile(asset.url.replace("http://", "https://"), buildFilename("1")); saveFile(asset.url.replace("http://", "https://"), buildFilename("1"));
}; };
const downloadPDF = () => {
if (!PDFCompatibility?.url) return;
saveFile(
PDFCompatibility?.url.replace("http://", "https://"),
buildFilename("pdf-compatibility", "pdf")
);
};
// const handleBestiesHoroscope = (item: Horoscope) => { // const handleBestiesHoroscope = (item: Horoscope) => {
// const { name, birthDate } = item; // const { name, birthDate } = item;
// navigate( // navigate(
@ -202,13 +230,37 @@ function HomePage(): JSX.Element {
// navigate(`${routes.client.nameHoroscopeResult()}?period=${item.period}`); // navigate(`${routes.client.nameHoroscopeResult()}?period=${item.period}`);
// }; // };
const { width, elementRef: pageRef } = useDynamicSize({});
return ( return (
<section <section
className={`${styles.page} page`} className={`${styles.page} page`}
style={{ style={{
backgroundImage: `url(${asset?.url.replace("http://", "https://")})`, backgroundImage: `url(${asset?.url.replace("http://", "https://")})`,
}} }}
ref={pageRef}
> >
<FullScreenModal
isOpen={isShowPDF}
className={styles["pdf-modal"]}
classNameContent={styles["pdf-modal__content"]}
>
<>
<div className={styles["pdf-buttons"]}>
<BackButton
onClick={() => setIsShowPDF(false)}
className={styles["close-pdf-button"]}
/>
<div className={styles["pdf-save"]} onClick={downloadPDF} />
</div>
<PDFViewer
width={width}
file={PDFCompatibility?.url}
className={styles["pdf-document"]}
/>
</>
</FullScreenModal>
{/* <div {/* <div
className={styles.background} className={styles.background}
style={{ style={{
@ -256,6 +308,15 @@ function HomePage(): JSX.Element {
crossClickHandler={() => setIsShowOnboardingHome(false)} crossClickHandler={() => setIsShowOnboardingHome(false)}
/> />
</Onboarding> </Onboarding>
{!!PDFCompatibility?.url?.length && (
<BlurringSubstrate
style={{ color: "#fff" }}
className={styles["content__buttons-item"]}
clickHandler={handleCompatibilityPDF}
>
<strong>Your Personalized READING</strong>
</BlurringSubstrate>
)}
<BlurringSubstrate <BlurringSubstrate
style={{ color: "#fa71ea" }} style={{ color: "#fa71ea" }}
className={styles["content__buttons-item"]} className={styles["content__buttons-item"]}

View File

@ -103,7 +103,11 @@
} }
.content__buttons-item { .content__buttons-item {
display: flex;
flex-direction: column;
justify-content: space-evenly;
width: 100% !important; width: 100% !important;
min-height: 52px;
border: solid #7b7570 2px; border: solid #7b7570 2px;
border-radius: 25px !important; border-radius: 25px !important;
text-align: center; text-align: center;
@ -190,6 +194,72 @@
width: 100%; width: 100%;
} }
.pdf-modal {
overflow-y: scroll;
max-width: 560px;
left: 50%;
transform: translateX(-50%);
}
.pdf-modal__content {
height: fit-content;
min-height: 100%;
}
.pdf-document {
margin-top: -39px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100%;
}
.pdf-buttons {
position: sticky;
top: 12px;
left: 0;
z-index: 33;
width: 100%;
height: 39px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px;
}
.close-pdf-button {
position: sticky;
padding: 6px;
width: 39px;
height: 39px;
background-color: #696969;
border-radius: 50%;
border: solid 1px #fff;
}
.pdf-save {
width: 39px;
height: 39px;
background-size: 70%;
background-image: url("/Save-icon.webp");
background-repeat: no-repeat;
background-position: center center;
-webkit-backdrop-filter: blur(14px);
background-color: #696969;
backdrop-filter: blur(14px);
border-radius: 100%;
cursor: pointer;
}
.close-pdf-button > svg {
margin-left: -3px;
}
.close-pdf-button > svg > path {
fill: #fff;
}
@keyframes pulse { @keyframes pulse {
0% { 0% {
transform: scale(0.9); transform: scale(0.9);

View File

@ -0,0 +1,90 @@
import styles from "./styles.module.scss";
import { useRef, useState } from "react";
import { Document, DocumentProps, Page } from "react-pdf";
import Loader, { LoaderColor } from "../Loader";
import { File } from "node_modules/react-pdf/dist/esm/shared/types";
import { Pagination } from "@mui/material";
interface IPDFViewerProps {
width?: number;
file?: File;
}
const pagesOfPaginationPageLength = 1;
function PDFViewer(props: IPDFViewerProps & DocumentProps = {}) {
const { width = 496, file, className = "" } = props;
const [numPages, setNumPages] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
const [isLoadingDocument, setIsLoadingDocument] = useState<boolean>(true);
const [paginationPage, setPaginationPage] = useState(1);
const [isLoadingPage, setIsLoadingPage] = useState<boolean>(true);
const handleChange = (_event: React.ChangeEvent<unknown>, value: number) => {
setIsLoadingPage(true);
setPaginationPage(value);
};
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
setNumPages(numPages);
setIsLoadingDocument(false);
}
return (
<>
<div
ref={containerRef}
style={{
minHeight: "calc(100dvh - 32px)",
}}
>
{isLoadingDocument && (
<Loader
color={LoaderColor.White}
className={styles["document-loader"]}
/>
)}
<Document
loading={<></>}
file={file}
onLoadSuccess={onDocumentLoadSuccess}
className={className}
>
{Array.from({ length: pagesOfPaginationPageLength }, (_, i) => {
return (
<Page
loading={<></>}
key={i}
pageNumber={
i + pagesOfPaginationPageLength * (paginationPage - 1) + 1
}
width={width}
className={styles["pdf-page"]}
onRenderSuccess={() => {
setIsLoadingPage(false);
}}
>
{isLoadingPage && (
<Loader
className={styles["pdf-page__loader"]}
color={LoaderColor.Black}
/>
)}
</Page>
);
})}
</Document>
</div>
{!isLoadingDocument && (
<Pagination
classes={{ ul: styles["pagination-list"] }}
className={styles.pagination}
count={Math.ceil(numPages / pagesOfPaginationPageLength)}
onChange={handleChange}
/>
)}
</>
);
}
export default PDFViewer;

View File

@ -0,0 +1,34 @@
.pdf-page {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: auto;
}
.document-loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 56;
}
.pdf-page__loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 56;
}
.pagination {
position: sticky;
bottom: 0dvh;
background-color: #fff;
}
.pagination-list {
justify-content: center;
}

View File

@ -149,7 +149,10 @@ function PaymentModal({
<> <>
<p className={styles["sub-plan-description"]}> <p className={styles["sub-plan-description"]}>
You will be charged only{" "} You will be charged only{" "}
<b>${getPrice(_activeProduct)} for your 3-day trial.</b> <b>
${getPrice(_activeProduct)} for your{" "}
{_activeProduct.trialDuration}-day trial.
</b>
</p> </p>
<p className={styles["sub-plan-description"]}> <p className={styles["sub-plan-description"]}>
We`ll <b>email you a reminder</b> before your trial period We`ll <b>email you a reminder</b> before your trial period

View File

@ -12,7 +12,7 @@ export const saveFile = (url: string, filename: string): void => {
}) })
} }
export const buildFilename = (prefix: string): string => { export const buildFilename = (prefix: string, fileType = 'jpg'): string => {
const date = new Date().toISOString().slice(0, 10) const date = new Date().toISOString().slice(0, 10)
return `${prefix}-${date}.jpg` return `${prefix}-${date}.${fileType}`
} }

View File

@ -3,9 +3,33 @@ import styles from "./styles.module.css";
import MainButton from "@/components/MainButton"; import MainButton from "@/components/MainButton";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import routes from "@/routes"; import routes from "@/routes";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
import { actions, selectors } from "@/store";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
function AdditionalDiscount() { function AdditionalDiscount() {
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useDispatch();
const activeProduct = useSelector(selectors.selectActiveProduct);
const { products, getText } = usePaywall({
placementKey: EPlacementKeys["aura.placement.secret.discount"],
});
useEffect(() => {
if (!products.length) return;
const _activeProduct = products.find(
(p) => p.trialPrice === activeProduct?.trialPrice
);
if (!_activeProduct) {
dispatch(actions.payment.update({ activeProduct: products[0] }));
}
if (_activeProduct) {
dispatch(actions.payment.update({ activeProduct: _activeProduct }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, products]);
const handleNext = () => { const handleNext = () => {
navigate(routes.client.trialPaymentWithDiscountV1()); navigate(routes.client.trialPaymentWithDiscountV1());
@ -14,21 +38,25 @@ function AdditionalDiscount() {
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<Title variant="h2" className={styles.title}> <Title variant="h2" className={styles.title}>
Save 65% off! Save {(getText("discount.1") as string).replace("-", "")} off!
</Title> </Title>
<img src="/friends.webp" alt="Friends" style={{ minHeight: "180px" }} /> <img src="/friends.webp" alt="Friends" style={{ minHeight: "180px" }} />
<div className={styles["discount-point"]}> <div className={styles["discount-point"]}>
<img src="/fire.webp" alt="Fire" /> <img src="/fire.webp" alt="Fire" />
<p className={styles["discount-point-description"]}> <p className={styles["discount-point-description"]}>
65% off on your personalized plan {(getText("discount.1") as string).replace("-", "")} off on your
personalized plan
</p> </p>
</div> </div>
<div className={styles["discount-point"]}> <div className={styles["discount-point"]}>
<img src="/present.webp" alt="Present" /> <img src="/present.webp" alt="Present" />
<p className={styles["discount-point-description"]}>7-day trial</p> <p className={styles["discount-point-description"]}>
{activeProduct?.trialDuration}-day trial
</p>
</div> </div>
<p className={styles["discount-description"]}> <p className={styles["discount-description"]}>
<span>$9</span> instead of $19 <span>${(activeProduct?.price || 0) / 100}</span> instead of $
{Number(getText("full.price")) / 100}
</p> </p>
<MainButton className={styles.button} onClick={handleNext}> <MainButton className={styles.button} onClick={handleNext}>
Get secret discount! Get secret discount!

View File

@ -21,6 +21,7 @@ function TrialChoicePage() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const selectedPrice = useSelector(selectors.selectSelectedPrice); const selectedPrice = useSelector(selectors.selectSelectedPrice);
const activeProduct = useSelector(selectors.selectActiveProduct);
const homeConfig = useSelector(selectors.selectHome); const homeConfig = useSelector(selectors.selectHome);
const email = useSelector(selectors.selectEmail); const email = useSelector(selectors.selectEmail);
const [isDisabled, setIsDisabled] = useState(true); const [isDisabled, setIsDisabled] = useState(true);
@ -30,6 +31,7 @@ function TrialChoicePage() {
const { products, isLoading, getText } = usePaywall({ const { products, isLoading, getText } = usePaywall({
placementKey: EPlacementKeys["aura.placement.redesign.main"], placementKey: EPlacementKeys["aura.placement.redesign.main"],
}); });
const { videoUrl } = useSelector(selectors.selectPersonalVideo); const { videoUrl } = useSelector(selectors.selectPersonalVideo);
useEffect(() => { useEffect(() => {
@ -103,6 +105,13 @@ function TrialChoicePage() {
<p className={`${styles.text} ${styles.bold} ${styles.blue}`}> <p className={`${styles.text} ${styles.bold} ${styles.blue}`}>
{getText("text.2", { {getText("text.2", {
color: "#1C38EA", color: "#1C38EA",
replacement: {
target: "${trialDuration}",
replacement:
activeProduct?.trialDuration.toString() ||
products[0].trialDuration.toString() ||
"3",
},
})} })}
</p> </p>
<div className={styles["price-container"]}> <div className={styles["price-container"]}>

View File

@ -4,11 +4,13 @@ import CustomButton from "../CustomButton";
import GuardPayments from "../GuardPayments"; import GuardPayments from "../GuardPayments";
import { useState } from "react"; import { useState } from "react";
import FullScreenModal from "@/components/FullScreenModal"; import FullScreenModal from "@/components/FullScreenModal";
import { IPaywallProduct } from "@/api/resources/Paywall"; import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
interface IPaymentTableProps { interface IPaymentTableProps {
product: IPaywallProduct; product: IPaywallProduct;
gender: string; gender: string;
placementKey: EPlacementKeys;
buttonClick: () => void; buttonClick: () => void;
} }
@ -16,7 +18,15 @@ const getPrice = (product: IPaywallProduct) => {
return (product.trialPrice || 0) / 100; return (product.trialPrice || 0) / 100;
}; };
function PaymentTable({ gender, product, buttonClick }: IPaymentTableProps) { function PaymentTable({
gender,
product,
placementKey,
buttonClick,
}: IPaymentTableProps) {
const { getText } = usePaywall({
placementKey,
});
const [isOpenPrivacyModal, setIsOpenPrivacyModal] = useState<boolean>(false); const [isOpenPrivacyModal, setIsOpenPrivacyModal] = useState<boolean>(false);
const handleSubscriptionPolicyClick = (event: React.MouseEvent) => { const handleSubscriptionPolicyClick = (event: React.MouseEvent) => {
event.preventDefault(); event.preventDefault();
@ -63,8 +73,10 @@ function PaymentTable({ gender, product, buttonClick }: IPaymentTableProps) {
<div className={styles["table-element"]}> <div className={styles["table-element"]}>
<p>Your cost per 2 weeks after trial</p> <p>Your cost per 2 weeks after trial</p>
<div> <div>
<span className={styles.discount}>$65</span> <span className={styles.discount}>
<span>${product.trialPrice / 100}</span> ${Number(getText("full.price")) / 100}
</span>
<span>${product.price / 100}</span>
</div> </div>
</div> </div>
</div> </div>
@ -75,10 +87,11 @@ function PaymentTable({ gender, product, buttonClick }: IPaymentTableProps) {
<GuardPayments /> <GuardPayments />
<p className={styles.policy}> <p className={styles.policy}>
You are enrolling in 2 weeks subscription. By continuing you agree that You are enrolling in 2 weeks subscription. By continuing you agree that
if you don't cancel prior to the end of the 3-day trial for the $ if you don't cancel prior to the end of the {product.trialDuration}-day
{getPrice(product)} you will automatically be charged $19 every 2 weeks trial for the ${getPrice(product)} you will automatically be charged $
until you cancel in settings. Learn more about cancellation and refund {product.price / 100}{" "}
policy in{" "} every 2 weeks until you cancel in settings. Learn more about
cancellation and refund policy in{" "}
<a onClick={handleSubscriptionPolicyClick}>Subscription policy</a> <a onClick={handleSubscriptionPolicyClick}>Subscription policy</a>
</p> </p>
</> </>

View File

@ -163,6 +163,7 @@ function TrialPaymentPage() {
gender={gender} gender={gender}
product={activeProduct} product={activeProduct}
buttonClick={openStripeModal} buttonClick={openStripeModal}
placementKey={EPlacementKeys["aura.placement.redesign.main"]}
/> />
<YourReading <YourReading
gender={gender} gender={gender}
@ -180,6 +181,7 @@ function TrialPaymentPage() {
gender={gender} gender={gender}
product={activeProduct} product={activeProduct}
buttonClick={openStripeModal} buttonClick={openStripeModal}
placementKey={EPlacementKeys["aura.placement.redesign.main"]}
/> />
</section> </section>
); );

View File

@ -2,7 +2,8 @@ import Title from "@/components/Title";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { selectors } from "@/store"; import { selectors } from "@/store";
import { IPaywallProduct } from "@/api/resources/Paywall"; import { EPlacementKeys, IPaywallProduct } from "@/api/resources/Paywall";
import { usePaywall } from "@/hooks/paywall/usePaywall";
const getPrice = (product: IPaywallProduct | null) => { const getPrice = (product: IPaywallProduct | null) => {
if (!product) { if (!product) {
@ -12,6 +13,9 @@ const getPrice = (product: IPaywallProduct | null) => {
}; };
function PaymentDiscountTable() { function PaymentDiscountTable() {
const { getText } = usePaywall({
placementKey: EPlacementKeys["aura.placement.secret.discount"],
});
const activeProduct = useSelector(selectors.selectActiveProduct); const activeProduct = useSelector(selectors.selectActiveProduct);
return ( return (
@ -30,14 +34,16 @@ function PaymentDiscountTable() {
<p className={styles.description}>Secret discount applied!</p> <p className={styles.description}>Secret discount applied!</p>
</div> </div>
<div className={styles.side}> <div className={styles.side}>
<span className={styles.discount}>-30%</span> <span className={styles.discount}>{getText("discount.0")}</span>
<strong>-50%</strong> <strong>{getText("discount.1")}</strong>
</div> </div>
</div> </div>
<div className={styles["cost-container"]}> <div className={styles["cost-container"]}>
<p>Your cost per 14 days after trial:</p> <p>Your cost per 14 days after trial:</p>
<div className={styles.side}> <div className={styles.side}>
<span className={styles.discount}>$19</span> <span className={styles.discount}>
${Number(getText("full.price")) / 100}
</span>
<strong>${(activeProduct?.price || 0) / 100}</strong> <strong>${(activeProduct?.price || 0) / 100}</strong>
</div> </div>
</div> </div>

View File

@ -3,39 +3,20 @@ import styles from "./styles.module.css";
import MainButton from "@/components/MainButton"; import MainButton from "@/components/MainButton";
import PaymentDiscountTable from "./PaymentDiscountTable"; import PaymentDiscountTable from "./PaymentDiscountTable";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import { useEffect, useState } from "react"; import { useState } from "react";
import { actions, selectors } from "@/store"; import { selectors } from "@/store";
import { useDispatch, useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall"; import { EPlacementKeys } from "@/api/resources/Paywall";
import PaymentModal from "@/components/PaymentModal"; import PaymentModal from "@/components/PaymentModal";
function TrialPaymentWithDiscount() { function TrialPaymentWithDiscount() {
const dispatch = useDispatch();
const { products } = usePaywall({
placementKey: EPlacementKeys["aura.placement.secret.discount"],
});
const productFromStore = useSelector(selectors.selectActiveProduct);
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false); const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const activeProduct = useSelector(selectors.selectActiveProduct);
const handleClose = () => { const handleClose = () => {
setIsOpenPaymentModal(false); setIsOpenPaymentModal(false);
}; };
useEffect(() => {
if (!products.length) return;
const activeProduct = products.find(
(p) => p.trialPrice === productFromStore?.trialPrice
);
if (!activeProduct) {
dispatch(actions.payment.update({ activeProduct: products[0] }));
}
if (activeProduct) {
dispatch(actions.payment.update({ activeProduct }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, products]);
return ( return (
<section className={`${styles.page} page`}> <section className={`${styles.page} page`}>
<Modal open={isOpenPaymentModal} onClose={handleClose} type="hidden"> <Modal open={isOpenPaymentModal} onClose={handleClose} type="hidden">
@ -56,14 +37,14 @@ function TrialPaymentWithDiscount() {
className={styles.button} className={styles.button}
onClick={() => setIsOpenPaymentModal(true)} onClick={() => setIsOpenPaymentModal(true)}
> >
Start your 3-day trial Start your {activeProduct?.trialDuration}-day trial
</MainButton> </MainButton>
<p className={styles.policy}> <p className={styles.policy}>
By continuing you agree that if you don't cancel prior to the end of the By continuing you agree that if you don't cancel prior to the end of the{" "}
3-days trial, you will automatically be charged $ {activeProduct?.trialDuration}-days trial, you will automatically be
{(productFromStore?.price || 0) / 100} for the introductory period of 14 charged ${(activeProduct?.price || 0) / 100} for the introductory
days thereafter the standard rate of $ period of 14 days thereafter the standard rate of $
{(productFromStore?.price || 0) / 100} every 14 days until you cancel in {(activeProduct?.price || 0) / 100} every 14 days until you cancel in
settings. Learn more about cancellation and refund policy in settings. Learn more about cancellation and refund policy in
Subscription terms. Subscription terms.
</p> </p>

View File

@ -4,9 +4,11 @@ interface IBackButtonProps {
onClick?: () => void; onClick?: () => void;
} }
function BackButton(props: IBackButtonProps) { function BackButton(
props: IBackButtonProps & React.HTMLAttributes<HTMLButtonElement>
) {
return ( return (
<button className={styles.button} onClick={props.onClick}> <button className={styles.button} onClick={props.onClick} {...props}>
<svg <svg
width="11" width="11"
height="20" height="20"

View File

@ -7,14 +7,15 @@ import PaymentModal from "@/components/PaymentModal";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { usePaywall } from "@/hooks/paywall/usePaywall"; import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall"; import { EPlacementKeys } from "@/api/resources/Paywall";
import { useDispatch } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { actions } from "@/store"; import { actions, selectors } from "@/store";
function MarketingTrialPayment() { function MarketingTrialPayment() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false); const [isOpenPaymentModal, setIsOpenPaymentModal] = useState<boolean>(false);
const activeProduct = useSelector(selectors.selectActiveProduct);
const { products } = usePaywall({ const { products, getText } = usePaywall({
placementKey: EPlacementKeys["aura.placement.email.marketing"], placementKey: EPlacementKeys["aura.placement.email.marketing"],
}); });
@ -48,7 +49,7 @@ function MarketingTrialPayment() {
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.banner}>Special Offer</div> <div className={styles.banner}>Special Offer</div>
<Title variant="h2" className={styles.title}> <Title variant="h2" className={styles.title}>
Start your 7-day trial Start your {activeProduct?.trialDuration}-day trial
</Title> </Title>
<p className={styles.description}>No pressure. Cancel anytime</p> <p className={styles.description}>No pressure. Cancel anytime</p>
<div className={styles["total-today"]}> <div className={styles["total-today"]}>
@ -67,16 +68,16 @@ function MarketingTrialPayment() {
Your cost per 2 weeks after trial Your cost per 2 weeks after trial
</p> </p>
<p className={styles.value}> <p className={styles.value}>
<span className={styles["old-price"]}>$29</span> <span className={styles["old-price"]}>${Number(getText("full.price")) / 100}</span>
<span className={styles["new-price"]}>$19</span> <span className={styles["new-price"]}>${(activeProduct?.price || 0) / 100}</span>
</p> </p>
</div> </div>
<p className={styles["sale-description"]}>Save $10 every period</p> <p className={styles["sale-description"]}>{getText("text.save")}</p>
<div className={styles.line} /> <div className={styles.line} />
<p className={styles["text-description"]}> <p className={styles["text-description"]}>
You will be charged only{" "} You will be charged only{" "}
<b> <b>
${(products[0]?.trialPrice / 100).toFixed(2) || 0} for your 7-day ${(products[0]?.trialPrice / 100).toFixed(2) || 0} for your {activeProduct?.trialDuration}-day
trial. trial.
</b>{" "} </b>{" "}
Subscription <b>renews automatically</b> until cancelled. You{" "} Subscription <b>renews automatically</b> until cancelled. You{" "}
@ -87,8 +88,8 @@ function MarketingTrialPayment() {
<p>Get access</p> <p>Get access</p>
</MainButton> </MainButton>
<p className={styles.policy}> <p className={styles.policy}>
By continuing you agree that if you don't cancel prior to the end of By continuing you agree that if you don`t cancel prior to the end of
the 3-days trial, you will automatically be charged $19 every 2 the {activeProduct?.trialDuration}-days trial, you will automatically be charged ${(activeProduct?.price || 0) / 100} every 2
weeks until you cancel in settings. Learn more about cancellation weeks until you cancel in settings. Learn more about cancellation
and refund policy in Subscription terms and refund policy in Subscription terms
</p> </p>

View File

@ -125,7 +125,7 @@ export default function PaymentScreen() {
</div> </div>
</div> </div>
<h1 className="payment-screen__title">Start your 7-day trial</h1> <h1 className="payment-screen__title">Start your {activeProductFromStore?.trialDuration}-day trial</h1>
<div className="payment-screen__total-today"> <div className="payment-screen__total-today">
<span>Total today</span> <span>Total today</span>
@ -163,7 +163,7 @@ export default function PaymentScreen() {
<div className="payment-screen__prices"> <div className="payment-screen__prices">
<span> <span>
You will be charged only{" "} You will be charged only{" "}
<b>${getFormattedPrice(trialPrice)} for your 7-day trial.</b> <b>${getFormattedPrice(trialPrice)} for your {activeProductFromStore?.trialDuration}-day trial.</b>
</span> </span>
<span> <span>

View File

@ -83,14 +83,12 @@ export function usePaywall({ placementKey }: IUsePaywallProps) {
}, [getPaywallByPlacementKey, placementKey, isMustUpdate]); }, [getPaywallByPlacementKey, placementKey, isMustUpdate]);
const getText = useCallback( const getText = useCallback(
( (key: string, options?: IGetTextProps) => {
key: string, const {
{
replacementSelector = "span", replacementSelector = "span",
color = "inherit", color = "inherit",
replacement, replacement,
}: IGetTextProps } = options || {};
) => {
const property = properties.find((property) => property.key === key); const property = properties.find((property) => property.key === key);
if (!property) return ""; if (!property) return "";
const text = property.value; const text = property.value;

View File

@ -1,5 +1,7 @@
@import "slick-carousel/slick/slick.css"; @import "slick-carousel/slick/slick.css";
@import "slick-carousel/slick/slick-theme.css"; @import "slick-carousel/slick/slick-theme.css";
@import 'react-pdf/dist/Page/AnnotationLayer.css';
@import 'react-pdf/dist/Page/TextLayer.css';
* { * {
box-sizing: border-box; box-sizing: border-box;

View File

@ -11,6 +11,10 @@ import { LegalContext, buildLegal } from "./legal";
import { getClientLocale, buildResources, fallbackLng } from "./locales"; import { getClientLocale, buildResources, fallbackLng } from "./locales";
import App from "./components/App"; import App from "./components/App";
import metricService from "./services/metric/metricService"; import metricService from "./services/metric/metricService";
import "core-js/actual";
import { pdfjs } from "react-pdf";
pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.js`;
const environments = import.meta.env; const environments = import.meta.env;

View File

@ -278,6 +278,10 @@ const routes = {
getUserVideos: () => getUserVideos: () =>
[dApiHost, "users", "videos", "combined"].join("/"), [dApiHost, "users", "videos", "combined"].join("/"),
// User videos
getUserPDFCompatibility: () =>
[dApiHost, "users", "pdf", "compatibility"].join("/"),
}, },
openAi: { openAi: {
createThread: () => [openAIHost, openAiPrefix, "threads"].join("/"), createThread: () => [openAIHost, openAiPrefix, "threads"].join("/"),