Merge branch 'develop' into 'main'

Develop - Palmistry in Aura and preload payment methods in palmistry

See merge request witapp/aura-webapp!398
This commit is contained in:
Daniil Chemerkin 2024-09-16 22:45:05 +00:00
commit ff9cf9d017
29 changed files with 1214 additions and 31 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
public/trial-choice.MOV Normal file

Binary file not shown.

View File

@ -20,6 +20,6 @@
linear-gradient(-45deg, #3a617120 9%, #21212120 72%, #21895120 96%);
background-blend-mode: color;
color: #fff;
// transform: scale(1.02);
// transform: scale(1.03);
}
}

View File

@ -54,6 +54,11 @@ function Email() {
const authorize = async () => {
metricService.reachGoal(EGoals.LEAD, [EMetrics.FACEBOOK]);
metricService.reachGoal(EGoals.ENTERED_EMAIL, [
EMetrics.KLAVIYO,
EMetrics.YANDEX,
EMetrics.FACEBOOK,
]);
await authorization(email, ESourceAuthorization["aura.palmistry.new"]);
};

View File

@ -45,6 +45,9 @@
}
.button-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
position: sticky;
bottom: 0dvh;

View File

@ -5,13 +5,22 @@ import { selectors } from "@/store";
import { getFormattedPrice } from "@/utils/price.utils";
import Guarantees from "../../components/Guarantees";
import Button from "../../components/Button";
import PaymentModal from "../../components/PaymentModal";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useEffect } from "react";
import {
useNavigate,
useOutletContext,
useSearchParams,
} from "react-router-dom";
import routes from "@/routes";
import { addCurrency, ELocalesPlacement } from "@/locales";
import { useTranslations } from "@/hooks/translations";
import Stars from "../../components/Stars";
import metricService, { EGoals } from "@/services/metric/metricService";
interface IPaymentContext {
isShowPaymentModal: boolean;
setIsShowPaymentModal: React.Dispatch<React.SetStateAction<boolean>>;
}
function Payment() {
const { translate } = useTranslations(ELocalesPlacement.PalmistryV1);
@ -20,7 +29,9 @@ function Payment() {
const currency = useSelector(selectors.selectCurrency);
const trialPrice = activeProductFromStore?.trialPrice || 0;
const fullPrice = activeProductFromStore?.price || 0;
const [isShowPaymentModal, setIsShowPaymentModal] = useState(false);
const { isShowPaymentModal, setIsShowPaymentModal } =
useOutletContext<IPaymentContext>();
const [searchParams] = useSearchParams();
const subscriptionStatus =
searchParams.get("redirect_status") === "succeeded" ? "subscribed" : "lead";
@ -31,6 +42,7 @@ function Payment() {
useEffect(() => {
if (subscriptionStatus !== "subscribed") return;
metricService.reachGoal(EGoals.PAYMENT_SUCCESS);
const timer = setTimeout(() => {
navigate(routes.client.skipTrial());
}, 1500);
@ -80,13 +92,6 @@ function Payment() {
{translate("/payment.get_personal_prediction")}
</Button>
)}
<PaymentModal
className={
isShowPaymentModal || subscriptionStatus === "subscribed"
? styles["payment-modal-active"]
: styles["payment-modal-hide"]
}
/>
</>
);
}

View File

@ -0,0 +1,83 @@
import PalmCameraModal from "@/components/palmistry/palm-camera-modal/palm-camera-modal";
import styles from "./styles.module.scss";
import { DataURIToBlob } from "@/services/data";
import { useApi } from "@/api";
import { IPalmistryFinger } from "@/api/resources/Palmistry";
import { IPalmistryFingerLocal } from "@/store/palmistry";
import { useDispatch } from "react-redux";
import { actions } from "@/store";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import { useState } from "react";
import Loader, { LoaderColor } from "@/components/Loader";
const fingersNames = {
thumb: "Thumb finger",
index_finger: "Index finger",
middle_finger: "Middle finger",
ring_finger: "Ring finger",
pinky: "Little finger",
};
const setFingersNames = (
fingers: IPalmistryFinger[]
): IPalmistryFingerLocal[] => {
if (!fingers) return [];
return fingers.map((finger) => {
return {
...finger,
fingerName: fingersNames[finger.name as keyof typeof fingersNames],
};
});
};
function Camera() {
const navigate = useNavigate();
const api = useApi();
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(false);
const getLines = async (file: File | Blob) => {
setIsLoading(true);
const formData = new FormData();
formData.append("file", file);
const result = await api.getPalmistryLines({ formData });
const fingers = setFingersNames(result?.fingers);
dispatch(
actions.palmistry.update({
lines: result?.lines,
fingers,
})
);
setIsLoading(false);
};
const onTakePhoto = async (photo: string) => {
// setIsUpladProcessing(true);
const file = DataURIToBlob(photo);
await getLines(file);
// setPalmPhoto(photo as string);
dispatch(
actions.palmistry.update({
photo,
})
);
navigate(routes.client.scannedPhotoV1());
};
return (
<>
{!isLoading && (
<PalmCameraModal
onClose={() => console.log("close")}
onTakePhoto={onTakePhoto}
/>
)}
{isLoading && (
<Loader className={styles.loader} color={LoaderColor.Black} />
)}
</>
);
}
export default Camera;

View File

@ -0,0 +1,6 @@
.loader {
transform: translate(-50%, -50%);
position: absolute;
top: 50%;
left: 50%;
}

View File

@ -0,0 +1,76 @@
import routes, { palmistryV1Prefix } from "@/routes";
import styles from "./styles.module.scss";
import Title from "@/components/Title";
import { useLocation, useNavigate } from "react-router-dom";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import { useDispatch } from "react-redux";
import { useEffect } from "react";
import { actions } from "@/store";
// import StarSVG from "../../images/SVG/Star";
import StarSVG from "@/components/PalmistryV1/images/SVG/Star";
import Header from "../../components/Header";
import QuestionnaireGreenButton from "../../ui/GreenButton";
function FindHappiness() {
const navigate = useNavigate();
const dispatch = useDispatch();
const location = useLocation();
const { translate } = useTranslations(ELocalesPlacement.PalmistryV1);
useEffect(() => {
const feature = location.pathname.replace(
routes.client.palmistryV1Welcome(),
""
);
dispatch(
actions.userConfig.setFeature(
feature.includes("/v1/gender") ? "" : feature
)
);
}, [dispatch, location.pathname]);
return (
<section className={`${styles.page} page`}>
<Header className={styles.header} />
<div className={styles["blocks-container"]}>
<div className={styles.block}>
<img src={`${palmistryV1Prefix}/darts.png`} alt="darts" />
<ol>
<li>{translate("/find-your-happiness.point1")}</li>
<li>
<b>{translate("/find-your-happiness.point2")}</b>
</li>
</ol>
</div>
<div className={styles.block}>
<StarSVG />
<ol>
<li>{translate("/find-your-happiness.point3")}</li>
<li>{translate("/find-your-happiness.point4")}</li>
</ol>
</div>
</div>
<img
className={styles.image}
src={`${palmistryV1Prefix}/hand-with-lines.png`}
alt="Hand with lines"
/>
<Title variant="h2" className={styles.title}>
{translate("/find-your-happiness.title")}
</Title>
<div className={styles["button-container"]}>
<QuestionnaireGreenButton
onClick={() => navigate(routes.client.scanInstructionV1())}
>
{translate("next")}
</QuestionnaireGreenButton>
</div>
<p className={styles.description}>
{translate("/find-your-happiness.text")}
</p>
</section>
);
}
export default FindHappiness;

View File

@ -0,0 +1,55 @@
.blocks-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-top: 24px;
& > .block {
display: flex;
flex-direction: row;
align-items: center;
height: 52px;
padding: 12px 9px;
border: solid 2px #3871c1;
border-radius: 10px;
gap: 6px;
& > ol {
list-style-type: disc;
padding-left: 15px;
& > li {
margin-bottom: 2px;
font-size: 12px;
}
}
}
}
.image {
width: 100%;
max-width: 250px;
margin-top: -21px;
min-height: 341px;
}
.title {
margin-bottom: 0;
}
.description {
text-align: center;
font-size: 14px;
}
.button-container {
width: 100%;
position: sticky;
bottom: 0dvh;
padding: 16px 0;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
}

View File

@ -20,6 +20,7 @@ import LoadingProfileModalChild from "../../components/LoadingProfileModalChild"
import ProgressBarSubstrate from "./ProgressBarSubstrate";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import { useMetricABFlags } from "@/services/metric/metricService";
function LoadingProfilePage() {
// const userDeviceType = useSelector(selectors.selectUserDeviceType);
@ -32,13 +33,18 @@ function LoadingProfilePage() {
const [isPause, setIsPause] = useState(false);
const interval = useRef<NodeJS.Timeout>();
const pointsRef = useRef<HTMLDivElement[]>([]);
const { flags } = useMetricABFlags();
const onEndLoading = useCallback(() => {
// if (isShowTryApp && userDeviceType === EUserDeviceType.ios) {
// return navigate(routes.client.tryApp());
// }
if (flags?.auraPalmistry?.[0] === "on") {
return navigate(routes.client.findHappinessV1());
}
return navigate(routes.client.emailEnterV1());
}, [navigate]);
}, [flags?.auraPalmistry, navigate]);
const getProgressValue = useCallback(
(index: number) => {

View File

@ -10,7 +10,10 @@ import { EPlacementKeys } from "@/api/resources/Paywall";
import { usePersonalVideo } from "@/hooks/personalVideo/usePersonalVideo";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import metricService, { EGoals } from "@/services/metric/metricService";
import metricService, {
EGoals,
useMetricABFlags,
} from "@/services/metric/metricService";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
@ -32,13 +35,18 @@ function OnboardingPage() {
selectors.selectPersonalVideo
);
const authCode = useSelector(selectors.selectAuthCode);
const { flags } = useMetricABFlags();
const auraVideoTrial = flags?.auraVideoTrial?.[0];
const handleNext = useCallback(() => {
if (auraVideoTrial === "on") {
return navigate(routes.client.trialChoiceVideoV1());
}
if (authCode?.length) {
return navigate(routes.client.tryAppV1());
}
return navigate(routes.client.trialChoiceV1());
}, [authCode, navigate]);
}, [auraVideoTrial, authCode?.length, navigate]);
useEffect(() => {
if (isVideoReady && progress >= 100) {

View File

@ -60,10 +60,12 @@
}
.buttons-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
@ -88,4 +90,10 @@
.lottie-animation {
width: 100%;
aspect-ratio: 401 / 242;
}
@media screen and (max-width: 393px) {
.button {
width: 100%;
}
}

View File

@ -0,0 +1,35 @@
import Title from "@/components/Title";
import styles from "./styles.module.scss";
import routes from "@/routes";
import BiometricData from "@/components/palmistry/biometric-data/biometric-data";
import { useNavigate } from "react-router-dom";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import ScanInstructionSVG from "@/components/PalmistryV1/images/SVG/ScanInstruction";
import QuestionnaireGreenButton from "../../ui/GreenButton";
import Header from "../../components/Header";
function ScanInstruction() {
const { translate } = useTranslations(ELocalesPlacement.PalmistryV1);
const navigate = useNavigate()
const handleClick = () => {
navigate(routes.client.cameraV1());
};
return (
<section className={`${styles.page} page`}>
<Header className={styles.header} />
<Title variant="h2" className={styles.title}>
{translate("/scan-instruction.title")}
</Title>
<ScanInstructionSVG />
<QuestionnaireGreenButton className={styles.button} onClick={handleClick}>
{translate("/scan-instruction.button")}
</QuestionnaireGreenButton>
<BiometricData className={styles.biometric} />
</section>
);
}
export default ScanInstruction;

View File

@ -0,0 +1,31 @@
.page {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.button {
margin-top: 40px;
}
.description {
font-style: 14px;
line-height: 125%;
text-align: center;
margin-top: 20px;
}
.biometric {
text-align: center;
line-height: 125%;
margin-top: 20px;
& > svg {
display: none;
}
}
.title {
margin-top: 24px;
}

View File

@ -0,0 +1,126 @@
import { useSelector } from "react-redux";
import styles from "./styles.module.scss";
import { selectors } from "@/store";
import { useEffect, useMemo, useRef, useState } from "react";
import { IPalmistryLine } from "@/api/resources/Palmistry";
import Title from "@/components/Title";
import { IPalmistryFingerLocal } from "@/store/palmistry";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import ScannedPhotoElement from "@/components/palmistry/scanned-photo/scanned-photo";
import Header from "../../components/Header";
const drawElementChangeDelay = 1500;
const startDelay = 500;
function ScannedPhoto() {
const navigate = useNavigate();
const photo = useSelector(selectors.selectPalmistryPhoto);
const fingers = useSelector(selectors.selectPalmistryFingers);
const lines = useSelector(selectors.selectPalmistryLines);
const changeTitleTimeOut = useRef<NodeJS.Timeout>();
const [currentElementIndex, setCurrentElementIndex] = useState(0);
const [title, setTitle] = useState("");
const [shouldDisplayPalmLines, setShouldDisplayPalmLines] = useState(false);
const [smallPhotoState, setSmallPhotoState] = useState(false);
const drawElements = useMemo(() => [...fingers, ...lines], [fingers, lines]);
useEffect(() => {
if (!drawElements[currentElementIndex]) return;
changeTitleTimeOut.current = setTimeout(() => {
const title =
(drawElements[currentElementIndex] as IPalmistryFingerLocal)
.fingerName || drawElements[currentElementIndex].name;
setTitle(title);
if (currentElementIndex < drawElements.length - 1) {
setCurrentElementIndex((prevState) => prevState + 1);
}
}, drawElementChangeDelay);
return () => {
if (changeTitleTimeOut.current) {
clearTimeout(changeTitleTimeOut.current);
}
};
}, [currentElementIndex, drawElements]);
useEffect(() => {
setShouldDisplayPalmLines(
lines.includes(drawElements[currentElementIndex] as IPalmistryLine)
);
}, [currentElementIndex, drawElements, lines]);
useEffect(() => {
if (currentElementIndex < drawElements.length - 1) return;
const timer = setTimeout(() => {
setSmallPhotoState(true);
}, drawElementChangeDelay * 2);
const goNextTimer = setTimeout(
() => navigate(routes.client.emailEnterV1()),
drawElementChangeDelay * drawElements.length + 8000
);
return () => {
if (timer) {
clearTimeout(timer);
}
if (goNextTimer) {
clearTimeout(goNextTimer);
}
};
}, [currentElementIndex, drawElements.length, navigate]);
useEffect(() => {
if (currentElementIndex < drawElements.length) return;
const timer = setTimeout(() => {
// navigate(routes.client.palmistryV1Email());
}, drawElementChangeDelay + 1000);
return () => clearTimeout(timer);
}, [currentElementIndex, drawElements.length, navigate]);
return (
<section className={`${styles.page} palmistry-container_type_scan-photo`}>
<Header className={styles.header} />
<Title variant="h2" className={styles.title}>
{title}
</Title>
<ScannedPhotoElement
photo={photo}
small={smallPhotoState}
drawElementChangeDelay={drawElementChangeDelay}
startDelay={startDelay}
displayLines={shouldDisplayPalmLines}
lines={lines}
fingers={fingers}
drawElements={drawElements}
/>
<h2
className="palmistry-container__waiting-title"
style={{
animationDelay: `${
drawElementChangeDelay * drawElements.length + 2500
}ms`,
}}
>
We are putting together a comprehensive Palmistry Reading just for you!
</h2>
<h3
className="palmistry-container__waiting-description"
style={{
animationDelay: `${
drawElementChangeDelay * drawElements.length + 3000
}ms`,
}}
>
Wow, looks like there is a lot we can tell about your ambitious and
strong self-confident future.
</h3>
</section>
);
}
export default ScannedPhoto;

View File

@ -0,0 +1,162 @@
.page {
width: 100%;
padding: 0 16px 74px;
margin: 0 auto;
max-width: 560px;
height: fit-content;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
gap: 40px;
--font-family-main: "SF Pro Text", sans-serif;
--stone-grey: #95959d;
--button-color: #121620;
--svg-blue: var(--strong-blue);
--pale-lavender: #dee5f9;
--pale-lavender-20: #dee5f9;
--orange: #ff9649;
--coral: #ff5c5d;
--rich-blue: #2b7cf6;
--bright-white: #fbfbfb;
--bright-red: #ff5758;
--light-gray: #d9d9d9;
--vivid-yellow: #ffc700;
--pale-gray: #c2cad8;
--pale-green: #75db9c;
--pale-cerulean: #82b7ef;
--greyish: #afafaf;
--vivid-green: #00ff38;
--dark-charcoal: #191f2d;
--blueish-gray: #6b76aa;
--violet: #9949ff;
--light-lavender: #c5c5d1;
--pale-pink: #fcd3df;
--cream-yellow: #fffbcd;
--pale-aqua: #c9fae6;
--pale-seafoam: #d3f1e1;
--pale-lilac: #dec6fe;
--pale-peach: #fdddc8;
--deep-charcoal: #1e1e1e;
--black: #000;
--bright-sea-green: #04a777;
--deep-cornflower-blue: #4663b7;
--charcoal-grey: #505051;
--pale-light-cerulean: #acd1ff;
--main-gradient: #fff;
--strong-blue: #066fde;
--strong-blue-text: #066fde;
--strong-blue-80: rgba(6, 111, 222, 0.8);
--midnight-black: #121620;
--footer-small-text: #121620;
--button-active: #fff;
--button-background: var(--pale-blue);
--button-active-bg: var(--strong-blue);
--slate-blue: #6b7baa;
--slate-blue-placeholder: #6b7baa;
--pale-blue: #eff2fd;
--pale-blue-input: #eff2fd;
--midnight-black-input: #121620;
--greyish-blue: #8e8e93;
--soft-blue: #4a567a;
--soft-blue-gray: #4a567a;
--soft-blue-periwinkle: #4a567a;
--gentle-blue: #9babd9;
--gentle-blue-svg: #9babd9;
--light-silver: #c7c7c7;
--light-silver-to-white: #c7c7c7;
--light-silver-to-lilac-blue: #c7c7c7;
--light-cornflower-blue: #c2ceee;
--white: #fff;
--dark-blue: #202b47;
--progress-line: #00a3ff;
--footer-shield: #b5c4ff;
--blue-color-text: #0066fd;
--black-color-text: #0066fd;
--transparent-to-gold: transparent;
--transparent-to-white: transparent;
--transparent-to-periwinkle: transparent;
--white-to-transparent: #fff;
--loader-background: rgba(16, 32, 77, 0.35);
}
.title {
min-height: 36px;
margin: 0;
font-size: 24px;
&::first-letter {
text-transform: uppercase;
}
}
.photo-container {
width: 100%;
height: fit-content;
position: relative;
// background-color: #cbcbcb;
}
.scanned-photo {
width: 100%;
}
.svg-objects {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
.finger-point {
animation: finger-show 1s linear;
animation-fill-mode: forwards;
transform: scale(0);
transform-origin: center center;
}
.line {
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2px;
fill-rule: evenodd;
clip-rule: evenodd;
stroke-miterlimit: 1.5;
stroke-dasharray: 500;
stroke: #fff;
fill: none;
animation: line-show 1.5s linear;
animation-fill-mode: forwards;
&.heart {
stroke: #f8d90f;
/* animation-delay: 4.5s; */
}
&.life {
stroke: #e51c39;
}
&.head {
stroke: #00d114;
/* animation-delay: 1.5s; */
}
&.fate {
stroke: #05ced8;
/* animation-delay: 3s; */
}
}
@keyframes finger-show {
100% {
transform: scale(1);
}
}
@keyframes line-show {
100% {
stroke-dashoffset: 0;
}
}

View File

@ -0,0 +1,251 @@
import styles from "./styles.module.scss";
import { useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import EmailsList from "@/components/EmailsList";
import Header from "../../components/Header";
import BackgroundTopBlob from "../../ui/BackgroundTopBlob";
import { useDynamicSize } from "@/hooks/useDynamicSize";
import PriceList from "../../components/PriceList";
import QuestionnaireGreenButton from "../../ui/GreenButton";
import { usePaywall } from "@/hooks/paywall/usePaywall";
import { EPlacementKeys } from "@/api/resources/Paywall";
import { getRandomArbitrary } from "@/services/random-value";
import Loader from "@/components/Loader";
import metricService, {
EGoals,
EMetrics,
useMetricABFlags,
} from "@/services/metric/metricService";
import PersonalVideo from "../TrialPayment/components/PersonalVideo";
import Toast from "../../components/Toast";
import BlurComponent from "@/components/BlurComponent";
import { useTranslations } from "@/hooks/translations";
import { ELocalesPlacement } from "@/locales";
import DiscountExpires from "../TrialPayment/components/DiscountExpires";
enum EDisplayOptionButton {
"alwaysVisible" = "alwaysVisible",
"visibleIfChosen" = "visibleIfChosen",
}
const displayOptionButton: EDisplayOptionButton =
EDisplayOptionButton.alwaysVisible; //
function TrialChoiceVideoPage() {
const { translate } = useTranslations(ELocalesPlacement.V1);
const dispatch = useDispatch();
const navigate = useNavigate();
const selectedPrice = useSelector(selectors.selectSelectedPrice);
const homeConfig = useSelector(selectors.selectHome);
const email = useSelector(selectors.selectEmail);
const [isDisabled, setIsDisabled] = useState(true);
const [visibleToast, setVisibleToast] = useState(false);
const [countUsers, setCountUsers] = useState(752);
const [isVisibleElements, setIsVisibleElements] = useState(false);
const { width: pageWidth, elementRef: pageRef } = useDynamicSize({});
const { gender } = useSelector(selectors.selectQuestionnaire);
const { products, isLoading, currency, getText } = usePaywall({
placementKey: EPlacementKeys["aura.placement.redesign.main"],
localesPlacement: ELocalesPlacement.V1,
});
const arrowLeft = useSelector(selectors.selectTrialChoiceArrowOptions)?.left;
const showElementsTimer = useRef<NodeJS.Timeout>();
const { flags } = useMetricABFlags();
const isShowTimer = flags?.showTimerTrial?.[0] === "show";
const { videoUrl } = useSelector(selectors.selectPersonalVideo);
useEffect(() => {
metricService.reachGoal(EGoals.AURA_TRIAL_CHOICE_PAGE_VISIT, [
EMetrics.KLAVIYO,
]);
}, []);
useEffect(() => {
return () => {
if (showElementsTimer.current) clearTimeout(showElementsTimer.current);
};
}, []);
const showElements = () => {
showElementsTimer.current = setTimeout(() => {
setIsVisibleElements(true);
}, 33_000);
};
useEffect(() => {
const randomDelay = getRandomArbitrary(3000, 5000);
const countUsersTimeOut = setTimeout(() => {
setCountUsers((prevState) => prevState + 1);
}, randomDelay);
return () => clearTimeout(countUsersTimeOut);
}, [countUsers]);
const handlePriceItem = () => {
metricService.reachGoal(EGoals.AURA_SELECT_TRIAL);
setIsDisabled(false);
};
const handleNext = () => {
if (isDisabled) {
setVisibleToast(true);
return;
}
dispatch(
actions.siteConfig.update({
home: { pathFromHome: homeConfig.pathFromHome, isShowNavbar: false },
})
);
navigate(routes.client.trialPaymentV1());
};
useEffect(() => {
if (!visibleToast) return;
const timeOut = setTimeout(() => {
setVisibleToast(false);
}, 6000);
return () => clearTimeout(timeOut);
}, [visibleToast]);
return (
<section
className={`${styles.page} page`}
ref={pageRef}
style={{
backgroundColor: gender === "male" ? "#C1E5FF" : "#f7ebff",
paddingTop: !videoUrl.length ? "15px" : "0px",
}}
>
<BackgroundTopBlob
width={pageWidth}
className={styles["background-top-blob"]}
height={180}
/>
<Header className={styles.header} />
<PersonalVideo
gender={gender}
url={"/trial-choice.MOV"}
classNameContainer={styles["personal-video"]}
isVisibleControllers={isVisibleElements}
onVideoStart={showElements}
/>
{!isLoading && isVisibleElements && (
<>
{isShowTimer && (
<DiscountExpires
className={styles["discount-expires"]}
style={{
marginTop: !videoUrl.length
? "60px"
: "calc((100% + 84px) / 16* 9 + 16px)",
}}
/>
)}
<div className={styles["price-container"]}>
<PriceList
products={products}
activeItem={selectedPrice}
classNameItem={styles["price-item"]}
classNameItemActive={`${styles["price-item-active"]} ${styles[gender]}`}
currency={currency}
click={handlePriceItem}
/>
<p
className={styles["auxiliary-text"]}
style={{
maxWidth: arrowLeft
? `${Number(arrowLeft.slice(0, -2)) - 8}px`
: "75%",
}}
>
{getText("text.3", {
color: "#1C38EA",
})}
</p>
<img
className={styles["arrow-image"]}
src="/arrow.svg"
alt={`Arrow to $${products.at(-1)?.trialPrice}`}
style={
arrowLeft
? {
left: arrowLeft,
}
: {}
}
/>
</div>
<div className={styles["emails-list-container"]}>
<EmailsList
title={getText("text.5", {
replacementSelector: "strong",
replacement: [
{
target: "${quantity}",
replacement: countUsers.toString(),
},
],
})}
products={products}
classNameContainer={`${styles["emails-container"]} ${styles[gender]}`}
classNameTitle={styles["emails-title"]}
classNameEmailItem={styles["email-item"]}
direction="right-left"
currency={currency}
/>
</div>
<p className={styles.email}>{email}</p>
{!isDisabled &&
displayOptionButton === EDisplayOptionButton.visibleIfChosen && (
<QuestionnaireGreenButton
className={styles.button}
disabled={isDisabled}
onClick={handleNext}
>
{getText("text.button.1", {
color: "#1C38EA",
})}
</QuestionnaireGreenButton>
)}
{displayOptionButton === EDisplayOptionButton.alwaysVisible && (
<BlurComponent
className={styles.blur}
gradientClassName={styles["gradient-blur"]}
isActiveBlur={true}
>
<QuestionnaireGreenButton
className={`${styles.button} ${
isDisabled ? styles.disabled : ""
}`}
onClick={handleNext}
>
{getText("text.button.1", {
color: "#1C38EA",
})}
</QuestionnaireGreenButton>
</BlurComponent>
)}
<p className={styles["auxiliary-text"]}>
{getText("text.4", {
color: "#1C38EA",
})}
</p>
</>
)}
{visibleToast && isDisabled && (
<Toast classNameContainer={styles["toast-container"]} variant="error">
{translate("/trial-choice.button")}
{/* Choose an amount that you think is reasonable. */}
</Toast>
)}
{isLoading && <Loader className={styles.loader} />}
</section>
);
}
export default TrialChoiceVideoPage;

View File

@ -0,0 +1,229 @@
.page {
display: flex;
flex-direction: column;
gap: 10px;
min-height: 100dvh;
height: fit-content;
background-color: #fff0f0;
padding: 0 42px 126px;
width: 100%;
max-width: 500px;
}
.background-top-blob {
position: absolute;
top: 0;
left: 0;
scale: 1.4;
}
.header {
z-index: 1;
width: calc(100% + 36px) !important;
}
.text {
font-size: 15px;
line-height: 125%;
font-weight: 300;
text-align: center;
}
.text.bold {
font-weight: 600;
}
.blue {
color: #1c38ea;
}
.auxiliary-text {
font-size: 12px;
line-height: 16px;
color: rgb(52, 52, 52);
width: 100%;
text-align: center;
position: sticky;
bottom: 8px;
filter: opacity(0);
will-change: opacity;
animation: appearance 1s forwards 1.5s;
}
.price-container {
position: relative;
width: 100%;
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 20px;
filter: opacity(0);
will-change: opacity;
animation: appearance 1s forwards;
}
.price-item {
background: #fff;
color: rgb(51, 51, 51);
box-shadow: rgba(84, 60, 151, 0.25) 2px 2px 6px;
border-radius: 12px;
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
font-weight: 600;
width: calc((100% - 30px) / 4);
max-width: 72px;
max-height: 72px;
height: auto;
aspect-ratio: 1 / 1;
position: relative;
}
.price-item-active {
color: rgb(251, 251, 255);
}
.price-item-active.male {
background-color: #85b6ff;
}
.price-item-active.female {
background-color: #d1acf2;
}
.arrow-image {
position: absolute;
width: 26px;
height: 33px;
top: 76px;
right: 32px;
}
.emails-list-container {
width: 100%;
margin-top: 20px;
filter: opacity(0);
will-change: opacity;
animation: appearance 1s forwards 0.5s;
}
.emails-container.female {
background-color: #d6bbee;
}
.emails-container.male {
background-color: #85b6ff;
}
.emails-title {
font-weight: 500;
line-height: 125%;
font-size: 15px;
color: #fff;
margin-bottom: 6px;
text-align: center;
}
.email-item {
background: rgb(251, 251, 255);
border-radius: 4px;
padding: 5px 7px;
font-size: 12px;
line-height: 130%;
display: flex;
width: max-content;
color: rgb(79, 79, 79);
}
.button {
font-size: 18px;
min-height: 0;
height: 50px;
position: fixed;
bottom: calc(0dvh + 16px);
width: calc(100% - 84px);
z-index: 10;
filter: opacity(0);
will-change: opacity;
animation: appearance 1s forwards 2s;
}
.blur {
position: fixed !important;
height: unset !important;
bottom: calc(0dvh + 16px);
width: calc(100% - 84px) !important;
max-width: 396px;
}
.gradient-blur {
top: -74px !important;
}
.button.disabled {
opacity: 0.3;
}
.toast-container {
position: fixed;
bottom: calc(0dvh + 82px);
width: calc(100% - 84px);
max-width: 400px;
}
.email {
font-weight: 500;
word-break: break-all;
white-space: normal;
line-height: 1.3;
text-align: center;
filter: opacity(0);
will-change: opacity;
animation: appearance 1s forwards 1s;
}
.loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.personal-video {
// position: fixed !important;
// top: 0dvh;
// z-index: 30;
margin-top: 32px !important;
border-radius: 0 !important;
background-image: url("/trial-choice-preview.png") !important;
}
.discount-expires {
flex-direction: row !important;
gap: 12px;
font-size: 1.5rem;
& > h6 {
font-size: 17px;
}
& > div > p,
& > div > div > span:first-child {
font-size: 20px;
}
& > div > div > span:last-child {
font-size: 10px;
}
}
@keyframes appearance {
0% {
filter: opacity(0);
}
100% {
filter: opacity(1);
}
}

View File

@ -10,10 +10,18 @@ interface IPersonalVideoProps {
gender: string;
url: string;
classNameContainer?: string;
isVisibleControllers?: boolean;
onVideoStart?: () => void;
}
const PersonalVideo = React.memo<IPersonalVideoProps>(
({ url, gender, classNameContainer = "" }) => {
({
url,
gender,
classNameContainer = "",
isVisibleControllers = true,
onVideoStart,
}) => {
const [isPlaying, setIsPlaying] = useState(false);
const [isStarted, setIsStarted] = useState(false);
const [isError, setIsError] = useState(false);
@ -26,6 +34,7 @@ const PersonalVideo = React.memo<IPersonalVideoProps>(
const onStart = () => {
setIsStarted(true);
if (onVideoStart) onVideoStart();
metricService.reachGoal(EGoals.ROSE_VIDEO_PLAY_START);
};
@ -72,7 +81,7 @@ const PersonalVideo = React.memo<IPersonalVideoProps>(
aspectRatio: "16 / 9",
}}
/>
{!isError && isStarted && (
{!isError && isStarted && isVisibleControllers && (
<PlayPauseButton
state={isPlaying ? "pause" : "play"}
onClick={handlePlayPause}

View File

@ -14,6 +14,10 @@ import PaymentModalNew from "@/components/PaymentModalNew";
import { addCurrency } from "@/locales";
import { getPriceCentsToDollars } from "@/services/price";
import routes from "@/routes";
import metricService, {
EGoals,
EMetrics,
} from "@/services/metric/metricService";
export default function PaymentScreen() {
const navigate = useNavigate();
@ -30,13 +34,19 @@ export default function PaymentScreen() {
React.useEffect(() => {
if (subscriptionStatus === "subscribed") {
metricService.reachGoal(EGoals.PAYMENT_SUCCESS);
if (activeProductFromStore) {
metricService.reachGoal(EGoals.PURCHASE, [EMetrics.FACEBOOK], {
currency: "USD",
value: ((activeProductFromStore.trialPrice || 100) / 100).toFixed(2),
});
}
setTimeout(() => {
// steps.goNext();
navigate(routes.client.skipTrial());
}, 1500);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscriptionStatus]);
}, [activeProductFromStore, navigate, subscriptionStatus]);
React.useEffect(() => {
if (!activeProductFromStore) {

View File

@ -44,7 +44,14 @@ export const useTranslations = (
if (_placement === ELocalesPlacement.PalmistryV1) {
_key = prefixGenderKey(prefixPlacementKey(key));
}
return t(_key, options);
const translation = t(_key, options);
if (translation === key) {
return t(`fallback.${_key}`, options);
}
return translation;
},
[placement, prefixGenderKey, prefixPlacementKey, t]
);

View File

@ -61,6 +61,7 @@ interface ITranslationJSON {
male: { [key: string]: string }
female: { [key: string]: string }
default: { [key: string]: string }
fallback: { male: { [key: string]: string }; female: { [key: string]: string }, default: { [key: string]: string } }
}
export const getTranslationJSON = async (placement: ELocalesPlacement | undefined, language: string): Promise<ITranslationJSON> => {
@ -70,12 +71,23 @@ export const getTranslationJSON = async (placement: ELocalesPlacement | undefine
const localePlacement = placement || ELocalesPlacement.V1
let result;
try {
const responseMale = await fetch(`${protocol}//${host}/locales/${localePlacement}/${defaultLanguage}/male_${defaultLanguage}.json`)
const resultMale = await responseMale.json()
const [
resultMale,
resultFemale,
resultMaleFallback,
resultFemaleFallback,
] = await Promise.all([
(await fetch(`${protocol}//${host}/locales/${localePlacement}/${defaultLanguage}/male_${defaultLanguage}.json`)).json(),
(await fetch(`${protocol}//${host}/locales/${localePlacement}/${defaultLanguage}/female_${defaultLanguage}.json`)).json(),
(await fetch(`${protocol}//${host}/locales/${localePlacement}/en/male_en.json`)).json(),
(await fetch(`${protocol}//${host}/locales/${localePlacement}/en/female_en.json`)).json()
]);
const responseFemale = await fetch(`${protocol}//${host}/locales/${localePlacement}/${defaultLanguage}/female_${defaultLanguage}.json`)
const resultFemale = await responseFemale.json()
result = { male: resultMale, female: resultFemale, default: resultMale }
result = {
male: resultMale, female: resultFemale, default: resultMale, fallback: {
male: resultMaleFallback, female: resultFemaleFallback, default: resultMaleFallback
}
}
} catch (error) {
result = await getTranslationJSON(localePlacement, fallbackLng)
}

View File

@ -36,6 +36,11 @@ import MentionedInPage from "@/components/pages/ABDesign/v1/pages/MentionedIn";
import TryAppPage from "@/components/pages/ABDesign/v1/pages/TryApp";
import { useEffect } from "react";
import { ELocalesPlacement } from "@/locales";
import TrialChoiceVideoPage from "@/components/pages/ABDesign/v1/pages/TrialChoiceVideo";
import FindHappiness from "@/components/pages/ABDesign/v1/pages/FindHappiness";
import ScanInstruction from "@/components/pages/ABDesign/v1/pages/ScanInstruction";
import Camera from "@/components/pages/ABDesign/v1/pages/Camera";
import ScannedPhoto from "@/components/pages/ABDesign/v1/pages/ScannedPhoto";
function ABDesignV1Routes() {
useEffect(() => {
@ -46,7 +51,7 @@ function ABDesignV1Routes() {
<Routes>
<Route element={<LayoutABDesignV1 />}>
<Route path={routes.client.genderV1()} element={<GenderPage />}>
<Route path=":targetId*" element={<GenderPage />} />
<Route path=":targetId/*" element={<GenderPage />} />
</Route>
<Route
path={routes.client.questionnaireV1()}
@ -138,6 +143,10 @@ function ABDesignV1Routes() {
path={routes.client.trialChoiceV1()}
element={<TrialChoicePage />}
/>
<Route
path={routes.client.trialChoiceVideoV1()}
element={<TrialChoiceVideoPage />}
/>
<Route
path={routes.client.trialPaymentV1()}
element={<TrialPaymentPage />}
@ -157,6 +166,19 @@ function ABDesignV1Routes() {
path={routes.client.mentionedInV1()}
element={<MentionedInPage />}
/>
<Route
path={routes.client.findHappinessV1()}
element={<FindHappiness />}
/>
<Route
path={routes.client.scanInstructionV1()}
element={<ScanInstruction />}
/>
<Route
path={routes.client.scannedPhotoV1()}
element={<ScannedPhoto />}
/>
<Route path={routes.client.cameraV1()} element={<Camera />} />
</Route>
</Routes>
);

View File

@ -1,9 +1,12 @@
import Header from "@/components/pages/ABDesign/v1/components/Header";
import styles from "./styles.module.css";
import { useSchemeColorByElement } from "@/hooks/useSchemeColorByElement";
import { useRef } from "react";
import { Outlet, useLocation } from "react-router-dom";
import { useRef, useState } from "react";
import { Outlet, useLocation, useSearchParams } from "react-router-dom";
import routes from "@/routes";
import PaymentModal from "@/components/PalmistryV1/components/PaymentModal";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
const isBackButtonVisibleRoutes = [
routes.client.palmistryV1Birthdate(),
@ -24,11 +27,17 @@ const isBackButtonVisibleRoutes = [
];
function LayoutPalmistryV1() {
const token = useSelector(selectors.selectToken);
const activeProductFromStore = useSelector(selectors.selectActiveProduct);
const location = useLocation();
const mainRef = useRef<HTMLDivElement>(null);
useSchemeColorByElement(mainRef.current, "section.page, .page, section", [
location,
]);
const [isShowPaymentModal, setIsShowPaymentModal] = useState(false);
const [searchParams] = useSearchParams();
const subscriptionStatus =
searchParams.get("redirect_status") === "succeeded" ? "subscribed" : "lead";
const getIsBackButtonVisible = () => {
for (const route of isBackButtonVisibleRoutes) {
@ -47,7 +56,16 @@ function LayoutPalmistryV1() {
/>
{/* <Suspense fallback={<LoadingPage />}> */}
<section className={styles.page}>
<Outlet />
<Outlet context={{ isShowPaymentModal, setIsShowPaymentModal }} />
{!!token.length && !!activeProductFromStore && (
<PaymentModal
className={
isShowPaymentModal || subscriptionStatus === "subscribed"
? styles["payment-modal-active"]
: styles["payment-modal-hide"]
}
/>
)}
</section>
{/* </Suspense> */}
</main>

View File

@ -22,4 +22,12 @@
display: flex;
flex-direction: column;
align-items: center;
}
.payment-modal-hide {
transform: translateY(150%);
}
.payment-modal-active {
animation: appearance 1s;
}

View File

@ -221,12 +221,17 @@ const routes = {
emailConfirmV1: () => [host, "v1", "email-confirm"].join("/"),
onboardingV1: () => [host, "v1", "onboarding"].join("/"),
trialChoiceV1: () => [host, "v1", "trial-choice"].join("/"),
trialChoiceVideoV1: () => [host, "v1", "trial-choice-video"].join("/"),
trialPaymentV1: () => [host, "v1", "trial-payment"].join("/"),
tryAppV1: () => [host, "v1", "try-app"].join("/"),
trialPaymentWithDiscountV1: () =>
[host, "v1", "trial-payment-with-discount"].join("/"),
additionalDiscountV1: () => [host, "v1", "additional-discount"].join("/"),
mentionedInV1: () => [host, "v1", "mentionedIn"].join("/"),
findHappinessV1: () => [host, "v1", "find-happiness"].join("/"),
scanInstructionV1: () => [host, "v1", "scan-instruction"].join("/"),
cameraV1: () => [host, "v1", "camera"].join("/"),
scannedPhotoV1: () => [host, "v1", "scanned-photo"].join("/"),
loadingPage: () => [host, "loading-page"].join("/"),
notFound: () => [host, "404"].join("/"),

View File

@ -131,7 +131,9 @@ const initMetricAB = () => {
type TABFlags = {
showTimerTrial: "show" | "hide";
text: "1" | "2" | "3"
text: "1" | "2" | "3";
auraVideoTrial: "on";
auraPalmistry: "on";
}
export const useMetricABFlags = () => {

View File

@ -1,3 +1,4 @@
import { language } from "@/locales";
import routes from "@/routes";
import { Helmet } from "react-helmet";
@ -43,7 +44,7 @@ fbq('track', 'PageView');`;
}
return (
<Helmet>
<Helmet htmlAttributes={{ lang: language }}>
<script>{FBScript}</script>
</Helmet>
);