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:
commit
ff9cf9d017
BIN
public/trial-choice-preview.png
Normal file
BIN
public/trial-choice-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/trial-choice.MOV
Normal file
BIN
public/trial-choice.MOV
Normal file
Binary file not shown.
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"]);
|
||||
};
|
||||
|
||||
|
||||
@ -45,6 +45,9 @@
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
bottom: 0dvh;
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
83
src/components/pages/ABDesign/v1/pages/Camera/index.tsx
Normal file
83
src/components/pages/ABDesign/v1/pages/Camera/index.tsx
Normal 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;
|
||||
@ -0,0 +1,6 @@
|
||||
.loader {
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
@ -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) => {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
126
src/components/pages/ABDesign/v1/pages/ScannedPhoto/index.tsx
Normal file
126
src/components/pages/ABDesign/v1/pages/ScannedPhoto/index.tsx
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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]
|
||||
);
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -22,4 +22,12 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.payment-modal-hide {
|
||||
transform: translateY(150%);
|
||||
}
|
||||
|
||||
.payment-modal-active {
|
||||
animation: appearance 1s;
|
||||
}
|
||||
@ -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("/"),
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user