diff --git a/public/trial-choice-preview.png b/public/trial-choice-preview.png new file mode 100644 index 0000000..1281c33 Binary files /dev/null and b/public/trial-choice-preview.png differ diff --git a/public/trial-choice.MOV b/public/trial-choice.MOV new file mode 100644 index 0000000..01fd269 Binary files /dev/null and b/public/trial-choice.MOV differ diff --git a/src/components/PalmistryV1/components/Answer/styles.module.scss b/src/components/PalmistryV1/components/Answer/styles.module.scss index 820fbb0..57f6408 100644 --- a/src/components/PalmistryV1/components/Answer/styles.module.scss +++ b/src/components/PalmistryV1/components/Answer/styles.module.scss @@ -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); } } diff --git a/src/components/PalmistryV1/pages/Email/index.tsx b/src/components/PalmistryV1/pages/Email/index.tsx index 10bcd21..ef4372e 100644 --- a/src/components/PalmistryV1/pages/Email/index.tsx +++ b/src/components/PalmistryV1/pages/Email/index.tsx @@ -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"]); }; diff --git a/src/components/PalmistryV1/pages/FindHappiness/styles.module.scss b/src/components/PalmistryV1/pages/FindHappiness/styles.module.scss index b6e523f..a94b339 100644 --- a/src/components/PalmistryV1/pages/FindHappiness/styles.module.scss +++ b/src/components/PalmistryV1/pages/FindHappiness/styles.module.scss @@ -45,6 +45,9 @@ } .button-container { + display: flex; + flex-direction: column; + align-items: center; width: 100%; position: sticky; bottom: 0dvh; diff --git a/src/components/PalmistryV1/pages/Payment/index.tsx b/src/components/PalmistryV1/pages/Payment/index.tsx index 3a4dbe3..950a84e 100644 --- a/src/components/PalmistryV1/pages/Payment/index.tsx +++ b/src/components/PalmistryV1/pages/Payment/index.tsx @@ -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>; +} 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(); + 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")} )} - ); } diff --git a/src/components/pages/ABDesign/v1/pages/Camera/index.tsx b/src/components/pages/ABDesign/v1/pages/Camera/index.tsx new file mode 100644 index 0000000..d8db674 --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/Camera/index.tsx @@ -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 && ( + console.log("close")} + onTakePhoto={onTakePhoto} + /> + )} + {isLoading && ( + + )} + + ); +} + +export default Camera; diff --git a/src/components/pages/ABDesign/v1/pages/Camera/styles.module.scss b/src/components/pages/ABDesign/v1/pages/Camera/styles.module.scss new file mode 100644 index 0000000..7459518 --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/Camera/styles.module.scss @@ -0,0 +1,6 @@ +.loader { + transform: translate(-50%, -50%); + position: absolute; + top: 50%; + left: 50%; +} diff --git a/src/components/pages/ABDesign/v1/pages/FindHappiness/index.tsx b/src/components/pages/ABDesign/v1/pages/FindHappiness/index.tsx new file mode 100644 index 0000000..69bc3d3 --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/FindHappiness/index.tsx @@ -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 ( +
+
+
+
+ darts +
    +
  1. {translate("/find-your-happiness.point1")}
  2. +
  3. + {translate("/find-your-happiness.point2")} +
  4. +
+
+
+ +
    +
  1. {translate("/find-your-happiness.point3")}
  2. +
  3. {translate("/find-your-happiness.point4")}
  4. +
+
+
+ Hand with lines + + {translate("/find-your-happiness.title")} + +
+ navigate(routes.client.scanInstructionV1())} + > + {translate("next")} + +
+

+ {translate("/find-your-happiness.text")} +

+
+ ); +} + +export default FindHappiness; diff --git a/src/components/pages/ABDesign/v1/pages/FindHappiness/styles.module.scss b/src/components/pages/ABDesign/v1/pages/FindHappiness/styles.module.scss new file mode 100644 index 0000000..9739b3a --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/FindHappiness/styles.module.scss @@ -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); +} diff --git a/src/components/pages/ABDesign/v1/pages/LoadingProfile/index.tsx b/src/components/pages/ABDesign/v1/pages/LoadingProfile/index.tsx index a509f66..038b6df 100644 --- a/src/components/pages/ABDesign/v1/pages/LoadingProfile/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/LoadingProfile/index.tsx @@ -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(); const pointsRef = useRef([]); + 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) => { diff --git a/src/components/pages/ABDesign/v1/pages/Onboarding/index.tsx b/src/components/pages/ABDesign/v1/pages/Onboarding/index.tsx index 9746f4b..da5c613 100644 --- a/src/components/pages/ABDesign/v1/pages/Onboarding/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/Onboarding/index.tsx @@ -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) { diff --git a/src/components/pages/ABDesign/v1/pages/QuestionnaireIntermediate/styles.module.css b/src/components/pages/ABDesign/v1/pages/QuestionnaireIntermediate/styles.module.css index 9e509e4..318da06 100644 --- a/src/components/pages/ABDesign/v1/pages/QuestionnaireIntermediate/styles.module.css +++ b/src/components/pages/ABDesign/v1/pages/QuestionnaireIntermediate/styles.module.css @@ -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%; + } } \ No newline at end of file diff --git a/src/components/pages/ABDesign/v1/pages/ScanInstruction/index.tsx b/src/components/pages/ABDesign/v1/pages/ScanInstruction/index.tsx new file mode 100644 index 0000000..79b5da3 --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/ScanInstruction/index.tsx @@ -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 ( +
+
+ + {translate("/scan-instruction.title")} + + + + {translate("/scan-instruction.button")} + + +
+ ); +} + +export default ScanInstruction; diff --git a/src/components/pages/ABDesign/v1/pages/ScanInstruction/styles.module.scss b/src/components/pages/ABDesign/v1/pages/ScanInstruction/styles.module.scss new file mode 100644 index 0000000..b31b47b --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/ScanInstruction/styles.module.scss @@ -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; +} diff --git a/src/components/pages/ABDesign/v1/pages/ScannedPhoto/index.tsx b/src/components/pages/ABDesign/v1/pages/ScannedPhoto/index.tsx new file mode 100644 index 0000000..bd65d53 --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/ScannedPhoto/index.tsx @@ -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(); + + 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 ( +
+
+ + {title} + + +

+ We are putting together a comprehensive Palmistry Reading just for you! +

+ +

+ Wow, looks like there is a lot we can tell about your ambitious and + strong self-confident future. +

+
+ ); +} + +export default ScannedPhoto; diff --git a/src/components/pages/ABDesign/v1/pages/ScannedPhoto/styles.module.scss b/src/components/pages/ABDesign/v1/pages/ScannedPhoto/styles.module.scss new file mode 100644 index 0000000..986870f --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/ScannedPhoto/styles.module.scss @@ -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; + } +} diff --git a/src/components/pages/ABDesign/v1/pages/TrialChoiceVideo/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialChoiceVideo/index.tsx new file mode 100644 index 0000000..d6fa563 --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/TrialChoiceVideo/index.tsx @@ -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(); + + 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 ( +
+ +
+ + {!isLoading && isVisibleElements && ( + <> + {isShowTimer && ( + + )} + +
+ +

+ {getText("text.3", { + color: "#1C38EA", + })} +

+ {`Arrow +
+
+ +
+

{email}

+ {!isDisabled && + displayOptionButton === EDisplayOptionButton.visibleIfChosen && ( + + {getText("text.button.1", { + color: "#1C38EA", + })} + + )} + {displayOptionButton === EDisplayOptionButton.alwaysVisible && ( + + + {getText("text.button.1", { + color: "#1C38EA", + })} + + + )} +

+ {getText("text.4", { + color: "#1C38EA", + })} +

+ + )} + {visibleToast && isDisabled && ( + + {translate("/trial-choice.button")} + {/* Choose an amount that you think is reasonable. */} + + )} + {isLoading && } +
+ ); +} + +export default TrialChoiceVideoPage; diff --git a/src/components/pages/ABDesign/v1/pages/TrialChoiceVideo/styles.module.scss b/src/components/pages/ABDesign/v1/pages/TrialChoiceVideo/styles.module.scss new file mode 100644 index 0000000..677b889 --- /dev/null +++ b/src/components/pages/ABDesign/v1/pages/TrialChoiceVideo/styles.module.scss @@ -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); + } +} diff --git a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalVideo/index.tsx b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalVideo/index.tsx index 80867ff..f852e9f 100644 --- a/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalVideo/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/TrialPayment/components/PersonalVideo/index.tsx @@ -10,10 +10,18 @@ interface IPersonalVideoProps { gender: string; url: string; classNameContainer?: string; + isVisibleControllers?: boolean; + onVideoStart?: () => void; } const PersonalVideo = React.memo( - ({ 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( const onStart = () => { setIsStarted(true); + if (onVideoStart) onVideoStart(); metricService.reachGoal(EGoals.ROSE_VIDEO_PLAY_START); }; @@ -72,7 +81,7 @@ const PersonalVideo = React.memo( aspectRatio: "16 / 9", }} /> - {!isError && isStarted && ( + {!isError && isStarted && isVisibleControllers && ( { 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) { diff --git a/src/hooks/translations/index.tsx b/src/hooks/translations/index.tsx index 36ad78b..c218f3b 100644 --- a/src/hooks/translations/index.tsx +++ b/src/hooks/translations/index.tsx @@ -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] ); diff --git a/src/locales/index.ts b/src/locales/index.ts index 7006acb..96dc2a1 100644 --- a/src/locales/index.ts +++ b/src/locales/index.ts @@ -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 => { @@ -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) } diff --git a/src/routerComponents/ABDesign/v1/index.tsx b/src/routerComponents/ABDesign/v1/index.tsx index 66c2cb3..ee8dd19 100644 --- a/src/routerComponents/ABDesign/v1/index.tsx +++ b/src/routerComponents/ABDesign/v1/index.tsx @@ -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() { }> }> - } /> + } /> } /> + } + /> } @@ -157,6 +166,19 @@ function ABDesignV1Routes() { path={routes.client.mentionedInV1()} element={} /> + } + /> + } + /> + } + /> + } /> ); diff --git a/src/routerComponents/Palmistry/v1/LayoutPalmistryV1/index.tsx b/src/routerComponents/Palmistry/v1/LayoutPalmistryV1/index.tsx index afee948..7b4e529 100644 --- a/src/routerComponents/Palmistry/v1/LayoutPalmistryV1/index.tsx +++ b/src/routerComponents/Palmistry/v1/LayoutPalmistryV1/index.tsx @@ -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(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() { /> {/* }> */}
- + + {!!token.length && !!activeProductFromStore && ( + + )}
{/*
*/} diff --git a/src/routerComponents/Palmistry/v1/LayoutPalmistryV1/styles.module.css b/src/routerComponents/Palmistry/v1/LayoutPalmistryV1/styles.module.css index e75f70b..5a4a749 100644 --- a/src/routerComponents/Palmistry/v1/LayoutPalmistryV1/styles.module.css +++ b/src/routerComponents/Palmistry/v1/LayoutPalmistryV1/styles.module.css @@ -22,4 +22,12 @@ display: flex; flex-direction: column; align-items: center; +} + +.payment-modal-hide { + transform: translateY(150%); +} + +.payment-modal-active { + animation: appearance 1s; } \ No newline at end of file diff --git a/src/routes.ts b/src/routes.ts index d576448..aa47285 100755 --- a/src/routes.ts +++ b/src/routes.ts @@ -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("/"), diff --git a/src/services/metric/metricService.ts b/src/services/metric/metricService.ts index 48ba77f..afa9612 100644 --- a/src/services/metric/metricService.ts +++ b/src/services/metric/metricService.ts @@ -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 = () => { diff --git a/src/utils/FBMetaPixel/index.tsx b/src/utils/FBMetaPixel/index.tsx index 59160ed..2f96f3e 100644 --- a/src/utils/FBMetaPixel/index.tsx +++ b/src/utils/FBMetaPixel/index.tsx @@ -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 ( - + );